# SHEP Progressive Disclosure — Complete Spec ("The Fold")

**Date:** 2026-03-24 (original) | **Last updated:** 2026-04-12
**Status:** Implemented and shipping
**Name:** "The Fold" — the product unfolds around the student's behavior

## System Overview

**13 fold items** across 9 phases. Core items (desk, scenarios, help) are always visible. All other items unlock via behavioral triggers, one at a time.

### Current Fold Registry

| Phase | ID | Trigger | Nav Group | Type |
|-------|----|---------|-----------|------|
| 0 | `desk` | always | standalone | nav |
| 0 | `scenarios` | always | standalone | nav |
| 0 | `help` | always | standalone | nav |
| 2 | `resume` | draft_created | standalone | nav |
| 3 | `file-cabinet` | submission_filed_first | — | dashboard |
| 4 | `professor` | evaluation_received (viewed) | practice | nav |
| 4b | `progress` | professor_eval_completed | commons | nav + dashboard |
| 5 | `chips` | submission_filed_with_chips | library | nav |
| 6 | `law` | research_game_completed | library | nav |
| 7 | `litigation` | submission_filed_3_with_args | practice | nav |
| 8 | `messages` | message_received | commons | nav |
| 8 | `people` | follow_event | commons | nav |
| 8 | `feed` | people_then_feed | commons | nav |

### Items NOT in fold (visibility managed elsewhere)

| ID | Visibility mechanism | Why not fold |
|----|---------------------|--------------|
| `quick-practice` (Runs) | `showWhen: () => true` — always visible | Hub page, not progressive |
| `paddock` (Coursework) | `showWhen: data.hasPaddockCourse` — visible when student has a course | Usage-gated, not behavioral |
| NextGen | Admin feature flag (`flags.nextgen`) | Separate product |

### Items removed from fold (2026-04-12)

| ID | Removal reason |
|----|---------------|
| `beginner-scenarios` | Starter lane + desk lifecycle replaced beginner scenario cards |
| `curriculum-wall` | Desk lifecycle surface states replaced fold-driven dashboard layout |
| `search` | Command palette search is sufficient; `/law` covers legal search |
| `ranks` | Consolidated into Progress page tabs |
| `impact` | Consolidated into Progress page tabs |
| `minTier` | Dead metadata — fold system handles all nav gating |

---

## Design Principle

**Every feature appears as a response to something the student did.** Not because a counter ticked. Not in batches. One at a time, with a contextual prompt connecting their action to the new capability. The student never thinks "I unlocked something" — they think "oh, of course that's here now."

Maximum 1 new sidebar item per reveal window. If multiple triggers fire, queue them. A reveal window is defined by a server-side time gap: minimum 1 hour between reveals (stored as `last_reveal_at` in fold state). Contextually-adjacent reveals (e.g., Professor appearing while viewing the evaluation page) may bypass the queue when the context makes it feel natural.

---

## Data Model

```sql
CREATE TABLE user_fold_state (
  user_id        UUID PRIMARY KEY REFERENCES user_profiles(id),
  phase          INT NOT NULL DEFAULT 0,
  unlocked_items JSONB NOT NULL DEFAULT '[]',
  reveal_queue   TEXT[] NOT NULL DEFAULT '{}',
  last_reveal_at TIMESTAMPTZ,
  dismissed_prompts JSONB NOT NULL DEFAULT '{}',
  pinned_items   JSONB NOT NULL DEFAULT '[]',
  safety_net_at  TIMESTAMPTZ,
  created_at     TIMESTAMPTZ NOT NULL DEFAULT now(),
  updated_at     TIMESTAMPTZ NOT NULL DEFAULT now()
);
```

**Note:** `search_count` was removed from the schema (search is no longer fold-gated).

**Event-driven advancement:** Fold state is computed and written when behavioral events happen (submission filed, evaluation viewed, chip tagged), not on every page load. The layout loader reads cached fold state.

---

## Navigation Architecture

Navigation config is static in `navigation.ts`. Two-stage filtering in `NavigationContent.svelte`:

1. **`filterNavByFold(navGroups, unlockedItems)`** — Items pass if they have a `showWhen` predicate (truthy reference) OR their ID is in `unlockedItems`.
2. **Usage-gated filter** — Items with `showWhen` predicates are called with page data. Must return `true`.

This means:
- Items with `showWhen` bypass fold (e.g., Runs is always visible, Paddock is visible when `hasPaddockCourse`)
- Items without `showWhen` require explicit fold unlock
- Core items (desk, scenarios, help) are standalone top-level items, not subject to group filtering

**Sidebar nav groups:**

| Group | Items |
|-------|-------|
| Practice Room | Runs (showWhen: always), Coursework (showWhen: hasPaddockCourse), Professor, Litigation |
| The Library | Law, My Chips |
| The Commons | Feed, Messages, People, Progress |

**Mobile bottom nav:** Mirrors sidebar progression. Minimum 3 items (Desk, Scenarios, Help). `filterMobileNavByFold` pads below minimum.

---

## Dashboard: Desk Surface States

The fold-driven `getDashboardSections` has been **deleted**. The dashboard now uses a **desk-state-resolver** that computes one of five surface states from course data:

| Surface State | When | What the student sees |
|---------------|------|----------------------|
| **INTRO_DESK** | No courses, no starter run | Starter lane: choose Torts / Contracts / ConLaw |
| **FIRST_POST_RUN_BRIDGE** | Completed starter run, no course | Bridge: "Make SHEP yours" (syllabus upload) |
| **COURSE_FORMING_DESK** | Has course, weeks not extracted yet | Course forming: parsing in progress |
| **COURSE_READY_DESK** | Course mapped, practice + readUp ready | Full coursework: practice center + weekly context |
| **MATURE_DESK** | Course ready + has drafts or evaluations | Full desk with work column (drafts, reviews) |

The resolver input:
```typescript
{
  hasCompletedStarterRun: boolean;
  hasSeenFirstPostRunBridge: boolean; // MVP: localStorage only
  hasAnyMappedCourse: boolean;
  primeCourseHasWeeksExtracted: boolean;
  primeCourseHasPracticeReady: boolean; // requires BOTH runType AND runHref
  primeCourseHasReadUpReady: boolean;   // optimistic proxy: Boolean(currentModuleTitle)
  hasDrafts: boolean;
  hasEvaluations: boolean;
}
```

---

## Phase Progression

### Phase 0: First Landing (signup complete)

**Sidebar:** My Desk, Scenarios, Help, Runs (4 items — Runs always visible via showWhen)

**Dashboard:** INTRO_DESK — starter lane with doctrine choice (Torts, Contracts, ConLaw)

### Phase 2: First draft created

**Sidebar adds:** Resume (auto-pinned, contextually adjacent)

### Phase 3: First submission filed

**Dashboard evolves:** File Cabinet section appears. Streak counter begins.

### Phase 4: First evaluation received AND viewed

**Sidebar adds:** Professor

**Trigger:** `hasViewedEvaluation` — real signal from `evaluations.first_viewed_at IS NOT NULL`. The student must actually open `/submissions/[id]` for a submission with evaluations. The `mark-evals-viewed` endpoint sets `first_viewed_at` on mount.

### Phase 4b: After completing a Professor evaluation

**Sidebar adds:** Progress

### Phase 5: After using chips in a submission

**Sidebar adds:** My Chips (THE LIBRARY section header appears)

### Phase 6: After completing a research drill

**Sidebar adds:** Law

### Phase 7: After 3+ submissions with argument chips

**Sidebar adds:** Litigation (PRACTICE ROOM section header appears)

### Phase 8: Social features (THE COMMONS — staggered)

- **Messages** → first message received
- **People** → first follow event
- **Feed** → after People unlocked

---

## Trigger Implementation

All triggers are pure boolean checks in `fold-triggers.ts`:

```typescript
{ itemId: 'resume',      check: (ctx) => ctx.hasDraft },
{ itemId: 'file-cabinet', check: (ctx) => ctx.submissionCount >= 1 },
{ itemId: 'professor',   check: (ctx) => ctx.hasViewedEvaluation },
{ itemId: 'progress',    check: (ctx) => ctx.hasCompletedAnyEvaluation },
{ itemId: 'chips',       check: (ctx) => ctx.chipSubmissionCount >= 1 },
{ itemId: 'law',         check: (ctx) => ctx.hasResearchGameCompletion },
{ itemId: 'litigation',  check: (ctx) => ctx.argumentChipSubmissionCount >= 3 },
{ itemId: 'messages',    check: (ctx) => ctx.hasMessage },
{ itemId: 'people',      check: (ctx) => ctx.hasFollow },
{ itemId: 'feed',        check: (ctx, unlocked) => ctx.hasPeopleUnlocked || unlocked.has('people') },
```

`TriggerContext` is built by `computeFoldContext` in `fold-context.ts` — 10 concurrent DB queries via Promise.all.

---

## The Reveal Mechanism

1. **Sidebar:** New item fades in. Static dot (--brand-accent at 60% opacity) appears next to the item, disappears on first click.
2. **Contextual prompt:** Non-modal inline card via `FoldPromptCard`. Connects action → capability. Dismissible, persisted server-side.
3. **Section headers appear with their first item.** Groups grow organically.
4. **Bottom nav mirrors sidebar.** Same `filterMobileNavByFold` function.
5. **Accessibility:** `aria-live="polite"` for reveals. Static dot, no pulsing.
6. **Theme:** All reveal elements use existing design tokens. Tested across all theme variants.

---

## Fallback: Time-based Safety Net

| Time since signup | Features available |
|---|---|
| 7 days | All Phase 0-4 items |
| 14 days | All Phase 5-8 items |
| 30 days | Everything |

Nightly cron checks signup age and force-advances fold phase.

---

## Key Files

| File | Purpose |
|------|---------|
| `src/lib/fold/fold-items.ts` | Registry — 13 items with phase, trigger, nav group |
| `src/lib/fold/fold-types.ts` | TypeScript types for fold items and trigger context |
| `src/lib/fold/filter-nav-by-fold.ts` | Pure function filtering nav groups by unlock state |
| `src/lib/server/fold/fold-context.ts` | Builds TriggerContext from 10 concurrent DB queries |
| `src/lib/server/fold/fold-triggers.ts` | Pure trigger evaluation (boolean checks) |
| `src/lib/server/fold/fold-engine.ts` | `advanceFold` — evaluates triggers, updates state |
| `src/lib/server/fold/fold-queries.ts` | DB read/write for `user_fold_state` |
| `src/lib/dashboard/desk-state-resolver.ts` | Resolves desk surface state from course data |
| `src/lib/dashboard/desk-types.ts` | Types for desk course state |
| `src/lib/config/navigation.ts` | Static nav config (items, groups, showWhen predicates) |
| `src/routes/api/fold/pop-reveal/+server.ts` | Pops next reveal from queue |

---

## Research Backing

- Product tours: 8% completion rate (Tandem AI 2026)
- NN/G: Tutorials "disruptive, often skipped, quickly forgotten"
- Duolingo: Action before registration, lesson-first onboarding
- Chess.com: Puzzles as daily habit, instant feedback, rating progression
- Hick's Law / Schwartz Paradox of Choice: More options at uncertainty = lower action rate
- Self-Determination Theory (Deci & Ryan): Autonomy, Competence, Relatedness
- Nir Eyal Hook Model: Trigger → Action → Variable Reward → Investment
- BJ Fogg Behavior Model: B = MAP (Motivation x Ability x Prompt)
- Breath of the Wild / Metroidvania: Ability gating through narrative context, not level counters
