Plan 02-03 (Wave 1 second plan) executed in sequential mode. 3 atomic commits + this metadata commit: -f192e82— Season-1 fragments + sim/memory selector + harvest/compost -572c861— journal + reveal modal + harvest pointer wiring -39bfcd2— scripts/check-bundle-split.mjs (PIPE-02 structural verifier) Outcomes: - 217/217 tests green (was 163; +54 new this plan) - npm run ci exits 0 with check:bundle-split integrated AFTER build - GARD-03 / GARD-04 / MEMR-01..06 / PIPE-02 satisfied end-to-end - The full Season-1 active-play loop works on real authored content: plant → grow → ready → click → harvest (deterministic, gated, no-dup, sentinel fallback for Pitfall 8) → reveal modal pops with full text → close → fragment files into journal under Season 1 → journal icon appears (D-23 first-harvest gate, invisible before) → click opens full-screen Memory Journal grouped by Season (D-24, MEMR-05 selectable DOM) - 17 Season-1 fragments authored in bible voice (9 warm + 3 contemplative + 2 heavy + 1 _meta sentinel + 2 long-form Markdown) - Plant-type unlock thresholds finalized (Plan author's discretion within D-05): rosemary @ 0 / yarrow @ 3 / winter-rose @ 6. Pitfall 10 boundaries pinned (locked at 2/5, unlocked at 3/6). - Pool-exhaustion sentinel chosen over repeat-most-recent — preserves no-dup invariant; warm pool depth ≥9 makes the sentinel structurally unreachable in normal Phase-2 play - compost-acknowledgements.ink content shipped ahead of Plan 02-04's Ink runtime; Garden.ts has TODO at the wiring point - PIPE-02 structurally verified by scripts/check-bundle-split.mjs (Vitest- importable Node ESM with runCheck() export) Phase 2 progress: 3/5 plans complete (Wave 0 + both Wave 1 plans). Wave 2 (02-04 lura-gate-beats + 02-05 letter-settings-e2e) is the only remaining Phase-2 work. SUMMARY at .planning/phases/02-season-1-vertical-slice-soil/02-03-harvest-journal-fragments-SUMMARY.md. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
32 KiB
phase, plan, subsystem, tags, requires, provides, affects, tech-stack, key-files, key-decisions, patterns-established, requirements-completed, duration, completed
| phase | plan | subsystem | tags | requires | provides | affects | tech-stack | key-files | key-decisions | patterns-established | requirements-completed | duration | completed | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 02-season-1-vertical-slice-soil | 03 | harvest-journal-fragments-vertical-slice |
|
|
|
|
|
|
|
|
|
12min | 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:
- Task 1: Season-1 fragments + sim/memory selector + harvest/compost commands —
f192e82(feat) - Task 2: Journal + reveal modal + harvest pointer wiring —
572c861(feat) - 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.mjsexits 0 afternpm run build; integrated into the CI chain so any future change that breaks the lazy-content plumbing fails the build. - No raw
Decimaloutsidesrc/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 sentinelsrc/sim/memory/selector.test.ts— 16 cases pinning determinism / gating / no-dup / sentinel fallback / sentinel pool exclusionsrc/sim/memory/index.ts— barrelsrc/ui/journal/Journal.tsx— D-24 full-screen modal, fragments grouped by Season, MEMR-05 selectablesrc/ui/journal/Journal.test.tsx— 7 casessrc/ui/journal/FragmentRevealModal.tsx— D-25 active-play reveal modalsrc/ui/journal/FragmentRevealModal.test.tsx— 6 casessrc/ui/journal/journal-icon.tsx— D-23 first-harvest reveal gate + D-29 corner affordancesrc/ui/journal/journal-icon.test.tsx— 3 casessrc/ui/journal/index.ts— barrelcontent/seasons/01-soil/fragments/lura-first-letter.md— long-form Markdown fragment, warmcontent/seasons/01-soil/fragments/winter-rose-night.md— long-form Markdown fragment, heavycontent/dialogue/season1/compost-acknowledgements.ink— 6 authored beat lines for Plan 02-04 to wirescripts/check-bundle-split.mjs— PIPE-02 structural verifier with exportablerunCheck()scripts/check-bundle-split.test.mjs— 3 Vitest cases proving import-without-exit + result shape
Modified (9)
src/content/schemas/fragment.ts— added optionaltagsfield for tonal-register gatingsrc/sim/garden/commands.ts— harvest + compost branches; SimContext interface; PLANT_UNLOCK_THRESHOLDS; Pitfall 10 mitigation; selectFragment integrationsrc/sim/garden/commands.test.ts— +18 new cases (harvest / compost / Pitfall 10 / sentinel fallback / immutability)src/sim/garden/index.ts— export harvest/compost/SimContextsrc/sim/index.ts— re-export./memorycontent/seasons/01-soil/fragments.yaml— replaced single placeholder with 14 authored fragments + sentinelsrc/ui/index.ts— re-export./journalsrc/App.tsx— mount<FragmentRevealModal />+<JournalIcon />src/game/scenes/Garden.ts— SimContext at create(); harvest/compost pointer dispatch; reveal-flow detection in update()package.json— newcheck:bundle-splitscript;cichain 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.tscovers (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 lintexits 0 (zero ESLint sim-purity violations; Pitfall 1 still mechanically defended).npm run buildexits 0 (Vite parses all 17 fragments — schema violation would fail the build per PIPE-01).npm run ciexits 0 end-to-end withcheck:bundle-splitintegrated.- 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:
- Pool exhaustion: sentinel fallback (
season1.soil._exhaustion), not repeat-most-recent — preserves the no-dup invariant onharvestedFragmentIds. - 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).
- Garden scene uses the EAGER
fragmentscorpus filtered to Season 1, notloadSeasonFragments(1)await. Simpler synchronous create(); PIPE-02 lazy plumbing is structurally verified for Phase 4+ to exploit. - 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.
- Journal-icon 'j' hotkey deferred to Plan 02-05 (Settings hotkey work).
- FragmentRevealModal silent-dismiss on unresolvable id (defensive, single-step setState transition).
- Knuth multiplicative hash for the seedHash spreads adjacent (count, tick) pairs across the 32-bit seed space.
- 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)
- 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.
- Added a
journal-icon.test.tsxfile (3 cases) the plan didn't explicitly request. The plan's task-2 acceptance criteria called forselectJournalRevealedto 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.tsand for Lura's beat-fire surface (1st / 4th / 8th harvest gated byharvestedFragmentIds.length). The requiredunlockedPlantTypesandharvestedFragmentIdswrites 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
harvestedFragmentIdsentries 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 ingit log --oneline -5 - Commit
572c861(Task 2): FOUND ingit log --oneline -5 - Commit
39bfcd2(Task 3): FOUND ingit log --oneline -5 npm run ciexits 0: VERIFIED- 217/217 tests pass: VERIFIED
node scripts/check-bundle-split.mjsexits 0 after build: VERIFIED- ESLint sim-purity rule: zero violations (lint exits 0)
- Build:
npm run buildexits 0; all 17 fragments parse without schema violation