e5d449095d
- 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.
327 lines
32 KiB
Markdown
327 lines
32 KiB
Markdown
---
|
||
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 '---<yaml>---<body>' 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 `<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 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 `---<yaml>---<body>` 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
|