# The Fold + Runs — Master PR Plan

**Date:** 2026-03-26
**Branch:** `fold/adopt-rename` (PR 1), `fold/integration` (PR 2)
**Base:** `main`
**Adopts:** `morph/integration` (#1798), `feat/first-hour-drills`, `mini-games` (#1806), `morph/polish` (#1805)
**Linear:** SHEP-578 (Daily Run), SHEP-582 (Daily Run design)
**Bot reviews:** Run on PR 2 only — skip stale reviews on adopted branches

## Review Summary

Reviewed: 2026-03-26 | Reviewers: VP Product, VP Engineering, VP Design

### Changes Applied

| # | Change |
|---|--------|
| 1 | Split into 2 PRs: PR 1 (adopt + rename, mechanical) and PR 2 (routes + triggers + Daily Run) |
| 2 | Unified streak system: extend existing `user_streaks` table with `run_completed` contribution type instead of rebuilding from `app_events` queries |
| 3 | Run gating uses `gatedBy` prerequisite Fold item (declarative) instead of raw phase numbers |
| 4 | Squash all branch migrations into single `184_fold_and_practice.sql` |
| 5 | Document expected final state of `+layout.server.ts` as merge contract |
| 6 | Dropped Journey Indicator (endowed progress) — undermines progressive disclosure philosophy; student shouldn't see the scaffolding |
| 7 | Dropped Sidebar Whispers — third notification channel alongside existing MorphPromptCard + dot is scope creep |
| 8 | Toolbar expansion stays at Phase 3 per approved spec — no shift to Phase 4 |
| 9 | Deferred Mere Exposure content to separate Linear issue — product/content task, not engineering |
| 10 | Daily Run only appears at Phase 3+ when student has 2+ unlocked Run types |
| 11 | Daily Run is phase-aware: selects from student's unlocked Runs, not global |
| 12 | Replaced streak freeze/repair with "warm restart" — no loss-aversion mechanics |
| 13 | Daily Run selection: deterministic rotation (date-seeded, cycled by available Run types) |
| 14 | Mobile nav: Runs appears at Phase 3+ as additional item; does NOT replace Scenarios |
| 15 | Grass icon: emoji (whimsical pasture decoration), `opacity: 0.7` on dark themes |
| 16 | Rename preserves DB analytics event types as `MORPH_REVEAL`/`MORPH_FALLBACK` with code-level aliases for analytics continuity |
| 17 | Fix `db.unsafe()` string interpolation in fallback cron — use tagged template query |
| 18 | Fix fire-and-forget state mutation in pop-reveal endpoint — await the write |
| 19 | Make Practice Room group header a clickable link to `/practice/` |
| 20 | Locked Run cards: `--text-muted` + remove sheen/lift + lock icon overlay (not `opacity: 0.5`) |
| 21 | Remove legacy `/practice/[areaId]` catch-all routes before adding new static routes |
| 22 | `FOLD_FEATURE_FIRST_USE` stores `first_used_at` in separate `item_first_use JSONB` column (not changing `unlocked_items` schema) |
| 23 | Add CI gate: test count must match or exceed 138 baseline after rename |
| 24 | Add Playwright test for `/drill` → `/practice/drill` redirect |
| 25 | Phase transitions happen on page load only, never reactively mid-session |

### Deferred to Linear Issues

- [ ] Journey Indicator / endowed progress (revisit post-ship with analytics data)
- [ ] Mere exposure content references (product/content task)
- [ ] Personal Legal Knowledge Graph (LKG) visualization
- [ ] Schema consolidation: unify `research_game_completions` + `practice_attempts` + `drill_completions` into single `practice_completions` table
- [ ] Domain-based Run filtering and recommendations
- [ ] Adaptive difficulty based on Run performance data

---

## What This PR Is

Two PRs that together ship the complete progressive disclosure system ("the Fold") and the organized practice game library ("Runs").

**PR 1 (adopt + rename):** Merge 3 branches, squash migrations, rename morph → fold. Mechanical, high file count, low logic change. Reviewable by verifying build + test count.

**PR 2 (routes + triggers + Daily Run):** Consolidate routes under `/practice/`, wire Fold triggers for each Run type, build the Daily Run and streak system, add analytics events. Logic changes that need real review.

The student opens SHEP and sees 3 items. They do a Run. The product grows around them. They come back tomorrow and do another Run. The streak builds. The Fold unfolds. Features discover the student.

---

## PR 1: Adopt + Rename

### Step 1 — Create clean branch from main
```
git checkout main && git pull
git checkout -b fold/adopt-rename
```

### Step 2 — Merge adopted branches (in order)

| Order | Branch | What It Brings | Conflict Strategy |
|-------|--------|---------------|-------------------|
| 1 | `morph/integration` (#1798) | Full Fold engine, nav filtering, dashboard phases, reveal UX, analytics, fallback cron, chip auto-save, research game | Both sides for source; ours for planning docs |
| 2 | `feat/first-hour-drills` | DrillView, TeachingCard, SuccessModal, ExampleMemo, 200+ prompts, drill tracking, dashboard drill card | Resolve against morph integration's dashboard |
| 3 | `mini-games` (#1806) | Issue Spotting, Argument Attack, game selector, practice_prompts/practice_attempts tables, 25+ seed prompts per game | Clean merge — no overlap with morph code |

**Merge contract — `+layout.server.ts`:** After each merge, verify that the layout loader:
1. Calls `ensureFoldState(userId)` (was `ensureMorphState`)
2. Returns `foldState` in the load function's return object
3. Registers `app:fold` dependency for invalidation
4. Fold state loading is NOT inside the profile cache (separate concern)

After all 3 merges, verify: `npm run lint && npm run check && npm run test`

### Step 3 — Squash migrations

All branch migrations (morph's 184-186, drills' 184, mini-games' 184) → single `184_fold_and_practice.sql`:

```sql
-- 184_fold_and_practice.sql
-- Creates all tables for the Fold system + practice games

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 '[]',
  item_first_use   JSONB NOT NULL DEFAULT '{}',
  search_count     INT NOT NULL DEFAULT 0,
  safety_net_at    TIMESTAMPTZ,
  created_at       TIMESTAMPTZ NOT NULL DEFAULT now(),
  updated_at       TIMESTAMPTZ NOT NULL DEFAULT now()
);

CREATE TABLE drill_completions ( ... );    -- from first-hour-drills
CREATE TABLE practice_prompts ( ... );     -- from mini-games
CREATE TABLE practice_attempts ( ... );    -- from mini-games
CREATE TABLE research_games ( ... );       -- from morph/polish
CREATE TABLE research_game_completions ( ... ); -- from morph/polish
CREATE TABLE daily_run (
  run_date     DATE PRIMARY KEY,
  run_type     TEXT NOT NULL,
  domain       TEXT NOT NULL,
  prompt_id    UUID NOT NULL,
  created_at   TIMESTAMPTZ NOT NULL DEFAULT now()
);

-- Existing user migration: all current users → Phase 9
INSERT INTO user_fold_state (user_id, phase, unlocked_items)
SELECT id, 9, '["all"]'::jsonb FROM user_profiles
ON CONFLICT (user_id) DO NOTHING;
```

### Step 4 — Rename morph → fold

**Method:** IDE rename symbol (not sed/regex) to avoid corrupting `isomorphic`, `polymorphic`, CSS properties.

| Category | Find | Replace |
|----------|------|---------|
| **Database** | `user_morph_state` | `user_fold_state` (rewritten in migration, not ALTER) |
| **Server module** | `$lib/server/morph/` | `$lib/server/fold/` |
| **Client module** | `$lib/morph/` | `$lib/fold/` |
| **Functions** | `advanceMorph()` → `advanceFold()`, `filterNavByMorph()` → `filterNavByFold()`, etc. |
| **Types** | `MorphState` → `FoldState`, `MorphItem` → `FoldItem` |
| **Layout data** | `morphState` → `foldState` |
| **Invalidation** | `app:morph` → `app:fold` |
| **API routes** | `/api/morph/*` → `/api/fold/*` |
| **Vercel cron** | `/api/cron/morph-fallback` → `/api/cron/fold-fallback` (update `vercel.json`) |
| **Test files** | `morph-*.test.ts` → `fold-*.test.ts` |

**Analytics event types preserved:** DB `event_type` values stay as `MORPH_REVEAL` and `MORPH_FALLBACK` for analytics continuity. Code maps them:
```typescript
export const FOLD_REVEAL = 'MORPH_REVEAL' as const;
export const FOLD_FALLBACK = 'MORPH_FALLBACK' as const;
```

### Step 5 — Fix inherited anti-patterns

**5a. `db.unsafe()` in fallback cron:** Replace string interpolation with tagged template:
```sql
WHERE (age >= interval '30 days' AND phase < 9)
   OR (age >= interval '14 days' AND phase < 8)
   OR (age >= interval '7 days' AND phase < 4)
```

**5b. Fire-and-forget in pop-reveal endpoint:** Await `saveFoldState` before returning JSON response. The 10-20ms latency is invisible; the serverless freeze risk is not.

**5c. Remove `.catch()` fallback on `research_game_completions` query:** All tables exist in the unified migration now. Let real errors surface.

### Step 6 — Verify

```bash
npm run lint && npm run check && npm run test
# Verify test count baseline:
npx vitest --reporter=json 2>/dev/null | jq '.numTotalTests'
# Must be >= 138
```

**CI gate:** Add a step that fails if total fold test count drops below 138.

**Post-deploy verification:** After first deploy, confirm the fold-fallback cron fires by checking `app_events` for a `MORPH_FALLBACK` event within 48 hours.

---

## PR 2: Routes + Triggers + Daily Run

Base: `fold/adopt-rename` (PR 1)

### Part 1: Route Consolidation — The `/practice/` Tree

**Pre-step:** Remove legacy `/practice/[areaId]/[length]/[role]` catch-all routes to eliminate routing ambiguity with new static routes. If still needed, move to `/scenarios/practice/`.

#### Current State (scattered)
```
/drill                              ← standalone
/practice/scavenger-hunt/           ← research game
/practice/scavenger-hunt/rules/
/practice/scavenger-hunt/cases/
/practice/games/issue-spotting      ← mini-games
/practice/games/argument-attack     ← mini-games
```

#### Target State (consolidated)
```
/practice/                          ← Run selector (game library home)
/practice/drill                     ← Analysis warm-up
/practice/issue-spotting            ← Issue Spotting
/practice/argument-attack           ← Argument Attack
/practice/research/                 ← Research Challenge
/practice/research/rules            ← Find the Rule
/practice/research/cases            ← Find the Case
```

#### Implementation
1. Move `routes/drill/` → `routes/practice/drill/`
2. Move `routes/practice/games/issue-spotting/` → `routes/practice/issue-spotting/`
3. Move `routes/practice/games/argument-attack/` → `routes/practice/argument-attack/`
4. Rename `routes/practice/scavenger-hunt/` → `routes/practice/research/`
5. Add 301 redirect: `/drill` → `/practice/drill` (SvelteKit `+server.ts`)
6. Remove `/practice/games/` parent route
7. Create `/practice/+page.svelte` — the Run selector
8. Add Playwright test: navigate `/drill`, assert arrival at `/practice/drill`

#### Navigation Integration

Make the **Practice Room** group header a clickable link to `/practice/` while keeping collapse behavior on the chevron icon. This transforms Practice Room from a mere category into a place — the student visits the Practice Room and sees what's available.

#### The Run Selector (`/practice/+page.svelte`)

Shows available Runs, **gated by prerequisite Fold items** (not phase numbers):

| Run | `gatedBy` | Unlocked When | Card Description |
|-----|-----------|--------------|-----------------|
| **Drill** | `null` (always available) | Signup | "Apply a legal rule to facts. 3 minutes." |
| **Issue Spotting** | `'file-cabinet'` | First submission filed | "Find the legal issues in a fact pattern. 3 minutes." |
| **Argument Attack** | `'chips'` | First chip usage | "Identify the flaw in a legal argument. 3 minutes." |
| **Research Challenge** | `'law'` | Research game triggers it | "Find the rule or case. Race the clock. 5 minutes." |

**Locked card treatment:** `pointer-events: none`, text at `--text-muted`, remove `.hover-lift-refined` and `.card-sheen`, centered `Lock` icon at 40% opacity. Card surface stays at `--bg-card` (preserves spatial hierarchy). One-line hint below: "Complete your first submission to unlock."

**Card component:** Extend `DashboardCurriculumSection` domain accent card pattern with lock/dimming states. Do NOT create a new card component.

**Whimsy:** Grass patch emoji 🌿 as header decoration on the Runs page. `opacity: 0.7` on dark themes via `:global(.dark)`.

The selector also shows (Phase 3+):
- **Daily Run** card (featured, top position) — see Part 3
- **Streak counter** — current streak, best streak
- **Recent Runs** — last 3 completed with scores

---

### Part 2: Fold Trigger Wiring for Runs

Each Run type connects to the Fold progression:

| Run Type | Fold Event Hook | Trigger |
|----------|----------------|---------|
| **Drill** | `onDrillComplete` | Phase 0 → Phase 1: dashboard evolves. **Already wired** — audit existing `api/drills/complete/+server.ts`, confirm it works after rename, mark as verified. |
| **Issue Spotting** | `onRunComplete('issue-spotting')` | No direct phase trigger — feeds effort data via `recordContribution()` |
| **Argument Attack** | `onRunComplete('argument-attack')` | No direct phase trigger — feeds effort data via `recordContribution()` |
| **Research Challenge** | `onResearchGameComplete` | Phase 6: Law Library appears. **Already wired** in morph/polish — verify after rename. |

#### How Runs feed the provenance economy

Every completed Run calls `recordContribution('run_completed', ...)` on the existing `user_streaks` table AND emits an analytics event:

```typescript
// Provenance economy — unified streak system
await recordContribution(userId, 'run_completed');

// Analytics — rich event data
trackEvent('RUN_COMPLETED', {
  run_type: 'drill' | 'issue-spotting' | 'argument-attack' | 'research',
  domain: 'torts' | 'contracts' | 'criminal' | ...,
  score: number,          // 0-100 normalized
  time_seconds: number,   // actual time spent
  streak_day: number,     // current streak length
  daily_run: boolean      // was this the Daily Run?
});
```

---

### Part 3: The Daily Run

#### Design Philosophy

The Daily Run is SHEP's equivalent of Chess.com's Daily Puzzle. It is the **single behavior the platform reinforces relentlessly**. Everything else — scenarios, evaluations, social features — emerges from being in the app. But the Run is what gets them there.

Research:
- Users who reach 7-day streak: **3.6x** more likely to stay long-term
- 3-7 minutes is the cognitive sweet spot for retention
- "One puzzle daily, done consistently, beats marathon sessions every time"
- Lowering the streak barrier **increased** long-term engagement

#### When It Appears

**The Daily Run only appears at Phase 3+** — when the student has at least 2 unlocked Run types. Before that, the only Run is Drill, and "Daily Run: Drill (again)" adds no value. The Phase 0-2 experience is the drill card on the dashboard, exactly as the approved spec defines.

#### How It Works

**Phase-aware selection:** The Daily Run selects from the student's unlocked Run types, not a global pool. A Phase 3 student sees Drill or Issue Spotting as their Daily Run. A Phase 6 student might see a Research Challenge.

**Selection algorithm — deterministic rotation:**
```typescript
function selectDailyRun(date: Date, unlockedRunTypes: RunType[]): DailyRunSelection {
  const dayOfYear = getDayOfYear(date);
  const runType = unlockedRunTypes[dayOfYear % unlockedRunTypes.length];
  const domainIndex = Math.floor(dayOfYear / unlockedRunTypes.length);
  const domain = LEGAL_DOMAINS[domainIndex % LEGAL_DOMAINS.length];
  return { runType, domain, promptId: selectPrompt(runType, domain, date) };
}
```

- Rotates through the student's unlocked Run types on a daily cycle
- Domain rotates on a longer cycle (each domain appears ~every 11 days)
- Prompt selected deterministically from seed data (never repeats within 90-day window)
- Testable, predictable, no cron needed for selection

**The `daily_run` table** stores the global daily prompt (shared context for "Did you do today's Run?") but the *which Run type* decision is per-student based on their Fold state.

#### The Streak — Unified with Existing System

**The streak counts days where the student completed at least one Run.** Powered by the existing `user_streaks` table with a new `run_completed` contribution type via `recordContribution()`.

This means:
- No new streak table
- No `app_events` sequential scan
- Existing advisory locks, badge milestones, and streak-at-risk notifications all work
- One streak, one mental model, one source of truth

| Streak Decision | Choice | Rationale |
|----------------|-------|-----------|
| What counts | Any completed Run (not just Daily) | Flexibility — any Run maintains the habit |
| Minimum effort | One Run, any type, any score | "Unrealistic to say it isn't possible" |
| **No streak freeze** | Warm restart instead | SHEP is "loving" — a break doesn't punish. Show "(3-day streak, returning)" not "streak lost" |
| Visibility | Run selector from Phase 3+, dashboard streak at Phase 3+ | Not visible before the student has context to care |
| Social | Streak on profile (future — deferred) | Deferred to social features Linear issue |

**Warm restart:** After a break, the streak counter shows the current streak with context: "🌿 Day 3 (best: 23 days)". The student sees their history honored, not erased. No anxiety mechanic. No loss aversion. Just warmth.

#### Daily Run on the Dashboard

| Fold Phase | Dashboard Treatment |
|-----------|-------------------|
| **Phase 0** | Drill card (centered, generous spacing) — per approved spec |
| **Phase 1-2** | Drill card secondary, scenario cards primary — per approved spec |
| **Phase 3+** | "Today's Run" teaser card in a dedicated strip — shows Run type, domain, "3 min", links to `/practice/` with daily run pre-selected |
| **Phase 5+** | "Today's Run" may feature Argument Attack or Research Challenge |

**The dashboard card is a teaser that deep-links to `/practice/`.** It shows domain + estimated time + "Go" action. The `/practice/` page shows the full card with scoring, streak, and history. Never duplicate run state across two components.

**Phase transitions happen on page load** (server-side Fold phase), not reactively mid-session.

---

### Part 4: Analytics Improvements

#### `FOLD_PROMPT_DISMISSED`

Wire existing FoldPromptCard (renamed from MorphPromptCard) dismiss handler:

```typescript
trackEvent('FOLD_PROMPT_DISMISSED', {
  phase: number,
  item: string,
  time_visible_ms: number,  // mount time → dismiss time
  dismissed_via: 'close_button' | 'navigation_away'
});
```

`navigation_away` tracked via `onDestroy` — if component unmounts without explicit dismiss.

#### `FOLD_FEATURE_FIRST_USE`

Track first navigation to a Fold-revealed page:

```typescript
trackEvent('FOLD_FEATURE_FIRST_USE', {
  feature: FoldFeatureId,  // typed: 'professor' | 'law-library' | 'litigation' | etc.
  hours_since_reveal: number,
  trigger_type: 'behavioral' | 'fallback',
  session_same_as_reveal: boolean
});
```

**Storage:** New `item_first_use JSONB DEFAULT '{}'` column on `user_fold_state` (additive, non-breaking). Keeps `unlocked_items` as simple `string[]`.

**Props size safety:** Add `console.warn` in events API when `propsJson.length > 3500` (87% of 4096 limit) to catch oversize events before silent truncation.

---

### Part 5: Fold State Caching

**Current:** Layout loader caches profile with 30s TTL but loads fold state uncached on every page load.

**Change:** Add separate fold state cache with 10s TTL. Explicit invalidation on fold mutations:
- `clearFoldCache(userId)` called in `advanceFold()`, `pop-reveal`, `pin`, `dismiss`
- 10s TTL is a balance: short enough for reveal responsiveness, long enough to reduce DB load

---

## Implementation Waves

### PR 1 — Adopt + Rename

#### Wave 0: Merge (no changes, just adopt)
1. Create `fold/adopt-rename` from `main`
2. Merge `morph/integration` → verify `+layout.server.ts` has fold state loading
3. Merge `feat/first-hour-drills` → verify `+layout.server.ts` survives
4. Merge `mini-games` → verify `+layout.server.ts` survives
5. Fix all merge conflicts, verify build passes

#### Wave 1: Rename (mechanical, no logic change)
6. Squash migrations into `184_fold_and_practice.sql`
7. Rename directories: `$lib/server/morph/` → `$lib/server/fold/`, `$lib/morph/` → `$lib/fold/`
8. IDE rename all symbols (functions, types, layout data, invalidation keys)
9. Rename API routes and update `vercel.json` cron path
10. Rename test files, update all imports
11. Add analytics event type aliases (`FOLD_REVEAL = 'MORPH_REVEAL' as const`)
12. Fix `db.unsafe()` → tagged template in fallback cron
13. Fix fire-and-forget → await in pop-reveal endpoint
14. Remove `.catch()` fallback on `research_game_completions` query
15. Verify: `npm run lint && npm run check && npm run test` + test count >= 138

### PR 2 — Routes + Triggers + Daily Run

#### Wave 2: Route Consolidation
16. Remove legacy `/practice/[areaId]` catch-all routes
17. Move `/drill` → `/practice/drill` + 301 redirect + Playwright test
18. Move `/practice/games/*` → `/practice/issue-spotting`, `/practice/argument-attack`
19. Rename `/practice/scavenger-hunt` → `/practice/research`
20. Create `/practice/+page.svelte` (Run selector with Fold gating)
21. Make Practice Room group header clickable link to `/practice/`
22. Implement locked card treatment (muted + lock icon, no opacity flatten)

#### Wave 3: Fold Trigger Wiring
23. Audit existing drill→Phase 1 wiring (verify, don't re-implement)
24. Wire `onRunComplete` events for Issue Spotting + Argument Attack
25. Add `run_completed` contribution type to `recordContribution()`
26. Add `RUN_COMPLETED` analytics event in all Run completion handlers
27. Verify all existing Fold triggers still work post-rename

#### Wave 4: Daily Run + Streak
28. Daily Run selection logic (deterministic rotation, per-student phase-aware)
29. Daily Run teaser card on dashboard (Phase 3+, links to `/practice/`)
30. Daily Run featured card on Run selector (Phase 3+)
31. Streak counter component (Run selector + dashboard at Phase 3+)
32. Warm restart display: "Day N (best: M days)"
33. Grass emoji 🌿 on Runs page header, `opacity: 0.7` on dark themes

#### Wave 5: Analytics + Polish
34. `FOLD_PROMPT_DISMISSED` — wire dismiss handler
35. `FOLD_FEATURE_FIRST_USE` — add `item_first_use` column, wire page load detection
36. Props size warning in events API (> 3500 bytes)
37. Fold state cache (10s TTL with explicit invalidation)

#### Wave 6: Quality Gates
38. New tests: Run selector Fold gating, Daily Run selection, streak logic
39. E2E: fresh user → drill → submission → eval → Fold progression
40. Theme testing: all reveal elements + Run selector + locked cards across 5 variants
41. Accessibility: ARIA live region, reduced motion, screen reader walkthrough
42. Run bot reviews on PR 2

---

## Files Changed (Estimated)

### PR 1 (Adopt + Rename)

| Category | Files | Nature |
|----------|-------|--------|
| Merge adoption | ~120 | From morph/integration alone |
| Migration squash | 5-8 | Replace multiple migrations with one |
| Rename (morph → fold) | ~40 | Find-replace + move directories |
| Anti-pattern fixes | 3 | db.unsafe, fire-and-forget, .catch removal |
| Tests (rename) | 10-15 | Update imports + verify count |
| **Total** | **~60-70 new/modified** | Mechanical — reviewable by verifying build + tests |

### PR 2 (Routes + Triggers + Daily Run)

| Category | Files | Nature |
|----------|-------|--------|
| Route moves | ~20 | Move directories, update imports |
| Run selector | 3-5 | New page + loader + component |
| Daily Run | 5-8 | Selection logic, dashboard card, streak component |
| Analytics | 3-4 | Event wiring, `item_first_use` column |
| Fold state cache | 2-3 | Cache wrapper + invalidation calls |
| Nav integration | 2-3 | Practice Room header link, mobile nav |
| Tests (new) | 8-12 | Run gating, daily selection, streak, redirect |
| **Total** | **~45-55** | Logic changes — needs real review |

---

## Success Criteria

### PR 1: Adopt + Rename
- [ ] All code says "fold" — zero instances of "morph" in app code (excluding analytics event type aliases)
- [ ] Single unified migration `184_fold_and_practice.sql` creates all tables
- [ ] All 138+ tests pass with fold naming
- [ ] `db.unsafe()` replaced with tagged template in fallback cron
- [ ] Pop-reveal endpoint awaits state write
- [ ] Build passes: `npm run lint && npm run check && npm run test`

### PR 2: Routes + Triggers + Daily Run
- [ ] All Runs live under `/practice/` with working redirect from `/drill`
- [ ] Run selector at `/practice/` shows available Runs gated by prerequisite Fold item
- [ ] Locked cards use `--text-muted` + lock icon (not opacity flatten)
- [ ] Practice Room header links to `/practice/`
- [ ] Drill completion triggers Phase 1 (verified, not re-implemented)
- [ ] Research Challenge completion triggers Phase 6 (verified)
- [ ] `RUN_COMPLETED` event fires for every Run type
- [ ] `recordContribution('run_completed')` called on every Run completion
- [ ] Daily Run card on dashboard (Phase 3+ only, teaser linking to `/practice/`)
- [ ] Streak counter tracks consecutive days via existing `user_streaks` table
- [ ] Warm restart display (no freeze, no repair, no loss aversion)
- [ ] Grass emoji 🌿 on Runs page, `opacity: 0.7` on dark themes
- [ ] `FOLD_PROMPT_DISMISSED` + `FOLD_FEATURE_FIRST_USE` events wired
- [ ] All 5 themes tested
- [ ] Bot reviews run and triaged

### Deferred (Linear Issues to Create)
- [ ] Journey Indicator / endowed progress (revisit with post-launch data)
- [ ] Mere exposure content references (product/content task)
- [ ] Personal LKG visualization ("your own pasture graph")
- [ ] Schema consolidation: unified `practice_completions` table
- [ ] Domain-based Run filtering and recommendations
- [ ] Domain streaks
- [ ] Adaptive difficulty
- [ ] Spaced repetition for Run prompts
- [ ] Push notifications for streak (needs mobile infra)
- [ ] Sidebar Whispers (revisit if existing reveal UX proves insufficient)

---

## Resolved Questions (formerly Open)

| # | Question | Resolution | Rationale |
|---|---------|-----------|-----------|
| 1 | Streak freeze mechanic | **Warm restart** — no freeze, no repair | SHEP is "loving" — a break doesn't punish. Show history, don't erase it. |
| 2 | Daily Run selection | **Deterministic rotation** seeded by date, cycled by unlocked types | Predictable, testable, shared experience. No cron needed. |
| 3 | Streak visibility at Phase 0 | **Phase 3+ only** — both on Run selector and dashboard | Don't show streak before the student has multiple Run types and context to care. |
| 4 | Mobile nav for Runs | **Additional item at Phase 3+**, labeled "Practice", does NOT replace Scenarios | Scenarios is the core content browse; Runs is practice. Different concerns. |
| 5 | Grass icon | **Emoji 🌿** — ship it, iterate if needed | P8: simple until proven otherwise. `opacity: 0.7` on dark themes. |

---

## Appendix: The Spirit

From the research: *"The easiest way not to learn is not to come back the next day."*

From the provenance economy: *"This thing I'm building matters, and my name is on it forever."*

From the Fold spec: *"The student doesn't discover features — the features discover the student."*

A law student between classes pulls out their phone. They see "Today's Run: Issue Spotting — Criminal Law. 3 minutes." They tap, read a fact pattern, tag the issues, see their score. Done. Their streak holds. 🌿 Day 14 (best: 14 days). They close the app and walk into Torts.

That's what we're building. The Fold makes the product grow around them. The Runs give them a reason to come back every day. The provenance economy gives their work permanence. Together, they create a platform that a law student doesn't just use — they *inhabit*.
