diff --git a/.planning/REQUIREMENTS.md b/.planning/REQUIREMENTS.md index a78a7e5..38b733b 100644 --- a/.planning/REQUIREMENTS.md +++ b/.planning/REQUIREMENTS.md @@ -12,7 +12,7 @@ Requirements for initial release. Each maps to roadmap phases. All are user-cent - [x] **CORE-01**: Player loads the game in a modern browser (Chrome, Firefox, Safari, Edge — last 2 stable releases) and reaches a playable state in under 5 seconds on a 25 Mbps connection. - [x] **CORE-02**: Game runs a deterministic, fixed-timestep simulation that advances by elapsed real time (not `setInterval` ticks), so a player who switches tabs or sleeps their device returns to a correctly-advanced garden. -- [x] **CORE-03**: Player who closes the game and returns finds the garden has progressed by the elapsed time (capped at 24 hours) — *no progression resumes from a stale snapshot*. +- [x] **CORE-03**: Player who closes the game and returns finds the garden has progressed by the elapsed time (capped at 24 hours) — *no progression resumes from a stale snapshot*. - [x] **CORE-04**: Player's progress saves to IndexedDB (with localStorage fallback), surviving browser refresh, browser updates, and at least 30 days of inactivity on Chrome and Firefox. - [x] **CORE-05**: Game requests persistent storage via `navigator.storage.persist()` on first save and surfaces the result respectfully if the browser declines. - [x] **CORE-06**: Saves are versioned (`{schemaVersion, payload, checksum}`) and the game refuses to load a save with a checksum mismatch, presenting the player with a recovery option. @@ -89,7 +89,8 @@ Requirements for initial release. Each maps to roadmap phases. All are user-cent - [x] **UX-01**: First-time player sees a single, painted "Begin" screen with no UI clutter; the garden reveals itself as the player interacts (A Dark Room rule). -- [ ] **UX-02**: Player who returns after time away receives a "while you were away" *letter from the garden* — written in voice, not a stat dump — describing what grew, what bloomed, what the wind brought. +- [x] **UX-02**: Player who returns after time away receives a "while you were away" *letter from the garden* — written in voice, not a stat dump — describing what grew, what bloomed, what the wind brought. + - [ ] **UX-03**: Player can buy plants/upgrades in multi-buy increments (×1 / ×10 / ×100 / Max) when the option is meaningful for the current scaling. - [ ] **UX-04**: Player can adjust separate Music, Ambient, and SFX volume sliders, with a master mute keybind; settings persist in saves. - [ ] **UX-05**: Player can toggle a reduced-motion option (respects `prefers-reduced-motion` system setting by default) that disables non-essential particles and animation. @@ -97,7 +98,7 @@ Requirements for initial release. Each maps to roadmap phases. All are user-cent - [ ] **UX-07**: All UI text is selectable, copy-pasteable, and supports browser zoom up to 200% without breaking layout. - [ ] **UX-08**: Color is never the sole carrier of information — icons, labels, or patterns provide a redundant channel for color-blind players. - [ ] **UX-09**: Tab title and favicon update to reflect a backgrounded state (e.g., a small bloom appears when a fragment is ready). -- [x] **UX-10**: Game saves state on `visibilitychange` to hidden, on `beforeunload`, and on Season transitions; behavior is identical between "tab backgrounded" and "tab closed." +- [x] **UX-10**: Game saves state on `visibilitychange` to hidden, on `beforeunload`, and on Season transitions; behavior is identical between "tab backgrounded" and "tab closed." - [x] **UX-11**: Numbers display in human-readable formats (1.2K, 4.5M, 8.9B, scientific notation past notation thresholds). - [ ] **UX-12**: Game surfaces *what Lura said yesterday* in returning-player UI affordances — never *fragments per hour* or *optimization metrics* (mechanic-as-metaphor doctrine). @@ -112,7 +113,7 @@ Requirements for initial release. Each maps to roadmap phases. All are user-cent - [ ] **PIPE-04**: Project ships visual regression testing for the asset library that flags style drift before any model migration is merged. - [x] **PIPE-05**: Project ships an `anti-FOMO doctrine` document and a `Season 7 end-state` design document in `.planning/` (or `docs/`) before economy code is written. - [x] **PIPE-06**: Project ships unit tests (Vitest) covering all save migrations and core economy formulas, run on every CI build. -- [ ] **PIPE-07**: Project ships an end-to-end smoke test (Playwright) that loads the game, plants a seed, harvests a fragment, and verifies persistence across a page reload. +- [x] **PIPE-07**: Project ships an end-to-end smoke test (Playwright) that loads the game, plants a seed, harvests a fragment, and verifies persistence across a page reload. ## v2 Requirements @@ -197,7 +198,7 @@ Populated by gsd-roadmapper during roadmap creation on 2026-05-08. Updated after |-------------|-------|--------| | CORE-01 | Phase 1 — Foundations & Doctrine | Complete (scaffold builds; full E2E <5s measurement is Phase 2 PIPE-07) | | CORE-02 | Phase 2 — Season 1 Vertical Slice (Soil) | Complete (Plan 02-01; drainTicks fixed-timestep accumulator + Clock injection; scene-driven tick wiring is Plan 02-02) | -| CORE-03 | Phase 2 — Season 1 Vertical Slice (Soil) | Complete (Plan 02-01; MAX_OFFLINE_MS=24h clamp + computeOfflineCatchup; letter overlay is Plan 02-05) | +| CORE-03 | Phase 2 — Season 1 Vertical Slice (Soil) | Complete (Plan 02-01 + 02-05; MAX_OFFLINE_MS=24h clamp + computeOfflineCatchup + PhaserGame.tsx boot path threads catchup → silent drainTicks → letter overlay) | | CORE-04 | Phase 1 — Foundations & Doctrine | Complete (IDB + localStorage fallback; codec + round-trip; Settings UI is Phase 2) | | CORE-05 | Phase 1 — Foundations & Doctrine | Complete (navigator.storage.persist() all 4 scenarios; Settings UI surface is Phase 2) | | CORE-06 | Phase 1 — Foundations & Doctrine | Complete (wrap/unwrap + CRC-32 checksum + SaveCorruptError) | @@ -253,7 +254,7 @@ Populated by gsd-roadmapper during roadmap creation on 2026-05-08. Updated after | AEST-08 | Phase 1 — Foundations & Doctrine | Complete (Zod ProvenanceSchema 6 fields + CI gate; north-star reference set deferred to Phase 5 per IOU) | | AEST-09 | Phase 1 — Foundations & Doctrine | Complete (human curation gate mechanism in place; recorded human decision in 01-05-IOU.md) | | UX-01 | Phase 2 — Season 1 Vertical Slice (Soil) | Complete (Plan 02-02; single fixed-position Begin overlay; no HUD/journal/settings; D-22 dismissal) | -| UX-02 | Phase 2 — Season 1 Vertical Slice (Soil) | Pending | +| UX-02 | Phase 2 — Season 1 Vertical Slice (Soil) | Complete (Plan 02-05; letter-from-the-garden.ink + Letter overlay + boot path silent catchup → openLetter at ≥5min absence; Pitfall 9 audio bootstrap on dismiss) | | UX-03 | Phase 8 — UX, Accessibility & Launch Polish | Pending | | UX-04 | Phase 8 — UX, Accessibility & Launch Polish | Pending | | UX-05 | Phase 3 — Watercolor & Cello Aesthetic | Pending | @@ -261,7 +262,7 @@ Populated by gsd-roadmapper during roadmap creation on 2026-05-08. Updated after | UX-07 | Phase 8 — UX, Accessibility & Launch Polish | Pending | | UX-08 | Phase 8 — UX, Accessibility & Launch Polish | Pending | | UX-09 | Phase 8 — UX, Accessibility & Launch Polish | Pending | -| UX-10 | Phase 2 — Season 1 Vertical Slice (Soil) | Complete (Plan 02-01; registerSaveLifecycleHooks + saveOnSeasonTransition; boot-path saveSync wiring is Plan 02-05) | +| UX-10 | Phase 2 — Season 1 Vertical Slice (Soil) | Complete (Plan 02-01 + 02-05; registerSaveLifecycleHooks + saveOnSeasonTransition; PhaserGame.tsx boot path wires saveSync via clock.now() with synchronous LocalStorage write + best-effort IDB) | | UX-11 | Phase 2 — Season 1 Vertical Slice (Soil) | Complete (Plan 02-01; formatHumanReadable K/M/B/T/scientific; BigQty.format() delegates) | | UX-12 | Phase 8 — UX, Accessibility & Launch Polish | Pending | | UX-13 | Phase 1 — Foundations & Doctrine | Complete (anti-fomo-doctrine.md authored + doc-lint tested; review-enforced per CONTEXT D-07) | @@ -271,7 +272,7 @@ Populated by gsd-roadmapper during roadmap creation on 2026-05-08. Updated after | PIPE-04 | Phase 8 — UX, Accessibility & Launch Polish | Pending | | PIPE-05 | Phase 1 — Foundations & Doctrine | Complete (both doctrine docs authored + 8 doc-lint assertions green) | | PIPE-06 | Phase 1 — Foundations & Doctrine | Complete (ci.yml runs npm run ci on push + PR; 53 tests / 12 files green) | -| PIPE-07 | Phase 2 — Season 1 Vertical Slice (Soil) | Pending | +| PIPE-07 | Phase 2 — Season 1 Vertical Slice (Soil) | Complete (Plan 02-05; tests/e2e/season1-loop.spec.ts — full Phase-2 loop in Chromium with FakeClock injection, 1.5s test runtime, 4s end-to-end) | **Per-Phase Counts:** @@ -294,4 +295,4 @@ Populated by gsd-roadmapper during roadmap creation on 2026-05-08. Updated after --- *Requirements defined: 2026-05-08* -*Last updated: 2026-05-09 after Phase 1 verification (16/16 REQ-IDs marked Complete)* +*Last updated: 2026-05-09 after Plan 02-05 execution (40/77 REQ-IDs marked Complete — Phase 1 + Phase 2 fully shipped pending /gsd-verify-work)* diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md index ef366c8..26da8e3 100644 --- a/.planning/ROADMAP.md +++ b/.planning/ROADMAP.md @@ -61,7 +61,7 @@ Plans: - [x] 02-02-begin-plant-grow-PLAN.md — sim/garden core (4×4 grid, 3 plant types, growth state machine, plantSeed) + render layer (Phaser primitives, ready-pulse, tile-coords) + BeginScreen + audio bootstrap + SeedPicker + UI strings (Wave 1; AEST-07, UX-01, GARD-01, GARD-02) ✓ 2026-05-09 (18 min) — see 02-02-begin-plant-grow-SUMMARY.md - [x] 02-03-harvest-journal-fragments-PLAN.md — Season-1 17 authored fragments + sim/memory selector (deterministic mulberry32, gated, no-dup, sentinel fallback for Pitfall 8) + harvest + compost commands (Pitfall 10 post-commit unlock thresholds) + Memory Journal + FragmentRevealModal + JournalIcon + PIPE-02 structural verifier (Wave 1; GARD-03, GARD-04, MEMR-01..06, PIPE-02) ✓ 2026-05-09 (12 min) — see 02-03-harvest-journal-fragments-SUMMARY.md - [x] 02-04-lura-gate-beats-PLAN.md — inklecate compile pipeline + 4 authored .ink files (3 Lura beats + compost acknowledgements) + sim/narrative tick-count gate (1st/4th/8th harvest) + LuraDialogue overlay + InkRenderer drip + Phaser gate visual indicator (Wave 2; STRY-01, STRY-06, STRY-07 vacuous, STRY-10) ✓ 2026-05-09 (24 min) — see 02-04-lura-gate-beats-SUMMARY.md -- [ ] 02-05-letter-settings-e2e-PLAN.md — sim/offline + auto-harvest + letter Ink + Letter overlay + Settings (Export/Import/Restore) + persistence-toast + boot-path save lifecycle wiring + URL-flag FakeClock injection + Playwright PIPE-07 e2e (Wave 2; UX-02, UX-10, CORE-03, CORE-11, PIPE-07) +- [x] 02-05-letter-settings-e2e-PLAN.md — sim/offline + auto-harvest + letter Ink + Letter overlay + Settings (Export/Import/Restore) + persistence-toast + boot-path save lifecycle wiring + URL-flag FakeClock injection + Playwright PIPE-07 e2e (Wave 2; UX-02, UX-10, CORE-03, CORE-11, PIPE-07) ✓ 2026-05-09 (20 min) — see 02-05-letter-settings-e2e-SUMMARY.md **UI hint**: yes ### Phase 3: Watercolor & Cello Aesthetic diff --git a/.planning/STATE.md b/.planning/STATE.md index 5b775d2..c808e60 100644 --- a/.planning/STATE.md +++ b/.planning/STATE.md @@ -3,15 +3,15 @@ gsd_state_version: 1.0 milestone: v1.0 milestone_name: milestone status: in_progress -stopped_at: "Phase 2 Wave 2 first plan (Plan 02-04 lura-gate-beats) executed in sequential mode. 3 atomic commits: c90f8f1 (ink compilation pipeline + 4 authored Season-1 .ink files + runtime loader), 7b79d11 (sim/narrative — Lura beat gating 1/4/8 harvest, STRY-10), 661f990 (Lura dialogue overlay + Ink runtime + gate visual + Garden scene wiring). 264/264 tests green (was 217; +47 new); npm run ci exits 0 end-to-end with compile:ink integrated into the chain BEFORE test. STRY-01 / STRY-06 / STRY-07 / STRY-10 requirements satisfied. Lura goes on the record as the warmth anchor — 3 authored Ink beats (lura-arrival/mid/farewell) gated at 1st/4th/8th harvest counts (D-14); STRY-10 mechanically defended by FakeClock-24h-no-harvest test. RESEARCH Assumption A6 verified first-try on Windows via the bundled inklecate binary at node_modules/inklecate/bin/. Vite emits 4 lazy code-split chunks for compiled Ink JSON. Sim purity firewall holds: src/sim/narrative/ contains zero inkjs imports. Compost-toast UI deferred to Plan 02-05's persistence-toast surface (compost-acknowledgements.ink rewritten in VAR-driven branch shape, ready for the runtime). Wave 2 final plan (02-05 letter-settings-e2e) is the only remaining Phase-2 work. Next: /gsd-execute-phase 2 to continue with Plan 02-05." -last_updated: "2026-05-09T14:32:00.000Z" +stopped_at: "Phase 2 Wave 2 final plan (Plan 02-05 letter-settings-e2e) executed in sequential mode. 4 atomic commits: 26eb77a (sim/offline + auto-harvest + letter Ink + letter-renderer), 5d58d6c (Letter overlay + Settings UI + boot save lifecycle + clock injection), dd48696 (Playwright e2e for PIPE-07), 31f8ede (compost beat toast — Plan 02-04 deferral). 312/312 vitest tests green (was 264; +48 new); Playwright PIPE-07 spec exits 0 in 1.5s test runtime / 4s end-to-end; npm run ci exits 0. UX-02 / UX-10 / CORE-03 / CORE-11 / PIPE-07 / GARD-02 / GARD-04 requirements satisfied. Phase 2 vertical slice closed end-to-end on real authored content + real save round-trip + real offline catchup — could plausibly ship as a free standalone Season-1 prologue. URL-flag FakeClock injection landed cleanly first-try, production-guarded by import.meta.env.PROD. Compost-beat UI wired as a thin transient CompostToast (minimum-viable; Ink runtime path remains compiled + loadable for Phase 4+ swap-in). gray-matter dep auto-removed (Rule 3 blocking issue surfaced via the e2e — Buffer not defined in browser; replaced with 15-line parseFrontmatter helper; bundle dropped 2.2MB → 1.9MB). All 24 Phase-2 REQ-IDs structurally satisfied across the 5-plan set. Phase 2 ready for /gsd-verify-work." +last_updated: "2026-05-09T15:08:00.000Z" last_activity: 2026-05-09 progress: total_phases: 8 completed_phases: 1 total_plans: 8 - completed_plans: 11 - percent: 19 + completed_plans: 12 + percent: 22 --- # Project State @@ -21,16 +21,16 @@ progress: See: .planning/PROJECT.md (updated 2026-05-08) **Core value:** Every idle mechanic must function as a metaphor that the player absorbs without being told. When economy and meaning conflict, meaning wins. -**Current focus:** Phase 02 — Season 1 Vertical Slice (Soil) — Wave 0 done; Wave 1 done (02-02 + 02-03); Wave 2 first plan (02-04) DONE; 02-05 is the only remaining Phase-2 work +**Current focus:** Phase 02 — Season 1 Vertical Slice (Soil) — ALL 5 PLANS DONE; ready for /gsd-verify-work ## Current Position -Phase: 02 (season-1-vertical-slice-soil) — 4/5 plans complete (Wave 0 + Wave 1 + first Wave 2 plan) -Plans: 5 of 5 created (3 waves); Wave 0 (02-01) DONE; Wave 1 (02-02 + 02-03) DONE; Wave 2 first plan (02-04 lura-gate-beats) DONE; Wave 2 final plan (02-05 letter-settings-e2e) queued -Status: Plan 02-04 lura-gate-beats executed in sequential mode — 3 atomic commits, 47 new tests (264/264 total green), npm run ci exits 0 with compile:ink integrated into the chain BEFORE test. STRY-01 / STRY-06 / STRY-07 (vacuous) / STRY-10 satisfied end-to-end. Lura's 3 Season-1 beats authored in voice + ship via the inklecate compile pipeline + inkjs runtime; gate visual indicator + DOM dialogue overlay both green. RESEARCH Assumption A6 verified first-try on Windows. Sim purity firewall holds: src/sim/narrative/ contains zero inkjs imports. -Last activity: 2026-05-09 -- Plan 02-04 execute complete +Phase: 02 (season-1-vertical-slice-soil) — 5/5 plans complete (all waves) +Plans: 5 of 5 created (3 waves); Wave 0 (02-01) DONE; Wave 1 (02-02 + 02-03) DONE; Wave 2 (02-04 + 02-05) DONE +Status: Plan 02-05 letter-settings-e2e executed in sequential mode — 4 atomic commits (26eb77a, 5d58d6c, dd48696, 31f8ede), 48 new tests (312/312 total vitest green), npm run ci exits 0, npx playwright test exits 0 in 1.5s. UX-02 / UX-10 / CORE-03 / CORE-11 / PIPE-07 / GARD-02 / GARD-04 satisfied end-to-end. Phase 2 vertical slice closed: a player can launch, plant, grow, harvest, meet Lura, leave the tab, return ≥5min later, see the letter from the garden in voice, dismiss to the live garden — and everything persists across reload (PIPE-07 e2e proves it). All 24 Phase-2 REQ-IDs structurally satisfied across the 5-plan set. The vertical slice could plausibly ship as a free standalone Season-1 prologue (banner concern #2's escape hatch realized). +Last activity: 2026-05-09 -- Plan 02-05 execute complete; Phase 2 ready for verification -Progress: [██░░░░░░░░] 19% +Progress: [██░░░░░░░░] 22% ## Verification Results @@ -61,21 +61,21 @@ Gates run: lint (exit 0), test (53/53 green, 12 files), validate:assets (2 asset **Velocity:** -- Total plans completed: 10 (1 partial — 01-05 Task 2 deferred via IOU) -- Average duration: ~5 min (Wave 1 baseline 6min; Wave 2 plans 4–8min; Plan 07 ~2min; Plan 02-01 ~12min; Plan 02-02 ~18min; Plan 02-03 ~12min — Phase-2 vertical-slice plans are heaviest) -- Total execution time: ~70 min across Phase 1 + Phase 2 Wave 0 + Wave 1 (both plans) +- Total plans completed: 12 (1 partial — 01-05 Task 2 deferred via IOU) +- Average duration: ~7 min across all plans; Phase-2 plans are heavier (12-24min each) +- Total execution time: ~106 min across Phase 1 + Phase 2 (all 12 plans) **By Phase:** | Phase | Plans | Total | Avg/Plan | |-------|-------|-------|----------| | 1. Foundations & Doctrine | 7/7 (complete) | ~30 min | ~5 min | -| 2. Season 1 Vertical Slice (Soil) | 4/5 (Wave 0 + Wave 1 + first Wave 2 plan complete) | ~66 min | ~16 min | +| 2. Season 1 Vertical Slice (Soil) | 5/5 (complete; ready for /gsd-verify-work) | ~86 min | ~17 min | **Recent Trend:** -- Last 5 plans: [01-07 ci-workflow · 02-01 foundations · 02-02 begin-plant-grow · 02-03 harvest-journal-fragments · 02-04 lura-gate-beats — all green] -- Trend: → (02-04 was 24 min — heaviest Phase-2 plan to date; first real player-narrative integration in the project covers a build pipeline + runtime + sim gate + UI overlay + canvas indicator + Garden scene wiring + 4 authored Ink files all in one plan; +47 new tests for 264/264 total green) +- Last 5 plans: [02-01 foundations · 02-02 begin-plant-grow · 02-03 harvest-journal-fragments · 02-04 lura-gate-beats · 02-05 letter-settings-e2e — all green] +- Trend: → (02-05 was 20min closing plan covering boot-path rewrite + 5 new components + Playwright e2e + Rule 3 auto-fix for gray-matter Buffer issue; +48 new tests for 312/312 total green) *Updated after each plan completion* @@ -106,6 +106,12 @@ Recent decisions affecting current work: - Plan 02-04 (Wave 2): STRY-07 satisfied vacuously for Phase 2 — zero .ink files contain Keeper-spoken lines. The gardener-keeper voice in compost beats acknowledges the player's actions but is never personified. Phase 7's binary choice surface (SEAS-09 / STRY-08) re-evaluates. - Plan 02-04 (Wave 2): Cadence values: DEFAULT_DELAY_MS=1500, PER_CHAR_MS=20, MAX_DELAY_MS=4000. Calibrated against typical 80-char line (3.1s) feeling close to a thoughtful texted reply, vs short "Oh." (1.56s) feeling like a beat. Tunable in playtest by editing src/ui/dialogue/ink-runtime.ts; constants exported for the Phase 8 UX-05 reduced-motion hook. - Plan 02-04 (Wave 2): Lura's `last_plant_type` derives from the most-recently-harvested fragment's tonal-register tag (warm → rosemary, contemplative → yarrow, heavy → winter-rose). The harvest pipeline doesn't currently store source plant type per harvest — Plan 02-05 may add that to offlineEvents. The tag-based proxy is sufficient for Phase 2's voice; Lura's branch on plant type is flavor, not a gate. +- Plan 02-05 (Wave 2): URL-flag FakeClock injection landed cleanly first-try, production-guarded by import.meta.env.PROD. Window slots `__tlgClock` / `__tlgFakeClock` / `__tlgStore` are written ONLY when `!isProd && devtime === 'fake'`; production builds silently ignore the flag. Playwright PIPE-07 spec exploits this to dispatch sim commands without pixel-precise canvas clicks — the test runs in 1.5s. +- Plan 02-05 (Wave 2): Compost-beat UI wired as a thin transient CompostToast (D-07 + GARD-04). Implementation choice surfaced in SUMMARY: minimum-viable bias chosen over the Ink runtime path. The Ink-authored richer voice in compost-acknowledgements.ink remains compiled + runtime-loadable for Phase 4+ to swap in if branching is needed. compostBeatTick monotonic counter (vs. boolean) ensures consecutive composts re-fire the toast without dedup. +- Plan 02-05 (Wave 2): Save-payload helpers extracted to src/save/payload.ts (W2 fix). Two-arg signature buildPayloadFromStore(state, nowMs) unifies Settings.tsx (passes Date.now()) and PhaserGame.tsx saveSync (passes clock.now()) without arity divergence. BLOCKER 3 — lastTickAt is the wall-clock anchor; the application layer owns the value. +- Plan 02-05 (Wave 2): 5-minute absence threshold (D-20) lives as ABSENCE_LETTER_THRESHOLD_MS constant in src/PhaserGame.tsx. Below 5min: silent resume, no overlay. ≥5min: letter Ink loads + slots bind + overlay opens. The Letter overlay's dismiss path calls bootstrapAudioContext synchronously inside the click handler (Pitfall 9 — returning player needs an audio gesture to land in the live garden). +- Plan 02-05 (Wave 2): gray-matter package replaced with a 15-line parseFrontmatter regex helper (Rule 3 — Blocking auto-fix). gray-matter pulls in Node's Buffer global which is undefined under Vite's browser bundle; the build emitted a 'Module buffer externalized' warning that masked the runtime ReferenceError surfacing only in real browsers (caught by the e2e). Bundle size dropped 2.2MB → 1.9MB as a tree-shake side effect. The dep itself remains in package.json as a deferred-items cleanup task. +- Plan 02-05 (Wave 2): Playwright dev port pinned to 5273 + --strictPort because the user's machine has another Vite project bound to 5173. reuseExistingServer false ensures the spec always launches a fresh Vite against this project. Documented in playwright.config.ts comment block. - Phases 4-7 deliver the remaining six Seasons in mechanic-introducing pairs (Season 2 alone with prestige, Seasons 3-4, Seasons 5-6, Season 7 alone) — at most one new mechanic per Season per the scope-defense doctrine. - Plan 01-01: scaffolded by hand (the official `npm create @phaserjs/game@latest` is interactive-only — `--template react-ts --yes` flags are silently ignored as of create-game v1.3.2); plan's documented fallback path was used. Vite 8 + TS 6 referenced-projects tsconfig layout adopted; `build` runs `tsc -b && vite build` so strict-TS gates every build. ESLint 9 installed → Plan 02 must use **flat config** (`eslint.config.js`), not legacy `.eslintrc.*`. - Plan 01-01: pre-installed `fake-indexeddb@^6` here so Plan 03 doesn't have to re-edit `package.json`. All Phase-1 dep versions match RESEARCH.md exactly within their `^` ranges. @@ -136,5 +142,5 @@ Items acknowledged and carried forward: ## Session Continuity Last session: 2026-05-09 -Stopped at: Phase 2 Wave 2 first plan (Plan 02-04 lura-gate-beats) executed in sequential mode — 3 atomic commits (c90f8f1, 7b79d11, 661f990), 47 new tests, 264/264 total green, npm run ci exits 0 (compile:ink integrated into the CI chain BEFORE test). STRY-01/STRY-06/STRY-07/STRY-10 satisfied end-to-end. Lura's 3 Season-1 beats authored in voice via the inklecate compile pipeline + inkjs runtime; gate visual indicator + DOM dialogue overlay both functional. RESEARCH Assumption A6 verified first-try on Windows via the bundled inklecate binary. Sim purity firewall holds: src/sim/narrative/ contains zero inkjs imports; ESLint sim-purity rule still green. Vite emits 4 lazy code-split chunks for compiled Ink JSON. SUMMARY at .planning/phases/02-season-1-vertical-slice-soil/02-04-lura-gate-beats-SUMMARY.md. -Next action: `/gsd-execute-phase 2` to continue with Plan 02-05 (offline catchup + letter + Settings + Playwright e2e — UX-02, UX-10, CORE-03, CORE-11, PIPE-07). Plan 02-05 will fold the compost-toast UI surface alongside the persistence-denied toast (compost-acknowledgements.ink runtime path is already wired; only the toast component is missing). Plan 02-05 is the final Phase-2 plan; completing it means Phase 2 is shippable as a free standalone Season-1 prologue. +Stopped at: Phase 2 Wave 2 final plan (Plan 02-05 letter-settings-e2e) executed in sequential mode — 4 atomic commits (26eb77a, 5d58d6c, dd48696, 31f8ede), 48 new tests, 312/312 total vitest green, npm run ci exits 0, Playwright PIPE-07 spec exits 0 in 1.5s test runtime / 4s end-to-end. UX-02 / UX-10 / CORE-03 / CORE-11 / PIPE-07 / GARD-02 / GARD-04 satisfied end-to-end. Phase 2 vertical slice closed: a player can launch, plant, grow, harvest, meet Lura, leave the tab, return ≥5min later, see the letter from the garden in voice, dismiss to the live garden — and everything persists across reload. URL-flag FakeClock injection production-guarded; gray-matter dep auto-removed (bundle 2.2MB → 1.9MB); compost beat wired as thin transient toast. SUMMARY at .planning/phases/02-season-1-vertical-slice-soil/02-05-letter-settings-e2e-SUMMARY.md. +Next action: `/gsd-verify-work` to UAT Phase 2. All 24 Phase-2 REQ-IDs structurally satisfied; the verifier consumes the e2e + SUMMARY for sign-off. After Phase 2 verification passes: `/gsd-discuss-phase 3` to begin the Watercolor & Cello Aesthetic phase (8 REQ-IDs: GARD-10, AEST-01..06, UX-05). diff --git a/.planning/phases/02-season-1-vertical-slice-soil/02-05-letter-settings-e2e-SUMMARY.md b/.planning/phases/02-season-1-vertical-slice-soil/02-05-letter-settings-e2e-SUMMARY.md new file mode 100644 index 0000000..72d4812 --- /dev/null +++ b/.planning/phases/02-season-1-vertical-slice-soil/02-05-letter-settings-e2e-SUMMARY.md @@ -0,0 +1,326 @@ +--- +phase: 02-season-1-vertical-slice-soil +plan: 05 +subsystem: letter-settings-e2e-vertical-slice-closeout +tags: [vertical-slice, letter, settings, save-lifecycle, offline-catchup, playwright-e2e, compost-toast, mvp, wave-2] + +# Dependency graph +requires: + - phase: 02-01 + provides: Zustand store + V1Payload extension fields + tick scheduler (drainTicks/computeOfflineCatchup) + save lifecycle hooks (registerSaveLifecycleHooks) + Phaser EventBus singleton + - phase: 02-02 + provides: sim/garden core + Garden Phaser scene (clock-via-window-slot read pattern) + BeginScreen + audio bootstrap + UI strings + - phase: 02-03 + provides: 17 Season-1 fragments + sim/memory selector + harvest/compost commands + Memory Journal + JournalIcon (D-23 first-harvest gate) + - phase: 02-04 + provides: inklecate compile pipeline + 4 authored Ink files + ink-loader (loadInkStory + INK_VARIABLE_MAP) + InkRenderer drip + LuraDialogue overlay + gate-renderer +provides: + - sim/offline module — OfflineEventBlockSchema (Zod) + EMPTY_OFFLINE_EVENTS + aggregateOfflineEvent pure aggregator (CONTEXT D-19) + - sim/garden/auto-harvest — autoHarvestReadyPlants silent-mode harvest branch (D-10) reusing the standard harvest() pipeline so selector + Pitfall 10 unlocks + STRY-10 Lura gate run identically; BLOCKER 3 invariant preserved (no lastTickAt writes) + - simulateOneTick silent mode — 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 Ink 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 extended — loadInkStory union accepts '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/save/payload.ts — buildPayloadFromStore(state, nowMs) + hydrateStoreFromPayload(state, payload). Two-arg signature (W2 fix) unifies Settings.tsx and PhaserGame.tsx saveSync without arity divergence. + - src/ui/letter/Letter.tsx — D-20 full-screen DOM overlay (UX-02). Loads compiled letter Ink, binds slots from offlineEvents, dismisses via Tend the garden button or backdrop click. Pitfall 9 — synchronous-inside-click bootstrapAudioContext call. + - src/ui/letter/letter-renderer.ts — pure buildLetterSlots helper (testable without happy-dom + Ink runtime). + - src/ui/settings/Settings.tsx — D-28 save-management modal (Export to Base64 / Import / Restore previous snapshot). BLOCKER 2 — Import pipeline is importFromBase64 → unwrap → migrate → hydrate. + - src/ui/settings/persistence-toast.tsx — D-30 one-time soft toast in voice when navigator.storage.persist() denies. Reads showPersistenceToast transient flag from session slice; sets persistenceToastShown=true after timeout. + - src/ui/settings/compost-toast.tsx — D-07 + GARD-04 thin transient compost beat toast (Plan 02-04 deferral). Cycles through uiStrings.post_harvest_beat lines on each compost dispatch; fades after 3.5s. + - PhaserGame.tsx full boot path rewrite — clock selection (?devtime=fake, production-guarded), save load (BLOCKER 1: unwrap → migrate), silent offline catchup via drainTicks(silent=true), letter overlay open at ≥5min absence, requestPersistence + showPersistenceToast wiring, Phaser start AFTER hydration, registerSaveLifecycleHooks with synchronous LocalStorage saveSync (Pitfall 7) + best-effort IDB write. W5 — lifecycle handle held in ref so outer cleanup detaches. + - App.tsx — mounts Letter, Settings, PersistenceToast, CompostToast, SettingsIcon (corner button); D-29 keyboard shortcuts (',' toggles Settings, 'j' toggles Journal via window event). + - tests/e2e/season1-loop.spec.ts — Playwright PIPE-07 smoke covering load → Begin → plant → fast-forward → harvest → reveal → journal → reload → persist. Sidesteps Phaser canvas pixel-clicking via window.__tlgStore command dispatch (production-guarded). + - playwright.config.ts — pinned port 5273 + --strictPort to avoid dev-server collisions; reuseExistingServer false; webServer timeout bumped 30s → 60s. + - src/content/loader.ts — gray-matter replaced with parseFrontmatter (15-line regex-based YAML frontmatter splitter). Rule 3 — Blocking auto-fix: gray-matter pulls in Node Buffer global which is undefined in the browser; the build emitted a 'Module buffer externalized' warning that masked the runtime ReferenceError surfacing only in real browsers (caught by the e2e). Bundle size dropped 2.2MB → 1.9MB as a side effect. + - PIPE-07 SATISFIED — full Phase-2 vertical slice exercised end-to-end in a real Chromium build under FakeClock injection. +affects: [/gsd-verify-work (Phase 2 verification consumes this plan's e2e + SUMMARY for sign-off), Phase 3 (Watercolor & Cello — paints over the working loop)] + +# Tech tracking +tech-stack: + added: [] + patterns: + - "Boot path as the binding layer (src/PhaserGame.tsx): clock selection → save load → unwrap → migrate → hydrate → silent offline catchup → maybe-open-letter → start Phaser → register save lifecycle hooks. Two useLayoutEffect blocks; lifecycle handle held in a ref so the outer cleanup can detach across the async IIFE boundary (W5)." + - "Silent-mode simulate (D-10): ctx.silent flips on for the offline catchup loop; simulateOneTick auto-harvests every ready-stage tile via autoHarvestReadyPlants. The harvest pipeline is reused identically — selector + Pitfall 10 unlocks + STRY-10 Lura gate all run; the only difference is who initiates (sim vs. player command)." + - "OfflineEventBlock as the letter's slot vocabulary (D-17/D-19): the silent catchup accumulates plantsBloomedCount + harvestedFragmentIds + luraBeatPending; buildLetterSlots converts to Ink VAR slots; letter Ink renders the authored skeleton. Pure data flow; no Date.now leaks." + - "Save-payload helpers extracted to src/save/payload.ts (W2 fix): single source of truth for buildPayloadFromStore(state, nowMs) + hydrateStoreFromPayload(state, payload). Two-arg signature lets PhaserGame's saveSync pass clock.now() and Settings.tsx pass Date.now() — same shape, different value, BLOCKER 3 invariant preserved (lastTickAt is wall-clock ms, owned by the application layer)." + - "Test-only window slots (__tlgStore + __tlgFakeClock + __tlgClock) gated by import.meta.env.PROD. Production builds silently ignore the ?devtime=fake URL flag; the slots themselves are never assigned. Playwright e2e exploits this to dispatch sim commands without pixel-precise canvas clicks (which Phaser doesn't make easy in headless)." + - "Compost toast as a thin transient surface (Plan 02-04 deferral): bumpCompostBeat monotonic counter in session slice → CompostToast watches the tick value via useEffect → cycles through uiStrings.post_harvest_beat lines. The Ink-authored richer voice in compost-acknowledgements.ink stays compiled + runtime-loadable for Phase 4+ to swap in if branching is needed." + - "Frontmatter parsing without gray-matter (Rule 3 auto-fix): 15-line parseFrontmatter regex handles the strict '------' shape under Vite's browser bundle without pulling in Node Buffer global. Bundle dropped 2.2MB → 1.9MB." + +key-files: + created: + - src/sim/offline/events.ts (OfflineEventBlockSchema + EMPTY_OFFLINE_EVENTS + aggregateOfflineEvent — D-19) + - src/sim/offline/events.test.ts (14 tests covering schema acceptance/rejection + aggregator immutability) + - src/sim/offline/index.ts (barrel) + - src/sim/garden/auto-harvest.ts (autoHarvestReadyPlants — D-10 silent-mode harvest) + - src/sim/garden/auto-harvest.test.ts (7 tests — single/multi-harvest, immature exclusion, BLOCKER 3 lastTickAt invariant, Lura gate threading) + - content/dialogue/season1/letter-from-the-garden.ink (authored letter Ink with VAR plants_bloomed / fragment_titles / lura_was_here) + - src/save/payload.ts (buildPayloadFromStore + hydrateStoreFromPayload shared helpers) + - src/ui/letter/Letter.tsx (D-20 full-screen overlay — loads letter Ink + binds slots + Pitfall 9 audio bootstrap on dismiss) + - src/ui/letter/Letter.test.tsx (7 tests — null-when-closed, dialog mounts, dismiss bootstraps audio + dismisses Begin gate, click-on-article does NOT dismiss, calls loadInkStory + ChoosePathString correctly) + - src/ui/letter/letter-renderer.ts (buildLetterSlots pure helper) + - src/ui/letter/letter-renderer.test.ts (10 tests — empty / single / multi / long-line slug fallback / missing-fragment fallback / lura_was_here flag / 50-bloom edge / zero-bloom path) + - src/ui/letter/index.ts (barrel) + - src/ui/settings/Settings.tsx (D-28 save-management modal) + - src/ui/settings/Settings.test.tsx (6 tests — null-when-closed, all 4 buttons mount, Close fires onClose, Export populates textarea + status, Import on bad payload shows soft error, Export→Import round-trip) + - src/ui/settings/persistence-toast.tsx (D-30 one-time soft toast) + - src/ui/settings/compost-toast.tsx (D-07 transient compost beat toast — Plan 02-04 deferral) + - src/ui/settings/compost-toast.test.tsx (4 tests — null at initial state, appears on bump, fades after timeout, re-fires on second bump) + - src/ui/settings/index.ts (barrel) + - tests/e2e/season1-loop.spec.ts (Playwright PIPE-07 full-loop smoke) + - .planning/phases/02-season-1-vertical-slice-soil/deferred-items.md (gray-matter package.json cleanup tracked) + modified: + - src/sim/garden/commands.ts (SimContext extended with `silent?: boolean`; simulateOneTick calls autoHarvestReadyPlants when ctx.silent; benign circular import with auto-harvest.ts is ESM-safe — neither needs the other at module-init time) + - src/sim/garden/index.ts (re-export autoHarvestReadyPlants) + - src/sim/index.ts (re-export ./offline) + - src/content/ink-loader.ts (extended union with 'letter-from-the-garden'; separate letterStoryGlob for lazy code-split; INK_VARIABLE_MAP gains plants_bloomed / fragment_titles / lura_was_here) + - src/save/index.ts (re-export buildPayloadFromStore + hydrateStoreFromPayload) + - src/store/session-slice.ts (showPersistenceToast + setShowPersistenceToast + compostBeatTick + bumpCompostBeat) + - src/ui/index.ts (re-export ./letter and ./settings) + - src/ui/journal/journal-icon.tsx (window 'tlg:toggle-journal' CustomEvent listener for D-29 'j' hotkey) + - src/PhaserGame.tsx (full boot path rewrite — clock selection + save load + silent catchup + lifecycle hooks) + - src/game/scenes/Garden.ts (formalized clock read via readClockSlot helper; compost branch calls bumpCompostBeat) + - src/App.tsx (mounts Letter, Settings, PersistenceToast, CompostToast, SettingsIcon; D-29 keyboard shortcuts) + - src/content/loader.ts (gray-matter replaced with parseFrontmatter; Rule 3 blocking-issue auto-fix) + - playwright.config.ts (port 5273 + strictPort; reuseExistingServer false; webServer timeout 60s) + - package.json (test:e2e script) + removed: [] + +key-decisions: + - "URL-flag FakeClock injection landed cleanly first-try via window.__tlgClock + __tlgFakeClock + __tlgStore slots, all gated by import.meta.env.PROD. Production builds silently ignore ?devtime=fake. Verified by Playwright running successfully with the flag and structurally by the production guard in PhaserGame.tsx's first useLayoutEffect." + - "Compost-beat UI wired as a thin transient toast (CompostToast) rather than the full Ink runtime surface. Implementation choice surfaced per the plan's must_have: minimum-viable bias keeps Phase 2 closing tight; the Ink-authored compost-acknowledgements.ink content stays compiled + runtime-loadable so Phase 4+ can swap in richer voice without touching sim or store." + - "Save-payload helpers extracted to src/save/payload.ts (W2 fix). Two-arg signature buildPayloadFromStore(state, nowMs) unifies Settings.tsx (passes Date.now()) and PhaserGame.tsx saveSync (passes clock.now()) without arity divergence. BLOCKER 3 — lastTickAt is the wall-clock anchor; the application layer owns the value." + - "5-minute absence threshold (D-20) lives as ABSENCE_LETTER_THRESHOLD_MS in src/PhaserGame.tsx (line ~76 of the constants block, exported via grep-able literal). Below 5min: silent resume, no overlay. ≥5min: letter Ink loads + slots bind + overlay opens. Verified by structural code review; the e2e exercises the <5min path implicitly (the spec's reload happens in <1s wall-clock so the overlay does NOT fire on returning-player reload — fragment persistence is what we assert there)." + - "Compost-beat compostBeatTick is a monotonic counter (vs. boolean) so consecutive composts re-fire the toast without dedup. Boolean would have required a manual reset after the timeout; the counter pattern is simpler + matches React's useEffect dep-array semantics for re-firing on every change." + - "Silent-mode auto-harvest reuses the standard harvest() pipeline (vs. duplicating the selector + unlock logic). The cycle (auto-harvest.ts imports harvest from commands.ts; commands.ts imports autoHarvestReadyPlants from auto-harvest.ts) is benign in ESM — neither function references the other at module-init time. Verified empirically by all 312 tests passing." + - "gray-matter package.json entry left in place as a deferred-items cleanup task. The dep is no longer imported anywhere under src/ but removing it is a separate maintenance commit (out of Plan 02-05 scope, which only auto-fixed the runtime block)." + - "Playwright dev port pinned to 5273 (not the 5173 default) because the user's machine has another Vite project bound to 5173 (Apothecary). reuseExistingServer: false ensures the spec always launches a fresh Vite against this project's vite.config.ts. --strictPort makes a port collision fail loudly rather than silently latching onto another app." + +patterns-established: + - "Boot path = the binding layer pattern. src/PhaserGame.tsx is the only place where save layer + scheduler + sim + store + Phaser all meet. It runs synchronously inside a useLayoutEffect (the async IIFE inside is for the await pattern only). Reusable for Phase 4+ Season-transition save-on-prestige logic." + - "Silent-mode simulate (D-10) — pure boolean flag on SimContext that flips behavior without changing function signatures. Reusable for Phase 4+ Memory Storms (Season 4) which may want a 'storm-tick' branch of similar shape." + - "Test-only window slots gated by import.meta.env.PROD. Reusable for Phase 8's visual-regression toolkit (which may want to expose render-tier internals to a test harness without polluting production builds)." + - "Thin transient toast pattern (CompostToast / PersistenceToast): tick counter or boolean in the session slice → component watches via useEffect → renders for a few seconds → fades. Reusable for any Phase 4+ Season-transition acknowledgement, Memory Storm warning, etc." + - "Frontmatter parsing without gray-matter (parseFrontmatter): 15-line regex handles strict YAML frontmatter under Vite's browser bundle. Reusable anywhere a project wants Markdown-with-frontmatter content without pulling in Node Buffer global." + +requirements-completed: [UX-02, UX-10, CORE-03, CORE-11, PIPE-07, GARD-02, GARD-04] + +# Metrics +duration: 20min +completed: 2026-05-09 +--- + +# Phase 2 Plan 05: Letter, Settings, Save Lifecycle, e2e Summary + +## One-liner + +Phase 2 closes — sim/offline + auto-harvest silent-mode branch (D-10), letter-from-the-garden Ink (UX-02 with the slot vocabulary plants_bloomed/fragment_titles/lura_was_here populated from offlineEvents), full-screen Letter overlay (D-20 with Pitfall 9 audio bootstrap on dismiss), Settings save-management UI (D-28 Export/Import/Restore with BLOCKER 2 unwrap→migrate pipeline), persistence-result toast (D-30) and a thin compost-beat toast (Plan 02-04 deferral), full PhaserGame.tsx boot path rewrite wiring clock selection (URL-flag FakeClock injection production-guarded by import.meta.env.PROD) + save lifecycle (UX-10) + offline catchup, and the Playwright PIPE-07 spec exercising the entire authored loop end-to-end (load → Begin → plant → fast-forward → harvest → reveal → journal → reload → persist). The Phase-2 vertical slice could plausibly ship as a free standalone Season-1 prologue. + +## Performance + +- **Duration:** ~20 min (sequential executor) +- **Started:** 2026-05-09T14:44:16Z +- **Completed:** 2026-05-09T15:08:00Z (approximate; this commit fires) +- **Tasks:** 3 main + 1 deferral-fold-in (compost toast) +- **Files created:** 19 (incl. tests + .ink + barrel files) +- **Files modified:** 14 + +## Task Commits + +Each task was committed atomically: + +1. **Task 1: sim/offline + auto-harvest + letter Ink + letter-renderer** — `26eb77a` (feat) +2. **Task 2: Letter overlay + Settings UI + boot save lifecycle + clock injection** — `5d58d6c` (feat) +3. **Task 3: Playwright e2e for PIPE-07 — full Phase-2 loop** — `dd48696` (test) +4. **Compost beat toast wiring (Plan 02-04 deferral)** — `31f8ede` (feat) + +**Plan metadata:** _(this commit)_ — `docs(02-05): complete letter-settings-e2e plan` + +## Accomplishments + +- **Phase 2 vertical slice closed end-to-end on real authored content + real save round-trip + real offline catchup.** A player can launch, plant rosemary, watch it grow, harvest a Season-1 fragment authored in voice, see it filed in the Memory Journal, meet Lura at the gate (Plan 02-04), close the tab, return ≥5min later, see the letter from the garden in voice, dismiss to the live garden — and everything persists across reload. +- **Banner Concern 4 (system-clock cheating) defended at every layer.** The boot path's computeOfflineCatchup clamps elapsed ms at MAX_OFFLINE_MS (24h); drainTicks refuses negative deltas; STRY-10 narrative gating counts harvest events not wall time (Plan 02-04); the ESLint sim-purity rule (Plan 02-01 Block 3) prevents Date.now/setInterval inside src/sim/. Plan 02-05 inherits all of these and adds nothing that breaks them. +- **PIPE-07 PASSES.** Playwright spec runs in 1.5s test-runtime, 4s end-to-end including dev-server cold start, well under the <30s budget. The spec is the canonical proof that Phase 2 is shippable: it actually loads the dev build in Chromium, dispatches sim commands, exercises the full loop, and asserts persistence. +- **24/24 Phase-2 REQ-IDs structurally satisfied across the 5-plan set.** See the table at the end of this summary; every requirement has a plan that owned it and a SUMMARY documenting the satisfaction. +- **Bundle size DROPPED.** Removing gray-matter (Rule 3 auto-fix during the e2e) brought the entry chunk from 2.2MB → 1.9MB without changing any feature surface. The Markdown loader path now uses a 15-line parseFrontmatter regex helper. + +## Files Created/Modified + +See frontmatter `key-files` for the full list (19 created + 14 modified). + +## Decisions Made + +See `key-decisions` in frontmatter (8 entries). Headlines: + +1. URL-flag FakeClock injection landed cleanly first-try, production-guarded by `import.meta.env.PROD`. +2. Compost-beat UI wired as a thin transient toast (CompostToast) — minimum-viable; Ink runtime path stays available for Phase 4+ to swap in richer voice. +3. Save-payload helpers extracted to `src/save/payload.ts` (W2) — two-arg `(state, nowMs)` signature unifies Settings.tsx (passes `Date.now()`) and PhaserGame.tsx saveSync (passes `clock.now()`). +4. 5-minute absence threshold lives as `ABSENCE_LETTER_THRESHOLD_MS` constant (CONTEXT D-20). +5. `compostBeatTick` is a monotonic counter (not boolean) so consecutive composts re-fire the toast without dedup. +6. Silent-mode auto-harvest reuses the standard `harvest()` pipeline; the benign ESM circular import is verified by all 312 tests passing. +7. `gray-matter` package.json entry left in `package.json` for a separate cleanup commit (deferred-items.md tracks it). +8. Playwright dev port pinned to 5273 + `--strictPort` to avoid collisions with another Vite project on the user's machine. + +## Compost-Beat UI Wiring Approach + +**Chosen: thin transient CompostToast** (`src/ui/settings/compost-toast.tsx`) reading from `uiStrings[1].post_harvest_beat` (3 short authored lines that rotate per compost). + +**Trade-off vs. Ink runtime path**: Phase 2 is closing tight; the user has been pushing back on ceremony. The Ink-authored richer voice in `content/dialogue/season1/compost-acknowledgements.ink` (6 short lines in the gardener-keeper voice, branched on `fragment_count`) IS: +- Compiled to JSON at every build (`npm run compile:ink` emits 5 .ink.json files now: 4 Lura + 1 letter; the compost compile output is also there). +- Runtime-loadable via `loadInkStory('compost-acknowledgements')` which Plan 02-04 wired. +- Sitting at the wiring point — `src/ui/settings/compost-toast.tsx` could be replaced wholesale with an Ink-driven component without touching the sim, store, or App.tsx mount. + +The thin-toast surface satisfies D-07 (post-harvest acknowledgement beat) + GARD-04 (compost yields a tonal beat) for Phase 2's minimum-viable closeout. Phase 4+ may upgrade to the Ink runtime path if playtest demands richer voice. + +## URL-Flag FakeClock Injection — Verification + +**Landed cleanly first-try.** No iteration was needed on the production-guard or the slot-exposure mechanics. Verification: + +- `window.__tlgFakeClock` and `window.__tlgStore` are written ONLY when `!isProd && devtime === 'fake'`. The production guard reads `import.meta.env.PROD` (Vite injects `true` for `vite build`, `false` for `vite dev`). +- Playwright spec uses `?devtime=fake` → both slots become available → spec dispatches `enqueueCommand` directly via `__tlgStore.getState().enqueueCommand({...})` and advances time via `__tlgFakeClock.advance(ms)`. +- Garden scene reads the clock via `readClockSlot()` which falls back to `wallClock` if no slot is set (covers the production code path + the unit-test path that instantiates the scene without going through `PhaserGame.tsx`). + +## Playwright Run Time + +- **Test runtime:** 1.5s (single spec, single test, single browser). +- **End-to-end including dev-server cold start:** ~4s. +- **Goal:** <30s per VALIDATION.md sampling rate row. Achieved with significant headroom. + +## Manual Smoke Test Confirmation + +**Not performed in this execution session** (sequential automated executor; user has not yet run `npm run dev`). Structural verification is comprehensive: + +- 312/312 Vitest cases green (was 264 before this plan; +48 new — 14 sim/offline + 7 sim/garden auto-harvest + 10 letter-renderer + 7 Letter + 6 Settings + 4 CompostToast). +- `npm run lint` exits 0 (zero ESLint sim-purity violations; sim/offline + sim/garden/auto-harvest contain zero Date.now / setInterval). +- `npm run compile:ink` emits 5 .ink.json files (Plan 02-04's 4 + this plan's letter). +- `npm run build` exits 0; entry bundle 1.9MB (down from 2.2MB after gray-matter removal); Vite emits 5 lazy code-split chunks for the compiled Ink. +- `npm run check:bundle-split` exits 0 (PIPE-02 OK — Season-1 content reachable via build output). +- `npm run ci` exits 0 end-to-end with all six gates green. +- `npx playwright test tests/e2e/season1-loop.spec.ts` exits 0 in 4s. + +The Plan 02-05 Playwright e2e IS the manual-smoke-equivalent for the active-play loop end-to-end. The user can run `npm run dev` to drive it interactively at any point. + +## Final Tally — All 24 Phase-2 REQ-IDs + +| REQ-ID | Plan | Status | +|--------|------|--------| +| CORE-02 | 02-01 (drainTicks fixed-timestep) + 02-02 (Garden update loop) | ✓ | +| CORE-03 | 02-01 (computeOfflineCatchup 24h cap) + 02-05 (boot path threads it) | ✓ | +| CORE-11 | 02-01 (drainTicks negative refusal) | ✓ | +| GARD-01 | 02-02 (plantSeed + SeedPicker) | ✓ | +| GARD-02 | 02-02 (growth state machine) + 02-05 (PIPE-07 verifies save round-trip) | ✓ | +| GARD-03 | 02-03 (harvest + reveal modal) | ✓ | +| GARD-04 | 02-03 (compost command) + 02-04 (compost.ink content) + 02-05 (CompostToast wired) | ✓ | +| MEMR-01 | 02-03 (selector returns exactly one fragment per harvest) | ✓ | +| MEMR-02 | 02-03 (17 fragments authored under /content/seasons/01-soil/) | ✓ | +| MEMR-03 | 02-03 (FragmentSchema regex enforces stable string ids) | ✓ | +| MEMR-04 | 02-03 (Memory Journal modal grouped by Season) | ✓ | +| MEMR-05 | 02-03 (DOM-rendered selectable text via `
` + userSelect:'text') | ✓ |
+| MEMR-06 | 02-03 (mulberry32-seeded selector + gating + no-dup + sentinel fallback) | ✓ |
+| STRY-01 | 02-04 (3 Lura beats authored + LuraDialogue overlay) | ✓ |
+| STRY-06 | 02-04 (compile-ink.mjs + 4 Lura beats) + 02-05 (letter Ink uses same pipeline) | ✓ |
+| STRY-07 | 02-04 (vacuously satisfied — zero Keeper-spoken lines in Phase-2 .ink files) | ✓ |
+| STRY-10 | 02-04 (lura-gate counts harvest events not wall time; FakeClock-24h-no-harvest test) | ✓ |
+| AEST-07 | 02-02 (BeginScreen + bootstrapAudioContext synchronous-inside-click) | ✓ |
+| UX-01 | 02-02 (Begin no-clutter overlay) + 02-03 (Journal reveals after first harvest) | ✓ |
+| UX-02 | **02-05 (Letter overlay loads letter-from-the-garden.ink + binds slots from offlineEvents + Pitfall 9 audio bootstrap)** | ✓ |
+| UX-10 | 02-01 (registerSaveLifecycleHooks + saveOnSeasonTransition) + 02-05 (PhaserGame.tsx boot wiring) | ✓ |
+| UX-11 | 02-01 (formatHumanReadable / BigQty.format K/M/B/T/scientific) | ✓ |
+| PIPE-02 | 02-02 (loadSeasonFragments lazy surface) + 02-03 (check-bundle-split.mjs structural verifier) | ✓ |
+| PIPE-07 | **02-05 (Playwright e2e — full Phase-2 loop end-to-end in Chromium)** | ✓ |
+
+**24 / 24 covered.**
+
+## Total Test Count Across Phase 1 + Phase 2
+
+- Phase 1 baseline: 53 tests
+- Plan 02-01 (Wave 0): +75 (≈) → 128
+- Plan 02-02 (Wave 1): +35 → 163
+- Plan 02-03 (Wave 1): +54 → 217
+- Plan 02-04 (Wave 2): +47 → 264
+- **Plan 02-05 (Wave 2): +48 → 312**
+
+312/312 tests green; 39 test files. `npm run ci` runs all of them in ~5s on this machine (Vitest only; Playwright is not in `ci` per minimum-viable doctrine — runs separately via `npm run test:e2e` before /gsd-verify-work and on release).
+
+## Deviations from Plan
+
+### Auto-fixed Issues
+
+**1. [Rule 3 — Blocking] gray-matter pulls in Node Buffer global which is undefined under Vite's browser bundle**
+
+- **Found during:** Task 3 — running the Playwright e2e for the first time. Vite dev mode surfaced `ReferenceError: Buffer is not defined` from `gray-matter/lib/utils.js`. The `vite build` step had been emitting a `Module "buffer" has been externalized for browser compatibility` warning since Plan 02-03 shipped; the warning masked a real runtime error that surfaces only in real browsers (Vitest + happy-dom never exercised the Markdown loader path because the existing tests use the test-only `loadFragmentsFromGlob` helper with mocked input).
+- **Issue:** The Markdown fragment loader (lura-first-letter.md, winter-rose-night.md from Plan 02-03) was effectively broken in production browsers since its initial commit. Players running the dev or production build would have seen the React app crash at module-eval time when `loadMdFragments()` ran inside `src/content/loader.ts`.
+- **Fix:** Replaced `gray-matter` with a 15-line `parseFrontmatter` regex helper in `src/content/loader.ts`. Handles the strict `------` shape the .md files use; anything else falls through cleanly. No new dependencies; the existing `yaml` package already does the YAML parse.
+- **Files modified:** src/content/loader.ts
+- **Verification:** `npm run dev` no longer throws Buffer ReferenceError; Playwright e2e plant→harvest→reveal round-trip works end-to-end; bundle size dropped 2.2MB → 1.9MB as a tree-shake side effect; 13 content tests still green.
+- **Committed in:** dd48696 (Task 3)
+- **Deferred follow-up:** `gray-matter` package.json entry could be removed in a maintenance commit (no code references it). Tracked in `.planning/phases/02-season-1-vertical-slice-soil/deferred-items.md`.
+
+### Tightenings (within plan author's discretion)
+
+1. **Compost-beat UI wired as a CompostToast** with 4 dedicated tests (`src/ui/settings/compost-toast.test.tsx`). The plan said "implementation choice surfaced in SUMMARY"; chose the minimum-viable thin-toast surface to keep Phase 2 closing tight. Surface choice documented in this SUMMARY's Compost-Beat UI Wiring Approach section above.
+2. **Playwright dev port + strictPort** — pinned to 5273 (not the default 5173) because the user's machine has another Vite project bound to 5173. Documented in playwright.config.ts comment block.
+3. **Boot path's two-stage Phaser start** — start Phaser AFTER state hydration so the Garden scene's create() reads the correct initial tickCount + tiles. The plan's draft sketched this; the implementation formalized it as the canonical ordering (await save load → hydrate → start Phaser → register lifecycle hooks).
+
+## Issues Encountered
+
+The gray-matter Buffer issue was the only substantive friction point. Beyond that, the plan was unusually well-specified — the 4 commits (3 main tasks + 1 compost-toast wiring) implemented as drafted with only minor cosmetic adjustments (e.g., `vi.hoisted` for the bootstrapSpy in Letter.test.tsx since Vitest hoists vi.mock factories above imports).
+
+## 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 + the compost-toast follow-up.
+
+## User Setup Required
+
+None — no external service configuration required. All work is in-tree TypeScript / authored content / a single Playwright spec.
+
+## Phase 2 Readiness for Verification
+
+- Phase 2's 5 plans are all complete:
+  - 02-01-foundations (Wave 0) — DONE
+  - 02-02-begin-plant-grow (Wave 1) — DONE
+  - 02-03-harvest-journal-fragments (Wave 1) — DONE
+  - 02-04-lura-gate-beats (Wave 2) — DONE
+  - **02-05-letter-settings-e2e (Wave 2) — DONE (this commit)**
+- All 24 Phase-2 REQ-IDs satisfied across the 5-plan set; the table above maps each.
+- `npm run ci` exits 0 (lint + compile:ink + 312/312 vitest + validate:assets + build + check:bundle-split).
+- `npm run test:e2e` exits 0 (Playwright PIPE-07 spec; ~4s end-to-end).
+- Phase 1's 53 tests + Phase 2's 259 new tests = 312 total green.
+- The vertical slice could plausibly ship as a free standalone Season-1 prologue: a player can launch, plant, grow, harvest, meet Lura, leave, return to a letter, dismiss, and the save round-trip survives all of it. The 7-Season scope risk's defended-by-an-escape-hatch is realized.
+
+**No blockers, no IOUs, no carried-over technical debt this plan produced** beyond the gray-matter dep cleanup tracked in deferred-items.md.
+
+## Self-Check: PASSED
+
+Verification performed at SUMMARY-write time:
+
+- src/sim/offline/events.ts: FOUND
+- src/sim/offline/events.test.ts: FOUND
+- src/sim/offline/index.ts: FOUND
+- src/sim/garden/auto-harvest.ts: FOUND
+- src/sim/garden/auto-harvest.test.ts: FOUND
+- content/dialogue/season1/letter-from-the-garden.ink: FOUND
+- src/save/payload.ts: FOUND
+- src/ui/letter/Letter.tsx: FOUND
+- src/ui/letter/Letter.test.tsx: FOUND
+- src/ui/letter/letter-renderer.ts: FOUND
+- src/ui/letter/letter-renderer.test.ts: FOUND
+- src/ui/letter/index.ts: FOUND
+- src/ui/settings/Settings.tsx: FOUND
+- src/ui/settings/Settings.test.tsx: FOUND
+- src/ui/settings/persistence-toast.tsx: FOUND
+- src/ui/settings/compost-toast.tsx: FOUND
+- src/ui/settings/compost-toast.test.tsx: FOUND
+- src/ui/settings/index.ts: FOUND
+- tests/e2e/season1-loop.spec.ts: FOUND
+- .planning/phases/02-season-1-vertical-slice-soil/deferred-items.md: FOUND
+- Commit 26eb77a (Task 1 — sim/offline + auto-harvest + letter Ink + letter-renderer): FOUND in `git log --oneline --all`
+- Commit 5d58d6c (Task 2 — Letter overlay + Settings + boot save lifecycle + clock injection): FOUND in `git log --oneline --all`
+- Commit dd48696 (Task 3 — Playwright e2e for PIPE-07): FOUND in `git log --oneline --all`
+- Commit 31f8ede (compost-toast wiring — Plan 02-04 deferral): FOUND in `git log --oneline --all`
+- `npm run ci` exits 0: VERIFIED
+- 312/312 vitest tests pass: VERIFIED
+- `npx playwright test tests/e2e/season1-loop.spec.ts` exits 0 (1.5s test runtime, ~4s end-to-end): VERIFIED
+- ESLint sim-purity rule: zero violations (`npm run lint` exits 0)
+- Build: `npm run build` exits 0; entry bundle 1.9MB (down from 2.2MB after gray-matter removal)
+- 5 lazy code-split Ink chunks emitted: lura-arrival, lura-mid, lura-farewell, compost-acknowledgements, letter-from-the-garden
+- All 24 Phase-2 REQ-IDs structurally satisfied across the 5-plan set