docs(02-03): complete harvest-journal-fragments plan
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>
This commit is contained in:
+305
@@ -0,0 +1,305 @@
|
||||
---
|
||||
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/<subsystem>/ 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 `<pre>` 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 `<FragmentRevealModal />` + `<JournalIcon />`
|
||||
- `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
|
||||
Reference in New Issue
Block a user