- 02-05-letter-settings-e2e-SUMMARY.md: full plan summary (frontmatter + decisions + REQ table + self-check). All 24 Phase-2 REQ-IDs structurally satisfied across the 5-plan set. - STATE.md: marked Plan 02-05 complete; Phase 2 ready for /gsd-verify-work; progress 19% → 22%; next action set to verifier. - ROADMAP.md: Plan 02-05 row marked [x] with duration + SUMMARY ref. - REQUIREMENTS.md: UX-02 / UX-10 / CORE-03 / PIPE-07 marked complete with traceability annotations citing Plan 02-05's contribution; per-row Plan 02-05 references added to UX-02, UX-10, CORE-03; PIPE-07 traceability table row updated.
32 KiB
phase, plan, subsystem, tags, requires, provides, affects, tech-stack, key-files, key-decisions, patterns-established, requirements-completed, duration, completed
| phase | plan | subsystem | tags | requires | provides | affects | tech-stack | key-files | key-decisions | patterns-established | requirements-completed | duration | completed | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 02-season-1-vertical-slice-soil | 05 | letter-settings-e2e-vertical-slice-closeout |
|
|
|
|
|
|
|
|
|
20min | 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:
- Task 1: sim/offline + auto-harvest + letter Ink + letter-renderer —
26eb77a(feat) - Task 2: Letter overlay + Settings UI + boot save lifecycle + clock injection —
5d58d6c(feat) - Task 3: Playwright e2e for PIPE-07 — full Phase-2 loop —
dd48696(test) - 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:
- URL-flag FakeClock injection landed cleanly first-try, production-guarded by
import.meta.env.PROD. - 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.
- Save-payload helpers extracted to
src/save/payload.ts(W2) — two-arg(state, nowMs)signature unifies Settings.tsx (passesDate.now()) and PhaserGame.tsx saveSync (passesclock.now()). - 5-minute absence threshold lives as
ABSENCE_LETTER_THRESHOLD_MSconstant (CONTEXT D-20). compostBeatTickis a monotonic counter (not boolean) so consecutive composts re-fire the toast without dedup.- Silent-mode auto-harvest reuses the standard
harvest()pipeline; the benign ESM circular import is verified by all 312 tests passing. gray-matterpackage.json entry left inpackage.jsonfor a separate cleanup commit (deferred-items.md tracks it).- Playwright dev port pinned to 5273 +
--strictPortto 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:inkemits 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.tsxcould 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.__tlgFakeClockandwindow.__tlgStoreare written ONLY when!isProd && devtime === 'fake'. The production guard readsimport.meta.env.PROD(Vite injectstrueforvite build,falseforvite dev).- Playwright spec uses
?devtime=fake→ both slots become available → spec dispatchesenqueueCommanddirectly via__tlgStore.getState().enqueueCommand({...})and advances time via__tlgFakeClock.advance(ms). - Garden scene reads the clock via
readClockSlot()which falls back towallClockif no slot is set (covers the production code path + the unit-test path that instantiates the scene without going throughPhaserGame.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 lintexits 0 (zero ESLint sim-purity violations; sim/offline + sim/garden/auto-harvest contain zero Date.now / setInterval).npm run compile:inkemits 5 .ink.json files (Plan 02-04's 4 + this plan's letter).npm run buildexits 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-splitexits 0 (PIPE-02 OK — Season-1 content reachable via build output).npm run ciexits 0 end-to-end with all six gates green.npx playwright test tests/e2e/season1-loop.spec.tsexits 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 <pre> + 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 definedfromgray-matter/lib/utils.js. Thevite buildstep had been emitting aModule "buffer" has been externalized for browser compatibilitywarning 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-onlyloadFragmentsFromGlobhelper 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 insidesrc/content/loader.ts. - Fix: Replaced
gray-matterwith a 15-lineparseFrontmatterregex helper insrc/content/loader.ts. Handles the strict---<yaml>---<body>shape the .md files use; anything else falls through cleanly. No new dependencies; the existingyamlpackage already does the YAML parse. - Files modified: src/content/loader.ts
- Verification:
npm run devno 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-matterpackage.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)
- 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. - 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.
- 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 ciexits 0 (lint + compile:ink + 312/312 vitest + validate:assets + build + check:bundle-split).npm run test:e2eexits 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 ingit log --oneline --all - Commit
5d58d6c(Task 2 — Letter overlay + Settings + boot save lifecycle + clock injection): FOUND ingit log --oneline --all - Commit
dd48696(Task 3 — Playwright e2e for PIPE-07): FOUND ingit log --oneline --all - Commit
31f8ede(compost-toast wiring — Plan 02-04 deferral): FOUND ingit log --oneline --all npm run ciexits 0: VERIFIED- 312/312 vitest tests pass: VERIFIED
npx playwright test tests/e2e/season1-loop.spec.tsexits 0 (1.5s test runtime, ~4s end-to-end): VERIFIED- ESLint sim-purity rule: zero violations (
npm run lintexits 0) - Build:
npm run buildexits 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