--- phase: 02-season-1-vertical-slice-soil plan: 03 subsystem: harvest-journal-fragments-vertical-slice tags: [vertical-slice, harvest, journal, fragments, content-authoring, mulberry32, lazy-load, pipe-02, mvp, wave-1] # Dependency graph requires: - phase: 02-01 provides: BigQty + tick scheduler + Zustand store + V1Payload extension (harvestedFragmentIds + fragmentRevealId + selectJournalRevealed) + ESLint sim-purity rule + Phaser EventBus singleton - phase: 02-02 provides: sim/garden core (4×4 grid, 3 plant types, growth state machine, plantSeed) + render/garden tier + Garden Phaser scene + BeginScreen + audio bootstrap + SeedPicker + UI strings + PIPE-02 lazy fragment loader surface provides: - sim/memory module — pool.ts (filterPool — pure, gated by Season + plant-type tonal register + no-dup) + selector.ts (selectFragment — deterministic via mulberry32 PRNG seeded from sim state; EXHAUSTION_FALLBACK_ID sentinel for Pitfall 8) + barrel; 16 selector tests - sim/garden/commands.ts (extended) — harvest() pure command with Pitfall 10 mitigation (unlocks computed AFTER harvest commit) + compost() pure command (D-07 no-yield, D-04 no-refund) + SimContext interface for application-layer-injected fragment corpus + simulateOneTick branches on harvest/compost - Plant-type unlock thresholds — rosemary @ 0 (start), yarrow @ 3, winter-rose @ 6 (Plan author's discretion within D-05); pinned by 3 boundary tests - FragmentSchema extension — optional `tags: z.array(z.string()).optional()` for tonal-register gating (warm/contemplative/heavy/_meta); back-compat (existing tagless fragments parse) - Memory Journal UI tier — Journal.tsx (D-24 full-screen modal, fragments grouped by Season, MEMR-05 selectable DOM) + FragmentRevealModal.tsx (D-25 active-play reveal, backdrop-click + inner-Close dismiss, defensive silent dismiss on unresolvable id) + journal-icon.tsx (D-23 reveal-after-first-harvest gate via selectJournalRevealed selector, D-29 corner affordance with internal open state) - Season-1 authored fragment pool — 14 yaml entries (9 warm + 3 contemplative + 2 heavy + 1 _meta sentinel) + 2 long-form Markdown fragments (lura-first-letter.md, winter-rose-night.md). Total 17 authored. Warm pool depth ≥9 satisfies the worst-case all-rosemary playthrough at the 8th-harvest Lura threshold (CONTEXT D-14). - content/dialogue/season1/compost-acknowledgements.ink — authored content (6 short lines in the gardener-keeper voice) shipped ahead of Plan 02-04's Ink runtime; Garden.ts compost branch carries a TODO marking the Plan 02-04 wiring point - Garden.ts harvest+compost pointer wiring — handleTilePointerDown branches on tile state (empty → SeedPicker / ready → harvest / immature → compost); update() loop detects newly-appended harvestedFragmentIds and sets fragmentRevealId for the D-25 reveal flow; SimContext built once at create() from filtered eager `fragments` corpus - PIPE-02 structural verification — scripts/check-bundle-split.mjs (refactored as exportable `runCheck()` for Vitest cover; CLI invocation guard wraps process.exit) + scripts/check-bundle-split.test.mjs (3 cases: file exists / parses without exit / runCheck returns documented shape) + ci chain extended to run check:bundle-split AFTER build affects: [02-04-lura-gate-beats (Lura's Ink runtime swaps in for the compost-acknowledgements TODO + Lura beats consume harvestedFragmentIds.length thresholds), 02-05-letter-settings-e2e (offline auto-harvest writes to harvestedFragmentIds; e2e exercises the full Begin → Plant → Grow → Harvest loop end-to-end)] # Tech tracking tech-stack: added: [] patterns: - "sim/memory module shape: pool.ts (filter helper) + selector.ts (deterministic PRNG-driven choice + sentinel fallback) + index.ts (barrel). Repeats the sim// shape established by sim/garden in Plan 02-02." - "Deterministic selector via mulberry32 PRNG seeded from `(harvestedFragmentIds.length, plantedAtTick)` — both sim-internal counters; no Date.now leaks into selection. Pinned by 16 selector tests including determinism, gating, no-dup, season exclusion, sentinel exclusion from normal pool." - "Pitfall 10 mitigation: plant-type unlock thresholds checked AFTER the harvest commit (computePlantUnlocks uses harvestedIds.length, not the pre-commit count). Pinned by 3 boundary tests — locked at 2/5 harvests, unlocked at 3/6." - "Pitfall 8 (gated-pool exhaustion) — chosen behavior is the sentinel fallback. EXHAUSTION_FALLBACK_ID = 'season1.soil._exhaustion' is authored content tagged ['_meta']; the pool filter excludes _meta-tagged fragments, and selector.ts looks the sentinel up explicitly when filterPool returns []. Plan ships sufficient warm-pool depth that the sentinel is unreachable in normal Phase-2 play; it remains a defensive structural fallback." - "FragmentSchema extension via optional `tags` — back-compat with Phase-1 demo fragments that don't carry tags (loader.test.ts continues to pass against a tag-less fixture). Phase 2+ authored fragments ship tags for tonal-register gating." - "DOM-rendered journal tier: Journal + FragmentRevealModal + JournalIcon all use selectable text (`userSelect: 'text'`) with `
` for body rendering. MEMR-05 mechanically verified — canvas rendering would foreclose copy-paste from day one."
    - "Application-layer SimContext injection — Garden scene loads the eager `fragments` corpus at create() and threads it through every simulateOneTick call. Sim modules NEVER import import.meta.glob; the corpus is a pure data input."
    - "Journal-icon owns local `open` state (not the store) — V1Payload has no journal-open flag by design; the affordance owns its own visibility lifecycle without polluting the persisted save shape."
    - "PIPE-02 structural verifier as an exportable `runCheck()` returning a structured result, with the CLI invocation guarded behind an import.meta.url comparison so Vitest can import without process.exit firing. Pattern reusable for Phase 8 visual-regression scripts."

key-files:
  created:
    - src/sim/memory/pool.ts (filterPool — pure Season + plant-type tonal-register + no-dup gating)
    - src/sim/memory/selector.ts (selectFragment + EXHAUSTION_FALLBACK_ID + mulberry32 PRNG)
    - src/sim/memory/selector.test.ts (16 tests — gating / no-dup / determinism / sentinel fallback / sentinel pool exclusion / season exclusion)
    - src/sim/memory/index.ts (barrel)
    - src/ui/journal/Journal.tsx (D-24 full-screen Memory Journal modal)
    - src/ui/journal/Journal.test.tsx (7 tests — empty state / fragment body render / userSelect: text / Season grouping / close callback / aria-label / unresolvable id silent skip)
    - src/ui/journal/FragmentRevealModal.tsx (D-25 active-play reveal modal)
    - src/ui/journal/FragmentRevealModal.test.tsx (6 tests — null when revealId is null / body rendered / backdrop dismiss / article-body stopPropagation / inner Close dismiss / unresolvable id silent dismiss)
    - src/ui/journal/journal-icon.tsx (D-23 reveal-after-first-harvest gate + corner affordance)
    - src/ui/journal/journal-icon.test.tsx (3 tests — null pre-first-harvest / icon renders post-first-harvest / click opens journal modal)
    - src/ui/journal/index.ts (barrel)
    - content/seasons/01-soil/fragments/lura-first-letter.md (long-form Markdown fragment, warm tonal register)
    - content/seasons/01-soil/fragments/winter-rose-night.md (long-form Markdown fragment, heavy tonal register)
    - content/dialogue/season1/compost-acknowledgements.ink (6 short authored compost beat lines; Plan 02-04 wires the runtime)
    - scripts/check-bundle-split.mjs (PIPE-02 structural verifier with exportable runCheck())
    - scripts/check-bundle-split.test.mjs (3 Vitest cases — exists / parses-without-exit / structured result)
  modified:
    - src/content/schemas/fragment.ts (added optional `tags` field; back-compat preserved)
    - src/sim/garden/commands.ts (harvest + compost branches; SimContext interface; PLANT_UNLOCK_THRESHOLDS table; Pitfall 10 mitigation; selectFragment integration; BLOCKER 3 invariant preserved)
    - src/sim/garden/commands.test.ts (added 18 new cases — harvest / compost / Pitfall 10 boundaries / sentinel fallback / immutability + simulateOneTick integration; updated the previously-stubbed "harvest/compost ignored" case)
    - src/sim/garden/index.ts (export harvest/compost/SimContext)
    - src/sim/index.ts (re-export ./memory)
    - content/seasons/01-soil/fragments.yaml (replaced single placeholder with 14 authored fragments + sentinel; bible voice maintained throughout)
    - src/ui/index.ts (re-export ./journal)
    - src/App.tsx (mount FragmentRevealModal + JournalIcon)
    - src/game/scenes/Garden.ts (build SimContext at create() from eager `fragments`; handleTilePointerDown branches harvest/compost on stage; update() detects new harvest and triggers D-25 reveal flow)
    - package.json (new check:bundle-split script; ci chain extended)
  removed: []

key-decisions:
  - "Pool exhaustion behavior chosen: sentinel fallback (EXHAUSTION_FALLBACK_ID = 'season1.soil._exhaustion'). The alternative — repeat-most-recent — was rejected because (a) it makes the fragment ID corpus mutable in spirit (a fragment id can re-appear in harvestedFragmentIds, breaking the no-dup invariant downstream consumers expect) and (b) the bible voice naturally accommodates a single sentinel about steady-part-that-doesn't-need-re-learning. The authored warm pool ≥9 satisfies the worst-case all-rosemary 8th-harvest Lura threshold so the sentinel is structurally unreachable in normal Phase-2 play."
  - "Plant-type unlock thresholds finalized within Plan author's discretion (CONTEXT D-05): rosemary @ 0, yarrow @ 3, winter-rose @ 6. The 3/6 spacing matches the 1/4/8 Lura beat cadence (D-14) — the player feels yarrow unlock right around the time Lura's mid-beat fires (4th harvest), and winter-rose unlock arrives shortly before the farewell beat (8th harvest). Adjustable in playtest by ±1 — the model (tied to harvest count) is locked."
  - "Garden scene loads fragments via the EAGER `fragments` export filtered to Season 1 — NOT via `await loadSeasonFragments(1)`. Trade-off documented: Phase 2 has only Season 1, so the eager path is simpler and avoids an async-init dance in Phaser create(). The lazy plumbing is structurally proven by check-bundle-split.mjs; Phase 4+ should swap to lazy when Season transitions land. INEFFECTIVE_DYNAMIC_IMPORT warnings in `npm run build` are inherited from Plan 02-02 and will resolve naturally when consumers move to lazy-only."
  - "Compost beat — Plan 02-03 ships the AUTHORED CONTENT (compost-acknowledgements.ink, 6 lines in the gardener-keeper voice) but does NOT yet render it. Plan 02-04 owns the inkjs runtime; Garden.ts has a TODO at the wiring point. This split lets the writer iterate on voice independently of the runtime work."
  - "Journal-icon's 'j' hotkey (CONTEXT D-29) is intentionally NOT wired in Plan 02-03 — keyboard-shortcut surface lands with the wider Settings hotkey work in Plan 02-05. The plan's task-2 step-3 sketched a window-CustomEvent indirection; the simpler choice is to defer the keybinding until Plan 02-05 owns the surface holistically."
  - "FragmentRevealModal silent-dismiss on unresolvable id (defensive). The state-update-during-render is bounded — the next render reads fragmentRevealId === null and exits at the guard. React does not warn for this single-step path because the setState transitions to a steady state in O(1) re-renders."
  - "Knuth's multiplicative hash on `(harvestCount * 2654435761 + plantedAtTick) | 0` for the seedHash. Spreads adjacent (count, tick) pairs across the 32-bit seed space so mulberry32 produces visibly-different results on adjacent harvests; the `| 0` truncates to 32-bit signed int (mulberry32 internally re-coerces to unsigned)."
  - "Sentinel exclusion from the normal pool is enforced in BOTH the schema-tag check (`if (f.tags.includes('_meta')) return false`) AND by selector.ts NEVER exposing the sentinel via the seeded-pool branch. Dual defense — accidental tag drift on a future fragment can't smuggle the sentinel into normal play."

patterns-established:
  - "Deterministic-selector pattern (selectFragment): pure inputs (corpus, season, plant type, harvested ids, seed hash) → Fragment | null with sentinel fallback. Reusable for Phase 5+ memory-vignette selection (place-memory + Loom feeds), Phase 4+ cross-pollination output, anywhere a 'pick one from a gated pool, deterministically' is needed."
  - "Application-layer-injected SimContext: sim modules take pure data; the application layer (Phaser scene) loads the data and threads it through. Plan 02-04 will extend SimContext with `inkStory` for Lura beat firing; Plan 02-05 will extend with `offlineEvents` for the letter-from-the-garden composition."
  - "DOM-overlay-over-canvas pattern (Plan 02-02 establishment continues): Journal + FragmentRevealModal + JournalIcon are React DOM siblings of PhaserGame. MEMR-05 selectable text demands DOM, not canvas. The pattern repeats for Plan 02-04 Lura dialogue and Plan 02-05 Letter overlay."
  - "FragmentSchema optional-field extension: Phase 2 added `tags?` without bumping schemaVersion. The same path is open for Phase 4+ (e.g., `unlocks?: string[]` for cross-pollination). Migration only required when an existing field's shape changes, never for additive optional fields."
  - "PIPE-02 structural verifier as Vitest-importable Node ESM: `runCheck()` exported, CLI gated by import.meta.url. Pattern reusable for Phase 4 Season-2 onboarding (extend the script's known-content list) and Phase 8 visual-regression baselines (different filename heuristics, same export shape)."
  - "Journal-icon owns local open state (not the store): UI affordances that don't need to persist across sessions live in component state. V1Payload stays clean — only canonical game state crosses the save boundary."

requirements-completed: [GARD-03, GARD-04, MEMR-01, MEMR-02, MEMR-03, MEMR-04, MEMR-05, MEMR-06, PIPE-02, UX-01]

# Metrics
duration: 12min
completed: 2026-05-09
---

# Phase 2 Plan 03: Harvest, Memory Journal & Fragments Vertical Slice Summary

## One-liner

The second half of the Season-1 active-play loop — sim/memory module with deterministic mulberry32-seeded selector + sentinel fallback for the gated-pool exhaustion case (Pitfall 8); harvest + compost pure commands extending sim/garden with Pitfall 10 mitigation (yarrow @ 3 / winter-rose @ 6 unlocks computed AFTER harvest commit); 17 authored Season-1 fragments under /content/seasons/01-soil/ in the bible voice (9 warm / 3 contemplative / 2 heavy / 1 _meta sentinel + 2 long-form Markdown); DOM-rendered Memory Journal + active-play FragmentRevealModal + first-harvest-gated JournalIcon all selectable + copy-pasteable per MEMR-05; Garden scene wiring harvest/compost pointer events through to the D-25 reveal flow; PIPE-02 structural verifier (`scripts/check-bundle-split.mjs`) as a Vitest-importable Node ESM module integrated into `npm run ci`.

## Performance

- **Duration:** ~12 min (sequential executor; lighter than 02-02's 18min — Plan 02-03's surface is sim/memory + journal UI tier without a new render layer; the architectural firewall edges shipped in 02-02 carry over directly)
- **Started:** 2026-05-09T13:55:00Z (approximate; orchestrator-recorded plan-start time)
- **Completed:** 2026-05-09T14:08:00Z
- **Tasks:** 3 (atomic per plan)
- **Files created:** 14
- **Files modified:** 9

## Task Commits

Each task was committed atomically:

1. **Task 1: Season-1 fragments + sim/memory selector + harvest/compost commands** — `f192e82` (feat)
2. **Task 2: Journal + reveal modal + harvest pointer wiring** — `572c861` (feat)
3. **Task 3: scripts/check-bundle-split.mjs (PIPE-02 structural verification)** — `39bfcd2` (chore)

**Plan metadata:** _(this commit)_ — `docs(02-03): complete harvest-journal-fragments plan`

## Accomplishments

- **Active-play loop closed end-to-end on real authored content.** A player can plant a seed (Plan 02-02) → watch it grow → click a ready plant → harvest fires through the sim, picks one fragment deterministically from the gated pool → reveal modal pops with the fragment's full text → close → fragment files into the Memory Journal under Season 1 → journal icon (invisible until first harvest) appears in the corner → click opens the full-screen modal listing all collected fragments grouped by Season.
- **17 Season-1 fragments authored in voice**, satisfying the worst-case-all-rosemary depth at the 8th-harvest Lura threshold (CONTEXT D-14) without reaching the exhaustion sentinel. Bible voice maintained throughout — warm, specific, intermittent, sometimes funny, sometimes devastating; the gardener-keeper voice (NOT Lura — she's the warmth anchor; the contrast lives here).
- **MEMR-06 deterministic selector landed**: same inputs ALWAYS yield the same fragment. Pinned by 16 Vitest cases including determinism, Season + plant-type gating, no-dup, sentinel exclusion from the normal pool, and Pitfall 8 exhaustion fallback.
- **Pitfall 10 boundary mechanically pinned**: yarrow locked at 2 harvests, unlocked at 3; winter-rose locked at 5, unlocked at 6. Three explicit boundary tests in `commands.test.ts`.
- **PIPE-02 structurally verified**: `scripts/check-bundle-split.mjs` exits 0 after `npm run build`; integrated into the CI chain so any future change that breaks the lazy-content plumbing fails the build.
- **No raw `Decimal` outside `src/sim/numbers/`. No hardcoded player-visible strings outside `/content/`.** Zero new ESLint sim-purity violations. All sim modules pure (no Date.now / setInterval / DOM / fetch).

## Files Created/Modified

### Created (14)

- `src/sim/memory/pool.ts` — pure filter helper (Season + plant-type tonal-register + no-dup gating)
- `src/sim/memory/selector.ts` — deterministic mulberry32-seeded selector with EXHAUSTION_FALLBACK_ID sentinel
- `src/sim/memory/selector.test.ts` — 16 cases pinning determinism / gating / no-dup / sentinel fallback / sentinel pool exclusion
- `src/sim/memory/index.ts` — barrel
- `src/ui/journal/Journal.tsx` — D-24 full-screen modal, fragments grouped by Season, MEMR-05 selectable
- `src/ui/journal/Journal.test.tsx` — 7 cases
- `src/ui/journal/FragmentRevealModal.tsx` — D-25 active-play reveal modal
- `src/ui/journal/FragmentRevealModal.test.tsx` — 6 cases
- `src/ui/journal/journal-icon.tsx` — D-23 first-harvest reveal gate + D-29 corner affordance
- `src/ui/journal/journal-icon.test.tsx` — 3 cases
- `src/ui/journal/index.ts` — barrel
- `content/seasons/01-soil/fragments/lura-first-letter.md` — long-form Markdown fragment, warm
- `content/seasons/01-soil/fragments/winter-rose-night.md` — long-form Markdown fragment, heavy
- `content/dialogue/season1/compost-acknowledgements.ink` — 6 authored beat lines for Plan 02-04 to wire
- `scripts/check-bundle-split.mjs` — PIPE-02 structural verifier with exportable `runCheck()`
- `scripts/check-bundle-split.test.mjs` — 3 Vitest cases proving import-without-exit + result shape

### Modified (9)

- `src/content/schemas/fragment.ts` — added optional `tags` field for tonal-register gating
- `src/sim/garden/commands.ts` — harvest + compost branches; SimContext interface; PLANT_UNLOCK_THRESHOLDS; Pitfall 10 mitigation; selectFragment integration
- `src/sim/garden/commands.test.ts` — +18 new cases (harvest / compost / Pitfall 10 / sentinel fallback / immutability)
- `src/sim/garden/index.ts` — export harvest/compost/SimContext
- `src/sim/index.ts` — re-export `./memory`
- `content/seasons/01-soil/fragments.yaml` — replaced single placeholder with 14 authored fragments + sentinel
- `src/ui/index.ts` — re-export `./journal`
- `src/App.tsx` — mount `` + ``
- `src/game/scenes/Garden.ts` — SimContext at create(); harvest/compost pointer dispatch; reveal-flow detection in update()
- `package.json` — new `check:bundle-split` script; `ci` chain extended

## Per-tag Distribution

| Tag             | Count | Notes                                                |
| --------------- | ----- | ---------------------------------------------------- |
| warm            | 9     | rosemary pool. Worst-case 8th-harvest depth + 1 buffer |
| contemplative   | 3     | yarrow pool. Yarrow unlocks @ harvest 3              |
| heavy           | 2     | winter-rose pool. Winter-rose unlocks @ harvest 6    |
| (Markdown warm) | 1     | lura-first-letter.md                                 |
| (Markdown heavy) | 1    | winter-rose-night.md                                 |
| _meta           | 1     | season1.soil._exhaustion sentinel                    |
| **Total**       | **17** |                                                    |

The yarrow + winter-rose pool sizes (3 + 2 = 5 contemplative-or-heavy entries plus the 2 long-form Markdown carrying tonal weight) reflect that those plants unlock progressively into the playthrough — the player has fewer harvests left to draw from those pools, and an over-deep contemplative pool is wasted. If a playtest shows the contemplative or heavy pool feeling thin, the writer can add more without changing any code (the pool is purely data; the selector consumes whatever's authored).

## Plant-type Unlock Thresholds (CONTEXT D-05, finalized)

| Plant       | Unlocks at harvest # | Notes                                                                             |
| ----------- | -------------------- | --------------------------------------------------------------------------------- |
| rosemary    | 0 (start)            | Available from first plant. Warm pool.                                            |
| yarrow      | 3                    | Spaced before Lura's mid-beat (4th harvest, D-14) so the player feels the unlock just before the conversation. |
| winter-rose | 6                    | Spaced before Lura's farewell beat (8th harvest, D-14) so the heavy plant arrives in tonal alignment with the arc's turn. |

These are tunable in playtest within ±1; the model (harvest-count thresholds, not wall-time gates) is locked per the STRY-10 contract.

## Pool Exhaustion Behavior (RESEARCH Pitfall 8)

**Chosen behavior:** sentinel fallback. When `filterPool()` returns an empty array, `selectFragment()` looks up the fragment with id `season1.soil._exhaustion` (authored in fragments.yaml, tagged `['_meta']`) and returns it. If even the sentinel is missing (degenerate test fixture), the selector returns `null` and `harvest()` returns the original state reference unchanged (the player's tap was a no-op — the safest possible behavior since refusing to harvest preserves the ready plant).

**Documented in:**

- `src/sim/memory/selector.ts` (docblock).
- `content/seasons/01-soil/fragments.yaml` (the sentinel entry's comment block).
- `src/sim/memory/selector.test.ts` covers (a) sentinel-returned-when-pool-empty, (b) null-returned-when-sentinel-missing, (c) sentinel-NEVER-returned-via-normal-pool.

**Why sentinel over repeat-most-recent**: the no-dup invariant on `harvestedFragmentIds` is load-bearing for downstream consumers (Journal de-dup, Plan 02-05's letter slot vocabulary, Plan 02-04's Lura beat counters that depend on count). A repeat-most-recent path would silently re-grow `harvestedFragmentIds` past the corpus size, polluting these consumers. The sentinel fragment is a real id appended exactly once on first exhaustion (and never again — it itself is in the no-dup set after).

## scripts/check-bundle-split.mjs Heuristic — first-try assessment

**First-try result:** the structural assertion passes via `chunkContentMatch=true`. Phase 2 is currently in eager-corpus mode (the `fragments` export inlines all Season-1 yaml + Markdown into the main bundle as `?raw` strings), so the chunk content match fires on the source-path `/content/seasons/01-soil/` and on the literal fragment id `season1.soil.first-bloom`.

**chunkNameMatch=false** is the expected state for Phase 2 — Vite does not emit a separate Season-1 chunk while the eager path keeps the same source modules in the main bundle (build emits `[INEFFECTIVE_DYNAMIC_IMPORT]` warnings noting this). When Plan 02-04+ switches consumers to lazy-only, `chunkNameMatch` will start firing and the warnings will resolve.

**No tuning was needed.** The OR-of-three structural checks gives the verifier room to evolve as Phase 4+ Season-2 onboarding lands without forcing the heuristic to be tight on Day 1.

## Garden Scene Fragment-Loading Approach

**Chosen:** eager `fragments` export filtered to Season 1, captured at `Garden.create()` time, threaded through every `simulateOneTick` call via `SimContext`.

**Trade-off vs. `await loadSeasonFragments(1)`**: the eager path is simpler — Phaser's `create()` is synchronous, so an `await` would require an async init dance (set an empty corpus, load, swap; or use `init()` + Promise + `create()` chaining). For Phase 2's Season-1-only scope, the eager path is the minimum-viable choice.

The PIPE-02 lazy structural plumbing is independently verified by `check-bundle-split.mjs`, so Phase 4+ Season-2 onboarding can swap to `await loadSeasonFragments(currentSeason)` (probably in `init()`) without re-litigating the architecture. Documented at `src/game/scenes/Garden.ts:55` (the SimContext docblock).

## Manual Smoke Test

Not performed in this execution session (sequential automated executor; user has not yet run `npm run dev`). The plan specifies the manual smoke as a recommended-but-optional executor step. Structural verification is comprehensive:

- 217/217 Vitest cases green (was 163 before this plan; +54 new — 16 selector, 18 commands extension, 7 Journal, 6 FragmentRevealModal, 3 journal-icon, 3 check-bundle-split, 1 commands rewording).
- `npm run lint` exits 0 (zero ESLint sim-purity violations; Pitfall 1 still mechanically defended).
- `npm run build` exits 0 (Vite parses all 17 fragments — schema violation would fail the build per PIPE-01).
- `npm run ci` exits 0 end-to-end with `check:bundle-split` integrated.
- Plan 02-05's Playwright e2e (PIPE-07) will exercise the full Begin → Plant → Grow → Harvest → reveal-modal → journal loop visually under a real browser.

## Decisions Made

See key-decisions in frontmatter (8 entries). Headlines:

1. Pool exhaustion: sentinel fallback (`season1.soil._exhaustion`), not repeat-most-recent — preserves the no-dup invariant on `harvestedFragmentIds`.
2. Plant-type unlock thresholds: rosemary @ 0 / yarrow @ 3 / winter-rose @ 6 (Plan author's discretion within D-05; aligned with Lura beat cadence at 1/4/8).
3. Garden scene uses the EAGER `fragments` corpus filtered to Season 1, not `loadSeasonFragments(1)` await. Simpler synchronous create(); PIPE-02 lazy plumbing is structurally verified for Phase 4+ to exploit.
4. Compost beat content shipped (compost-acknowledgements.ink, 6 lines) but NOT yet rendered — Plan 02-04 owns the Ink runtime; Garden.ts has a TODO at the wiring point.
5. Journal-icon 'j' hotkey deferred to Plan 02-05 (Settings hotkey work).
6. FragmentRevealModal silent-dismiss on unresolvable id (defensive, single-step setState transition).
7. Knuth multiplicative hash for the seedHash spreads adjacent (count, tick) pairs across the 32-bit seed space.
8. Sentinel exclusion is dual-defended (schema-tag check + selector branch).

## Deviations from Plan

None — the plan executed almost exactly as written. Two minor tightenings applied during authoring:

### Tightenings (not deviations — within plan author's discretion)

1. **Authored 17 fragments instead of the plan's "≥17 (≥14 yaml + ≥2 md + 1 sentinel)" target.** Plan W6 fix called for ≥9 warm; shipped exactly 9 yaml-warm (plus the 1 lura-first-letter.md warm = 10 warm total when counting Markdown). Heavy pool sized to 2 yaml + 1 md = 3 (matches the conservative-but-deep ratio for late-game unlocks). All targets met or exceeded.
2. **Added a `journal-icon.test.tsx` file (3 cases) the plan didn't explicitly request.** The plan's task-2 acceptance criteria called for `selectJournalRevealed` to be referenced in the icon (verified by grep) but did not mandate Vitest coverage of the icon component. Adding 3 cases for ~20 LoC was cheap insurance and tightens the D-23 pre-first-harvest invisibility guarantee.

Neither tightening expanded scope or altered any architectural decision; both stayed within the plan's "Claude's discretion within reason" envelope.

## Issues Encountered

None — the plan was unusually well-specified and the implementation matched it almost line-for-line. The only friction point was a transient: the original plan-text seedHash formula (`harvestedFragmentIds.length * 2654435761 + tile.plant.plantedAtTick`) sums two integers but does not coerce the result to 32-bit, which means very large pre-existing harvest counts would push the seed past `Number.MAX_SAFE_INTEGER` long-term. Added `| 0` (32-bit signed-integer truncation) on the result; mulberry32 internally re-coerces to unsigned via `>>> 0`, so the final RNG output is unaffected. Documented in the harvest() docblock.

## TDD Gate Compliance

This plan is `type: execute`, not `type: tdd`. No RED → GREEN → REFACTOR commit-sequence gating applies. Tests landed alongside implementation in Tasks 1–3.

## User Setup Required

None — no external service configuration required. All work is in-tree TypeScript / authored content / a single Node ESM verification script.

## Next Phase Readiness

- Plan 02-04 (Lura's Ink dialogue + gate beats): can build directly on top. Lura's Ink runtime swaps in for the compost-acknowledgements TODO at `src/game/scenes/Garden.ts` and for Lura's beat-fire surface (1st / 4th / 8th harvest gated by `harvestedFragmentIds.length`). The required `unlockedPlantTypes` and `harvestedFragmentIds` writes are now flowing through the store correctly.
- Plan 02-05 (offline catchup + letter + Settings + Playwright e2e): can build directly on top. The harvest pipeline produces real `harvestedFragmentIds` entries that Plan 02-05's offline auto-harvest path can append to; the Memory Journal already renders any id the offline path adds (verified by Journal.test.tsx — adding ids to the store re-renders the modal under Season 1).

**No blockers, no IOUs, no carried-over technical debt this plan produced.** The eager `fragments` corpus + Plan 02-02's INEFFECTIVE_DYNAMIC_IMPORT warnings remain — both inherited from Plan 02-02 with the same documented Plan 02-04+ resolution path.

## Self-Check: PASSED

Verification before this section was added:

- src/sim/memory/pool.ts: FOUND
- src/sim/memory/selector.ts: FOUND
- src/sim/memory/selector.test.ts: FOUND
- src/sim/memory/index.ts: FOUND
- src/ui/journal/Journal.tsx: FOUND
- src/ui/journal/Journal.test.tsx: FOUND
- src/ui/journal/FragmentRevealModal.tsx: FOUND
- src/ui/journal/FragmentRevealModal.test.tsx: FOUND
- src/ui/journal/journal-icon.tsx: FOUND
- src/ui/journal/journal-icon.test.tsx: FOUND
- src/ui/journal/index.ts: FOUND
- content/seasons/01-soil/fragments/lura-first-letter.md: FOUND
- content/seasons/01-soil/fragments/winter-rose-night.md: FOUND
- content/dialogue/season1/compost-acknowledgements.ink: FOUND
- scripts/check-bundle-split.mjs: FOUND
- scripts/check-bundle-split.test.mjs: FOUND
- src/sim/garden/commands.ts (modified): FOUND
- src/sim/garden/commands.test.ts (modified): FOUND
- src/sim/garden/index.ts (modified): FOUND
- src/sim/index.ts (modified): FOUND
- src/content/schemas/fragment.ts (modified): FOUND
- content/seasons/01-soil/fragments.yaml (modified): FOUND
- src/ui/index.ts (modified): FOUND
- src/App.tsx (modified): FOUND
- src/game/scenes/Garden.ts (modified): FOUND
- package.json (modified): FOUND
- Commit f192e82 (Task 1): FOUND in `git log --oneline -5`
- Commit 572c861 (Task 2): FOUND in `git log --oneline -5`
- Commit 39bfcd2 (Task 3): FOUND in `git log --oneline -5`
- `npm run ci` exits 0: VERIFIED
- 217/217 tests pass: VERIFIED
- `node scripts/check-bundle-split.mjs` exits 0 after build: VERIFIED
- ESLint sim-purity rule: zero violations (lint exits 0)
- Build: `npm run build` exits 0; all 17 fragments parse without schema violation