# SHEP Morph Experience — Implementation Definition

**Date:** 2026-03-24
**Phase:** Define (Double Diamond Phase 2)
**Inputs:** Approved spec (02), Research report (03), Codebase exploration
**Status:** Ready for review

---

## Problem Statement

SHEP has 17 sidebar items and a complex feature surface that overwhelms new students. The current UI presents everything at once — a violation of cognitive load theory (max 3-4 items under uncertainty) and Hick's Law (more options = lower action rate). Research confirms 90% of users churn without strong onboarding, and the average SaaS activation rate is only 37.5%.

The Morph Experience replaces this with behavior-triggered progressive disclosure: features appear one at a time as natural responses to student actions, within a spatial metaphor ("places not tools") that creates belonging rather than tool fatigue. The spec is validated by 14/15 cited research frameworks and exceeds state-of-the-art patterns used by Duolingo, Notion, Linear, and others.

---

## Scope

### In Scope

1. **Core morph system** — data model, state machine, navigation filtering, reveal mechanism
2. **Dashboard evolution** — phase-aware section visibility
3. **Reveal UX** — static dot, contextual prompts, section header transitions, accessibility
4. **Toolbar simplification** — basic/full formatting toolbar gated by morph phase
5. **Analytics** — `MORPH_REVEAL`, `MORPH_FALLBACK`, `MORPH_PROMPT_DISMISSED`, `MORPH_FEATURE_FIRST_USE`
6. **Fallback cron** — time-based safety net (7/14/30 days)
7. **Migration strategy** — existing users set to Phase 9, settings reset option
8. **Five research-backed improvements** — endowed progress, micro-acknowledgments, Phase 3 split, mere exposure, additional analytics
9. **Theme compatibility** — all 5 variants (Classic, Academia, Midnight, Sage, OLED Night)

### Out of Scope

- Command palette (deferred to later phase per spec)
- Research drill minigame (separate Linear issue, Phase 6 trigger dependency)
- Chip auto-save on submit (tracked separately, Phase 5 enabler)
- Spaced repetition engine (v2 opportunity from research)
- Knowledge map visualization (v2 opportunity from research)
- Adaptive difficulty thresholds (v2, needs post-launch data)

### Prerequisites (Must Land First)

| Prerequisite | Status | Blocking |
|-------------|--------|----------|
| Nav group restructure PR (extract Scenarios from Practice Room) | Unresolved | Yes — morph filtering assumes restructured groups |
| Drill card PR | Unmerged | Yes — Phase 0 dashboard depends on it |
| Dashboard phase mockups | Not started | Yes — each phase state needs design before build |
| Research drill minigame scoped | Not started | No — only blocks Phase 6 trigger, not core system |

---

## Technical Requirements

### Work Stream 1: Data Model & Server Infrastructure

**Migration 184: `user_morph_state` table**

```sql
CREATE TABLE user_morph_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 '[]',
  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()
);
```

**Server module:** `$lib/server/morph/`
- `morph-state.ts` — CRUD for `user_morph_state`, cached reads
- `morph-triggers.ts` — `computeMorphTriggers(userId)` with targeted SQL queries per phase
- `morph-engine.ts` — state machine: evaluates triggers, manages queue, respects 1-hour window, handles contextual bypass
- `morph-analytics.ts` — emit `MORPH_REVEAL`, `MORPH_FALLBACK`, `MORPH_PROMPT_DISMISSED`, `MORPH_FEATURE_FIRST_USE`

**Event-driven advancement points** (where `computeMorphTriggers` runs):
- After submission filed (form action in scenario workspace)
- After evaluation received/viewed (evaluation page load)
- After Professor evaluation completed (form action)
- After chip tagged in submission (chip creation handler)
- After research drill completed (drill completion handler)
- After DM received (message polling detects first unread)
- After follow created/received (follow handler)
- After search performed (search API handler increments `search_count`)

**Layout loader integration:**
- Register `app:morph` dependency in root `+layout.server.ts`
- Load `user_morph_state` alongside profile (single query, cached with same TTL)
- Return `morphState` to all pages via layout data
- Call `invalidate('app:morph')` from client when behavioral triggers fire

**Error resilience:**
- Client: store pending morph triggers in localStorage
- Server: authoritative persistence on next successful write
- Reconciliation: on layout load, compare client pending vs server state, replay if needed
- Worst case: student sees same phase until next successful server write

### Work Stream 2: Navigation Architecture

**File:** `apps/web-svelte/src/lib/config/navigation.ts`

**New pure function:**
```typescript
function filterNavByMorph(
  navGroups: NavGroup[],
  morphState: MorphState
): NavGroup[]
```

- Filters items from `navGroups` based on `morphState.unlocked_items`
- Removes empty groups (group header only appears when it has visible items)
- Returns filtered groups preserving order
- Scenarios remains standalone top-level item (Phases 0-6)

**Integration points:**
- `NavigationContent.svelte` — call `filterNavByMorph()` with layout data `morphState`
- Collapsed sidebar — same filter (data-layer, not template-layer)
- `MobileBottomNav.svelte` — same filter, minimum 3 items (Desk, Scenarios, Help)

**Pinned items migration:**
- Currently: `sskit_nav_pinned` in localStorage
- Target: `pinned_items` JSONB in `user_morph_state`
- Migration path: on first morph state creation, read localStorage pins, write to server, clear localStorage
- Intersect pinned IDs with morph-visible set — never render a pinned item not yet morphed in
- Default pins per phase (e.g., Resume auto-pinned at Phase 2)

### Work Stream 3: Dashboard Evolution

**File:** `apps/web-svelte/src/routes/dashboard/+page.svelte`

**New pure function:**
```typescript
function getDashboardSections(morphState: MorphState): DashboardVisibility
```

Returns boolean flags per section:
```typescript
interface DashboardVisibility {
  drillCard: boolean;         // Phase 0+
  scenarioCards: boolean;     // Phase 1+
  fileCabinet: boolean;       // Phase 3+
  evalsInProgress: boolean;   // Phase 3+
  streakCounter: boolean;     // Phase 3+ (subtle)
  curriculumWall: boolean;    // Phase 4+
  journeyIndicator: boolean;  // Phase 0+ (new: endowed progress)
}
```

**Phase-specific dashboard states:**

| Phase | Dashboard Content |
|-------|------------------|
| **0** | Greeting + drill card (centered, generous spacing) + journey indicator |
| **1** | Greeting + drill card (secondary) + 3 beginner scenario cards + nudge text |
| **2** | Same as Phase 1 (sidebar adds Resume, dashboard unchanged) |
| **3** | + File Cabinet section + Evaluations in Progress + subtle streak |
| **4+** | + Full curriculum wall (1L / Upper Level) |

Each section wraps in `{#if sections.drillCard}`, `{#if sections.curriculumWall}`, etc. Single component, no separate routes.

**Dependency:** Dashboard phase mockups must be designed before implementation (unresolved item #6).

### Work Stream 4: Reveal Mechanism

**4a. Sidebar reveal animation**
- New item fades in with subtle slide (`opacity: 0 → 1`, `translateY: 4px → 0`, within 500ms)
- Static dot: `--brand-accent` at 60% opacity, 6px circle, disappears on first click
- Dot state tracked in `user_morph_state.unlocked_items` (each item has `seen: boolean`)
- Gate hover effects behind `@media (hover: hover) and (pointer: fine)`

**4b. Contextual prompts**
- Non-modal inline card on relevant page
- Max 26 words (research: users read ~20% of text, 38% close modals in 4 seconds)
- Connects action → capability: "You've received expert feedback. Now try the other side."
- Dismissible (X button or navigation away)
- Dismissed state persisted server-side in `dismissed_prompts` JSONB
- Recovery: "What's new in your SHEP" section on Help page lists recent reveals

**4c. Micro-acknowledgments (research improvement #2)**
- One-sentence inline tooltip on first sidebar appearance (NOT a modal, NOT a toast)
- Disappears after first interaction with the item
- Tone: matter-of-fact ("Your draft is saved. Pick up where you left off."), not celebratory
- Uses existing tooltip component, positioned adjacent to new nav item

**4d. Section header transitions**
- Section headers (PRACTICE ROOM, THE LIBRARY, THE COMMONS) appear with their first item
- Height transition (`0 → natural height`) + opacity fade, within 500ms budget
- Test with `prefers-reduced-motion: reduce` for graceful degradation

**4e. Accessibility**
- `aria-live="polite"` region announces morph reveals to screen readers
- Follow existing `pinnedLiveMessage` pattern in `NavigationContent.svelte`
- Announcement text mirrors contextual prompt: "Professor is now available in Practice Room."

**4f. Theme compatibility**
- All reveal elements use existing tokens (`--brand-accent`, `Card`, `acad-frame-card`)
- No new tokens needed
- Must be tested across all 5 themes

### Work Stream 5: Toolbar Simplification

**Affected component:** Formatting toolbar within scenario editor (NOT `CaseWorkspaceToolbar.svelte` which is the workspace action bar)

**Prop:** `toolbarTier: 'basic' | 'full'`
- Passed from workspace page based on `morphState.phase`
- `basic` (Phase 0-3): Chips + Format only
- `full` (Phase 4+): All formatting buttons (PARAGRAPH, Tools, More)

**Research improvement #3 — Split Phase 3:**
- Original spec: toolbar expands at Phase 3
- Revised: toolbar expands at Phase 4 (after evaluation feedback loop)
- Rationale: reduces Phase 3 from 4 simultaneous changes to 3 (under cognitive ceiling)
- The student's next writing session after Phase 4 gets the expanded toolbar — contextually appropriate

### Work Stream 6: Analytics Instrumentation

**Four event types** (extending existing `trackEvent()` API):

```typescript
// Behavioral trigger fires
trackEvent('MORPH_REVEAL', {
  phase: 4,
  item: 'professor',
  trigger_type: 'behavioral',
  trigger_source: 'evaluation_viewed',
  time_since_signup_hours: 2.5
});

// Time-based fallback fires
trackEvent('MORPH_FALLBACK', {
  phase_advanced_to: 4,
  days_since_signup: 7,
  items_unlocked: ['resume', 'professor', 'progress']
});

// Contextual prompt dismissed without action (research improvement #5)
trackEvent('MORPH_PROMPT_DISMISSED', {
  phase: 4,
  item: 'professor',
  time_visible_ms: 3200,
  dismissed_via: 'close_button' // or 'navigation_away'
});

// Feature first used after reveal (research improvement #5)
trackEvent('MORPH_FEATURE_FIRST_USE', {
  item: 'professor',
  hours_since_reveal: 0.5,
  trigger_type: 'behavioral',
  session_same_as_reveal: true
});
```

**Implementation:**
- `MORPH_REVEAL` + `MORPH_FALLBACK`: emitted from `morph-engine.ts` server-side
- `MORPH_PROMPT_DISMISSED`: emitted from contextual prompt component client-side
- `MORPH_FEATURE_FIRST_USE`: emitted on first navigation to a morph-revealed page (check `unlocked_items[item].first_used_at` is null)

### Work Stream 7: Fallback Cron

**Implementation:** Vercel cron job (or Neon DB function) running nightly

**Logic:**
```
For each user WHERE phase < 9:
  days_since_signup = now() - created_at

  IF days_since_signup >= 30 AND phase < 9:
    advance to Phase 9, unlock all items
  ELIF days_since_signup >= 14 AND phase < 8:
    advance to Phase 8, unlock Phase 5-8 items
  ELIF days_since_signup >= 7 AND phase < 4:
    advance to Phase 4, unlock Phase 0-4 items

  For each newly unlocked item:
    add compressed contextual prompt to reveal_queue
    emit MORPH_FALLBACK event
```

**One code path:** Layout loader reads morph state — it never checks signup age directly. The cron writes to the same `user_morph_state` table.

### Work Stream 8: Existing User Migration

**Migration 185: Seed morph state for existing users**

```sql
-- Associates and test accounts: Phase 9
INSERT INTO user_morph_state (user_id, phase, unlocked_items)
SELECT id, 9, '["all"]'::jsonb
FROM user_profiles
WHERE email IN (SELECT email FROM associate_emails)
   OR email LIKE '%@test.%';

-- Real users with activity: Phase 9
INSERT INTO user_morph_state (user_id, phase, unlocked_items)
SELECT id, 9, '["all"]'::jsonb
FROM user_profiles
WHERE id IN (SELECT DISTINCT user_id FROM submissions)
ON CONFLICT (user_id) DO NOTHING;

-- All remaining users: Phase 9 (small user base)
INSERT INTO user_morph_state (user_id, phase, unlocked_items)
SELECT id, 9, '["all"]'::jsonb
FROM user_profiles
ON CONFLICT (user_id) DO NOTHING;
```

**Settings page option:** "Reset to fresh experience" button that sets `phase = 0`, clears `unlocked_items`, resets `dismissed_prompts`.

**New signups after deploy:** Automatically start at Phase 0 (table default).

### Work Stream 9: Endowed Progress (Research Improvement #1)

**Component:** `JourneyIndicator.svelte` (new, minimal)

**Design:**
- Subtle, atmospheric indicator on dashboard — NOT a progress bar or checklist
- Think "you are here" on a building floor plan, not "Level 1 of 9"
- Shows the student's position in their growth with first step already behind them
- Uses spatial metaphor: dots/nodes representing phases, first one already lit at signup
- Positioned in dashboard, visible at Phase 0+
- Uses existing design tokens, respects regal/minimal variants

**Data:** Derived from `morphState.phase` — no additional server state needed

**Risk mitigation:** If it feels gamified in testing, remove it. The research says endowed progress works (34% vs 19% completion), but SHEP's scholarly context means the execution must be atmospheric, not metric-driven.

### Work Stream 10: Mere Exposure (Research Improvement #4)

**Approach:** Passive references to upcoming features woven into existing content. NOT greyed-out nav items or "coming soon" labels.

**Examples:**
- Evaluation feedback text could mention "Your reasoning chips are building a collection" (before My Chips appears at Phase 5)
- Scenario descriptions could reference "The Legal Corpus" (before Law Library appears at Phase 6)
- Teaching card content could mention "adversarial practice" (before Litigation appears at Phase 7)

**Implementation:** Content changes in scenario descriptions, evaluation templates, and teaching cards. No new components. Pure copy work that references concepts before they become navigation items.

**Timing:** Can be done independently of core morph system. Best done after Phase mapping is complete so references are accurate.

---

## Edge Cases

| Edge Case | Handling |
|-----------|----------|
| Multiple triggers fire simultaneously | Queue overflow: max 1 sidebar item per reveal window, FIFO queue |
| Student goes inactive for weeks | Time-based fallback cron advances phase with compressed prompts |
| Student resets morph but has existing work | Phase 0 with existing File Cabinet content — show empty dashboard, File Cabinet appears at Phase 3 when re-triggered |
| Server write fails during morph advancement | Client localStorage stores pending trigger, replays on next successful load |
| Student accesses a morph-hidden page via direct URL | Page loads normally — morph controls discoverability, not access |
| Contextually-adjacent reveals (Professor while viewing eval) | May bypass 1-hour queue when context makes it natural |
| Student on multiple devices | Server-side state is authoritative; client reconciles on load |
| Pinned item that hasn't morphed in yet | Intersect pinned IDs with morph-visible set — pin is stored but not rendered |
| Prompt dismissed accidentally | Recovery via Help page "What's new in your SHEP" section |
| Phase 6 trigger (research drill) not yet built | Phase 6 unreachable via behavioral trigger until drill ships; time fallback at 14 days covers it |

---

## Constraints

| Constraint | Source | Impact |
|-----------|--------|--------|
| Max 3-5 new concepts per session | Cognitive Load Theory (Sweller) | Each phase introduces ≤ 3 UI changes |
| Max 26 words for contextual prompts | Chameleon Benchmark 2025 | Prompt copy must be extremely concise |
| 500ms animation budget | Spec (change #17) | All transitions (fade, slide, height) within budget |
| `prefers-reduced-motion` respect | Accessibility spec | Static dot (not pulsing), optional transitions |
| All 5 theme variants | Spec (change #26) | Every reveal element tested across themes |
| Existing `trackEvent()` API | Codebase constraint | 64-char event type, 4096-byte props limit |
| Server cache TTL 30s (prod) / 5s (dev) | Layout loader caching | Morph state changes visible within TTL window |
| `aria-live="polite"` | Accessibility spec | Screen reader announces reveals |

---

## Success Criteria

### Must-Have (Launch)
- [ ] Student signs up and sees 3 sidebar items (Desk, Scenarios, Help)
- [ ] Completing drill evolves dashboard (scenario cards appear)
- [ ] Creating first draft adds Resume to sidebar with fade-in + dot
- [ ] First submission shows File Cabinet + Evaluations in Progress
- [ ] First evaluation viewed shows Professor with contextual prompt
- [ ] Mobile bottom nav mirrors sidebar progression
- [ ] All reveals work across 5 themes
- [ ] Screen reader announces reveals via `aria-live`
- [ ] `MORPH_REVEAL` and `MORPH_FALLBACK` events emit correctly
- [ ] Time-based fallback advances stuck users
- [ ] Existing users migrated to Phase 9 with reset option
- [ ] Direct URL access works regardless of morph phase

### Should-Have (Polish)
- [ ] `MORPH_PROMPT_DISMISSED` and `MORPH_FEATURE_FIRST_USE` events
- [ ] Micro-acknowledgment tooltips on first reveal appearance
- [ ] Endowed progress journey indicator on dashboard
- [ ] Help page "What's new in your SHEP" section
- [ ] Toolbar expansion moved to Phase 4 (split from Phase 3)

### Could-Have (v2)
- [ ] Mere exposure content woven into scenarios and evaluations
- [ ] Adaptive difficulty thresholds based on analytics data
- [ ] Spaced repetition for legal reasoning patterns
- [ ] Knowledge map visualization

---

## Metrics to Track Post-Launch

| Metric | Target | Measurement |
|--------|--------|-------------|
| Time to first drill completion | < 5 min | `MORPH_REVEAL` Phase 1 timestamp - signup |
| Phase 0 → Phase 1 conversion | > 70% | Users reaching Phase 1 / total signups |
| Phase 3 → Phase 4 conversion | > 40% | The aha moment completion rate |
| Behavioral trigger coverage | > 90% | `MORPH_REVEAL` / (`MORPH_REVEAL` + `MORPH_FALLBACK`) |
| Same-session feature adoption | > 60% | `MORPH_FEATURE_FIRST_USE` where `session_same_as_reveal = true` |
| Prompt dismissal without action | < 30% | `MORPH_PROMPT_DISMISSED` / total prompts shown |
| D7 retention | > 7% | Top 25% of products (Amplitude 2025 benchmark) |
| Phase 4 reached by day 7 | > 50% | Peak experience should happen in week 1 |

---

## Recommended Implementation Order

### Wave 1: Foundation (no UI changes visible)
1. Migration 184: `user_morph_state` table
2. Migration 185: Existing user migration (all to Phase 9)
3. `$lib/server/morph/` module (state, triggers, engine)
4. Layout loader: load morph state, register `app:morph` dependency
5. `filterNavByMorph()` pure function + unit tests

### Wave 2: Navigation + Dashboard (core UX)
6. Integrate `filterNavByMorph()` into NavigationContent, collapsed sidebar, MobileBottomNav
7. `getDashboardSections()` + dashboard phase visibility
8. Pinned items migration (localStorage → server)
9. Reveal animations (fade-in, static dot, section header transitions)

### Wave 3: Triggers + Prompts (behavior-driven)
10. Wire behavioral triggers at all event points (submission, evaluation, chips, etc.)
11. Contextual prompt component + dismissed state persistence
12. Toolbar simplification (`toolbarTier` prop)
13. `MORPH_REVEAL` analytics instrumentation

### Wave 4: Safety Net + Polish
14. Fallback cron (7/14/30 day safety net)
15. `MORPH_FALLBACK` analytics
16. Settings page "Reset to fresh experience"
17. Help page "What's new" section
18. `MORPH_PROMPT_DISMISSED` + `MORPH_FEATURE_FIRST_USE` analytics

### Wave 5: Research Improvements
19. Endowed progress journey indicator
20. Micro-acknowledgment tooltips
21. Phase 3 → Phase 4 toolbar split
22. Mere exposure content integration

---

## Open Questions for Founder

1. **Journey indicator design:** Atmospheric dots/path or something else? Need design direction before building.
2. **Mere exposure copy:** Who writes the scenario/evaluation content that passively references upcoming features? This is content work, not engineering.
3. **Phase 6 dependency:** The research drill minigame is a prerequisite for Phase 6's behavioral trigger. Is this scoped as a separate project? What's the timeline?
4. **Fallback window validation:** The 7/14/30 day windows are reasonable heuristics but not research-validated. Should we A/B test alternative windows (5/10/21)?
5. **Phase 3 split:** Research recommends moving toolbar expansion to Phase 4. Do you agree with this change to the approved spec?
