- src/sim/offline/: OfflineEventBlockSchema (Zod) + EMPTY_OFFLINE_EVENTS
+ aggregateOfflineEvent pure aggregator (D-19); 14 tests green
- src/sim/garden/auto-harvest.ts: autoHarvestReadyPlants silent-mode
branch (D-10); reuses harvest() pipeline so selector + Pitfall 10
unlocks + STRY-10 Lura gate all run identically; BLOCKER 3 invariant
preserved (no lastTickAt writes); 7 tests green
- simulateOneTick: ctx.silent triggers auto-harvest sweep before tick
increment; active-play path unchanged (silent defaults false)
- content/dialogue/season1/letter-from-the-garden.ink: authored skeleton
with VAR plants_bloomed / fragment_titles / lura_was_here per D-17/D-18;
bible voice, anti-FOMO compliant, 24h cap silent in voice (D-11)
- ink-loader: loadInkStory union extended with letter-from-the-garden;
separate letterStoryGlob for lazy code-split chunk; INK_VARIABLE_MAP
gains plants_bloomed / fragment_titles / lura_was_here slots reading
from session.pendingLetterEventBlock
- src/ui/letter/letter-renderer.ts: pure buildLetterSlots helper —
prefers fragment first-sentence body for tonal weight, slugified-id
fallback; 10 tests green
- npm run compile:ink emits 5 .ink.json files (was 4); Vite emits the
letter as a separate lazy chunk (letter-from-the-garden.ink-*.js)
- 295/295 tests green (was 264; +31 new); npm run ci exits 0
- scripts/compile-ink.mjs: build-time inklecate runner using bundled binary
(BLOCKER 4 — uses node_modules/inklecate/bin, not stale -windows/-mac path strings).
Assumption A6 verified first-try on Windows; the same binary path resolution
works on macOS + Linux per the wrapper's own getInklecatePath convention.
- scripts/compile-ink.test.mjs: 3 Vitest cases proving the compiler runs +
emits valid JSON with inkVersion. wipe=false for the test path so it can
run in parallel with the ink-loader test without racing on the wipe step.
- 4 Season-1 .ink files authored in voice (Lura warmth-anchor, gardener-keeper
for compost): lura-arrival.ink, lura-mid.ink, lura-farewell.ink,
compost-acknowledgements.ink (rewrite of Plan 02-03 scaffolded version into
VAR-driven branch shape consumable by the runtime).
- src/content/ink-loader.ts: loadInkStory + bindGardenStateToInk +
INK_VARIABLE_MAP. Centralized snake_case slot mapping per Pitfall 4. UTF-8
BOM stripped before Story instantiation.
- src/content/ink-loader.test.ts: 8 cases — Story instantiation for all 4
beats, fragment_count binding, Pitfall 4 snake_case enforcement, silent
skip for stories missing declared vars.
- package.json: build now runs compile:ink first; ci chain runs compile:ink
before test so ink-loader.test.ts's precondition check passes.
- .gitignore: src/content/compiled-ink/ excluded (regenerated on every build).
npm run ci exits 0; 11 new tests green (228 total).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Task 2 of Plan 02-03: ship the Memory Journal UI surfaces (D-23/D-24/D-25)
and wire harvest + compost pointer events through the Garden scene to the
sim → store → React reveal flow.
src/ui/journal/ (new module):
- Journal.tsx — full-screen modal (D-24); fragments grouped by Season;
DOM-rendered text with userSelect: text per MEMR-05; reads
harvestedFragmentIds from the store; resolves ids against the eager
`fragments` corpus (defensive — unresolvable ids skip silently).
- FragmentRevealModal.tsx — D-25 active-play reveal modal; backdrop click
+ inner Close button dismiss; event.stopPropagation on the article
body so clicking inside the text doesn't dismiss; defensive silent
dismiss on unresolvable id.
- journal-icon.tsx — D-23 + D-29 corner affordance; gated by
selectJournalRevealed (`harvestedFragmentIds.length > 0`); local open
state (no store pollution); 'j' hotkey deferred to Plan 02-05.
- index.ts — barrel.
- 16 new Vitest cases across 3 test files (Journal: 7 / FragmentRevealModal:
6 / journal-icon: 3); all green.
src/App.tsx — mount FragmentRevealModal + JournalIcon as DOM siblings of
PhaserGame.
src/ui/index.ts — re-export ./journal.
src/game/scenes/Garden.ts — harvest/compost pointer flow:
- create() builds a SimContext from the eager `fragments` corpus filtered
to Season 1; passed to every simulateOneTick call (Phase 4+ should swap
to await loadSeasonFragments(currentSeason) when Season transitions land).
- handleTilePointerDown branches on tile state: empty → seed picker
event; ready plant → enqueue 'harvest' command; immature plant → enqueue
'compost' command (TODO Plan 02-04: render the Ink-authored compost
acknowledgement beat from compost-acknowledgements.ink).
- update() detects newly-appended harvestedFragmentIds and sets
fragmentRevealId so the reveal modal pops with the new fragment text.
- BLOCKER 3 invariant preserved — sim writes tickCount, never lastTickAt.
content/dialogue/season1/compost-acknowledgements.ink — authored content
for the GARD-04 + D-07 compost beat. 6 short lines in the gardener-keeper
voice (NOT Lura — she's the warmth anchor; the garden's voice is the
contrast). Plan 02-04 wires the inkjs runtime; Plan 02-03 ships the
content so the writer can iterate independently.
214/214 tests green (was 163; +51 new this plan); npm run lint exits 0;
npm run ci exits 0; npm run build exits 0 with the expected
INEFFECTIVE_DYNAMIC_IMPORT warnings (eager `fragments` export still
imports the Season-1 yaml/md statically alongside the lazy
loadSeasonFragments path — documented in 02-02-SUMMARY.md).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>