Files
TheLastGarden/.planning/phases/02-season-1-vertical-slice-soil/02-VERIFICATION.md
T
2026-05-09 11:27:16 -04:00

237 lines
32 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
phase: 02-season-1-vertical-slice-soil
verified: 2026-05-09T11:24:00Z
verifier_run_at: 2026-05-09T11:24:00Z
status: human_needed
score: 24/24 must-haves structurally verified
overrides_applied: 0
re_verification: false
per_req:
CORE-02: PASS
CORE-03: PASS
CORE-11: PASS
GARD-01: PASS
GARD-02: PASS
GARD-03: PASS
GARD-04: PASS
MEMR-01: PASS
MEMR-02: PASS
MEMR-03: PASS
MEMR-04: PASS
MEMR-05: PASS
MEMR-06: PASS
STRY-01: PASS (structural; tone needs human read)
STRY-06: PASS
STRY-07: PASS (vacuous — Phase 2 ships zero Keeper-spoken lines)
STRY-10: PASS
AEST-07: PASS
UX-01: PASS
UX-02: PASS (structural; letter tone needs human read)
UX-10: PASS
UX-11: PASS
PIPE-02: PASS (structural; chunkContentMatch=true; chunkNameMatch deferred to Phase 4+ when consumers move to lazy-only)
PIPE-07: PASS
human_verification:
- test: "Read the three Lura .ink files in voice"
expected: "Lura reads as warmth-anchor / contrast / not co-griever; specific + intermittent + sometimes funny; the farewell carries 'The garden persists.' as the load-bearing turn"
why_human: "Tone quality is inherently subjective; the bible voice cannot be programmatically scored. The author already noted in 02-04 SUMMARY that 'user reviews the .ink files at next merge.' This is that review."
- test: "Read the letter-from-the-garden.ink in voice"
expected: "Letter is contemplative, anti-FOMO compliant, never 'you missed X come back tomorrow', honors D-11 (24h cap silent in voice — no numeric '28h' copy), reads like short fiction not stat dump"
why_human: "UX-02 explicitly forbids stat-dump framing; tonal compliance is a human judgment. Code structurally enforces slot-based composition but the words themselves need eyes."
- test: "Run npm run dev and exercise the loop manually"
expected: "Begin screen appears with no clutter → click Begin → AudioContext bootstraps → garden visible → click empty tile → SeedPicker popover appears → choose rosemary → wait ~2min for growth → click ready plant → fragment reveal modal → close → journal-icon appears → click → modal lists fragment → reload → fragment persists. Compose ~9 harvests to fire all 3 Lura beats in sequence and confirm cadence + visual gate indicator + DOM-rendered selectable text."
why_human: "Visual layout, ready-pulse cadence, gate alpha-pulse, dialogue drip cadence (1500ms base + 20ms/char), and overall feel are not testable without live eyes. Plan SUMMARYs explicitly state 'Manual smoke test: not performed in this execution session.'"
- test: "Verify the Begin screen feels A-Dark-Room-clean"
expected: "First-load shows ONLY 'The Last Garden' / 'tend' / Begin button — no HUD, no settings icon visible behind, no journal, no seed picker. After clicking Begin, garden tiles fade in. Returning-player path (D-22) skips Begin entirely."
why_human: "AEST-07 + UX-01 are about visual restraint; the typographic placeholder gets a human pass on whether it lands tonally."
- test: "Verify offline catchup → letter overlay flow on a returning save"
expected: "Plant a seed, close tab, return after ≥5 minutes (or simulate via clock manipulation in dev), letter overlay appears with composed Ink text reflecting plants_bloomed / fragment_titles / lura_was_here slots; a single tap dismisses to live garden; audio bootstraps on dismiss (Pitfall 9)."
why_human: "The Playwright e2e exercises the <5min path (no letter); the ≥5min letter path is structurally tested (Letter.test.tsx + buildLetterSlots.test.ts) but the user-facing flow needs eyes."
- test: "Confirm the gate visual indicator + LuraDialogue overlay flow"
expected: "After 1st harvest, soft alpha-pulse appears on the gate at canvas (880, 384); click → React DOM dialogue overlay opens; lines drip with text-message cadence; close → resolvePendingLuraBeat marks visited; second click on gate (no pending) is a soft no-op."
why_human: "Phaser canvas rendering and pulse cadence are not unit-tested (Phaser scenes need a real canvas; covered by Plan 02-05 e2e but only structurally for plant rendering, not gate)."
---
# Phase 2: Season 1 Vertical Slice (Soil) — Verification Report
**Phase Goal:** Player can launch the game, plant a seed, watch it grow, harvest a memory fragment authored in real Season 1 content, meet Lura at the gate, leave the tab for hours, and return to a letter-from-the-garden describing what bloomed — the entire core loop and content pipeline proven on Season 1 with no aesthetic polish required.
**Verified:** 2026-05-09T11:24:00Z
**Status:** HUMAN_NEEDED — all 24 REQ-IDs structurally PASS; tone-quality items need human eyeballs before sign-off.
**Re-verification:** No — initial verification.
**Overall verdict:** PHASE STRUCTURALLY COMPLETE — code passes every automated gate; tone review and live-loop verification are the remaining work.
---
## Verification Gates (Actual Runs at 11:18-11:23)
| Gate | Command | Result |
|------|---------|--------|
| Tests | `npm test` | 39 test files, **312/312 tests** passed (5.54s) |
| Lint | `npm run lint` | Exit 0 (2 informational boundaries-plugin deprecation notices about a v5→v6 rename — not lint warnings; informational stderr only) |
| Build | `npm run build` | Exit 0; entry chunk 1.9MB; 5 lazy code-split Ink chunks (lura-arrival, lura-mid, lura-farewell, compost-acknowledgements, letter-from-the-garden) |
| Bundle split | `npm run check:bundle-split` | Exit 0; PIPE-02 OK — `chunkNameMatch=false, chunkContentMatch=true` (eager-corpus mode for Phase 2; Phase 4+ moves consumers to lazy-only) |
| Asset provenance | `node scripts/validate-assets.mjs` | Exit 0; `[provenance] all 2 assets carry valid provenance.` |
| Compiled Ink | `ls src/content/compiled-ink/season1/` | 5 .ink.json files (4 Lura beats + 1 letter; matches /content/dialogue/season1/) |
| Playwright e2e | `npx playwright test tests/e2e/season1-loop.spec.ts` | Exit 0; **1 passed** in 4.0s (test runtime 1.6s) |
All automated gates green.
---
## Goal Achievement
### Observable Truths (mapped to ROADMAP Success Criteria)
| # | Truth | Status | Evidence |
|---|-------|--------|----------|
| SC1 | Begin gate honored from frame one — single hand-painted "Tend the garden / Begin", AudioContext.resume on user gesture, no UI clutter on initial load | VERIFIED (structural) | `src/ui/begin/BeginScreen.tsx:28` calls `bootstrapAudioContext()` SYNCHRONOUSLY inside the click handler (Pitfall 5 — iOS Safari construction-inside-gesture); `src/ui/begin/use-audio-bootstrap.ts:24-45` creates the AudioContext lazily and calls `_ctx.resume()`. UI is a single fixed-position overlay with `zIndex: 100` covering the canvas, only title + subtitle + Begin CTA from `uiStrings[1].begin`. D-22 returning-player path: `src/PhaserGame.tsx:159` calls `appStore.getState().dismissBeginGate()` when a save record exists. Tone-quality of the typographic placeholder needs a human read. |
| SC2 | Plant → grow → harvest → fragment → journal flow with selectable copy-pasteable text and stable string fragment IDs; fragment authored in /content/ Markdown+frontmatter; growth state persists across browser refresh | VERIFIED | Plant: `src/sim/garden/commands.ts` `plantSeed()` is pure with D-05 unlock-gate + occupied-tile silent no-op. Grow: `src/sim/garden/growth.ts` `advanceGrowth()` state machine sprout→mature@33%→ready@100% per `GROWTH_THRESHOLDS`. Harvest: `commands.ts` `harvest()` calls `selectFragment()`, empties tile, appends to `harvestedFragmentIds`, runs Pitfall 10 unlock recompute. Fragment: `src/sim/memory/selector.ts` deterministic mulberry32 PRNG seeded from sim state. Journal: `src/ui/journal/Journal.tsx` full-screen modal renders fragment bodies inside `<pre>` with `userSelect: 'text'` (DOM, not canvas — MEMR-05 mechanically defended). Stable IDs: 17 yaml fragments + 2 markdown fragments under `content/seasons/01-soil/`, all matching `/^season1\.[a-z0-9._-]+$/`. Refresh persistence: PIPE-07 e2e at `tests/e2e/season1-loop.spec.ts:188-218` reloads page after harvest and asserts fragment still in store + still in journal. **PASSED in 1.6s.** |
| SC3 | Compost an immature plant yields tonal beat acknowledgement; deterministic fragment selector never duplicates within playthrough until pool exhausted; respects Season/story-state gating; Lura appears at gate with text-message-cadence Ink dialogue compiled to JSON | VERIFIED (structural; Lura tone needs human read) | Compost: `commands.ts` `compost()` empties tile, no fragment yield (D-07), no resource refund (D-04 infinite seeds); `src/ui/settings/compost-toast.tsx` cycles through `uiStrings[1].post_harvest_beat` (3 quiet authored lines) on each compost dispatch via `bumpCompostBeat`. Selector no-dup: `src/sim/memory/pool.ts` `filterPool()` excludes already-harvested ids; `selector.ts` 16 tests cover determinism + gating + sentinel exhaustion fallback (`season1.soil._exhaustion`, tagged `_meta`, excluded from normal pool). Lura: `src/sim/narrative/lura-gate.ts` gates on `state.harvestedFragmentIds.length` reaching 1/4/8 thresholds (D-14); `src/ui/dialogue/LuraDialogue.tsx` renders inkjs Story via `InkRenderer` with text-message cadence (1500ms base + 20ms/char, capped 4000ms). 4 Ink files compile to 4 JSON files via `scripts/compile-ink.mjs` (BLOCKER 4 — uses `node_modules/inklecate/bin/inklecate{.exe}` directly, not stale per-platform path strings). |
| SC4 | Tab close + return ≤24h: garden progresses by elapsed real time (not setInterval), refuses negative deltas, caps offline catchup at 24h; return screen is the *letter from the garden* (not a stat dump); saves fire on visibilitychange + beforeunload + Season transitions | VERIFIED (structural; letter tone + ≥5min flow need human read) | Elapsed real time: `src/sim/scheduler/tick.ts` `drainTicks()` is a pure fixed-timestep accumulator; the boot path in `src/PhaserGame.tsx:163-211` calls `computeOfflineCatchup(payload.lastTickAt, nowMs)` then drains via silent simulate. Negative refusal: `tick.ts:53-55` returns the original state with `ticksApplied=0` if `accumulatorMs < 0`. 24h cap: `tick.ts:32` `MAX_OFFLINE_MS = 24 * 3600 * 1000`; `tick.ts:56` clamps via `Math.min(accumulatorMs, MAX_OFFLINE_MS)`; `catchup.ts:36` clamps `cappedMs = raw < 0 ? 0 : Math.min(raw, MAX_OFFLINE_MS)`. Letter: `src/ui/letter/Letter.tsx` loads `letter-from-the-garden.ink`, binds plants_bloomed / fragment_titles / lura_was_here slots, opens at ≥5min absence (D-20: `ABSENCE_LETTER_THRESHOLD_MS = 5 * 60 * 1000` at `PhaserGame.tsx:69`); content is anti-FOMO compliant (no numeric "28h" copy in any branch — verified in `letter-from-the-garden.ink:7-15`). Save lifecycle: `src/save/lifecycle.ts:29-42` registers visibilitychange→hidden + beforeunload synchronous handlers; `saveOnSeasonTransition()` callable for Phase 4+. PhaserGame.tsx wires saveSync via clock.now() (BLOCKER 3 wall-clock anchor) + synchronous LocalStorage write (Pitfall 7) + best-effort IDB. |
| SC5 | Playwright e2e smoke passes: load → dismiss begin → plant → fast-forward growth → harvest → verify journal → refresh page → verify persistence; story progression gates on tick count NOT wall time (system-clock cheat resistance) | VERIFIED | `tests/e2e/season1-loop.spec.ts` exercises all 16 steps under URL flag `?devtime=fake` (production-guarded by `import.meta.env.PROD`). Test runs in 1.6s end-to-end (4.0s including dev-server cold start). Fast-forward via `__tlgFakeClock.advance(ms)`. STRY-10: `src/sim/narrative/lura-gate.ts:47-50` `advanceLuraBeatProgress(progress, harvestCount)` takes ONLY harvest count — no clock parameter. STRY-10 test in `lura-gate.test.ts` advances FakeClock 24h with 0 harvests and confirms `progress.pending === null`. ESLint sim-purity rule (`eslint.config.js` Block 3) bans Date.now/setInterval inside `src/sim/**` with `clock.ts` as the single exception; lint exits 0. |
**Score: 5/5 ROADMAP success criteria structurally satisfied.** Subjective tone-quality items routed to human verification (see frontmatter `human_verification`).
---
## REQ-ID Coverage (24/24)
| REQ-ID | Owner Plan(s) | Status | Evidence |
|--------|---------------|--------|----------|
| CORE-02 | 02-01 + 02-02 | PASS | `drainTicks` fixed-timestep accumulator at `src/sim/scheduler/tick.ts`; TICK_MS=200 (5Hz); 7 scheduler tests green; Garden.ts update() loop drives it via injected clock. |
| CORE-03 | 02-01 + 02-05 | PASS | MAX_OFFLINE_MS=24h clamp at `tick.ts:32`; `computeOfflineCatchup` reports `hitOfflineCap=true` on excess; PhaserGame.tsx boot path threads catchup → silent drainTicks → letter overlay open at ≥5min. 5 catchup tests green. |
| CORE-11 | 02-01 | PASS | `drainTicks` returns original state with `ticksApplied=0` on negative `accumulatorMs` (tick.ts:53-55); ESLint sim-purity rule enforces no Date.now inside `src/sim/**` outside `clock.ts`. Lint exits 0; 1 test pins the negative-refusal behavior. |
| GARD-01 | 02-02 | PASS | `plantSeed` at `commands.ts` (D-05 unlock-gate + occupied silent no-op + immutability via map-spread); SeedPicker DOM popover; Garden scene `pointerdown` enqueues. 14 commands.test.ts cases. |
| GARD-02 | 02-02 + 02-05 | PASS | `advanceGrowth` pure function with 3-stage state machine; `plant-renderer.ts` primitives per stage; Garden scene `appStore.subscribe` drives reactive `repaintPlants`. PIPE-07 e2e verifies save round-trip restores tile state. |
| GARD-03 | 02-03 | PASS | `harvest()` pure command refuses immature plants, calls `selectFragment()`, empties tile, recomputes Pitfall 10 unlocks. Garden.ts `handleTilePointerDown` enqueues `'harvest'` on a ready-stage click. |
| GARD-04 | 02-03 + 02-04 + 02-05 | PASS | `compost()` pure command empties tile, no yield (D-07), no refund (D-04). Garden.ts compost branch enqueues + bumps `compostBeatTick`; CompostToast cycles `uiStrings[1].post_harvest_beat`. The Ink-authored richer voice in `compost-acknowledgements.ink` is compiled + runtime-loadable for Phase 4+ to swap in. |
| MEMR-01 | 02-03 | PASS | `harvest()` calls `selectFragment()` exactly once per ready-stage harvest; result appended to `harvestedFragmentIds`. Pinned by 16 selector tests + commands harvest tests. |
| MEMR-02 | 02-03 | PASS | 17 fragments under `/content/seasons/01-soil/` (16 named yaml + 1 sentinel) plus 2 long-form Markdown (lura-first-letter.md, winter-rose-night.md); PIPE-01 enforced (build fails on schema violation). **Note (info-level):** the 02-03 SUMMARY claims "14 yaml entries (9 warm + 3 contemplative + 2 heavy + 1 _meta)" — the actual count is 17 yaml (9 warm + 4 contemplative + 3 heavy + 1 _meta). Substantive constraint "warm pool depth ≥9" holds; documentation undercounts but does not affect goal achievement. |
| MEMR-03 | 02-03 | PASS | All 17 yaml + 2 markdown fragment ids match `/^season1\.[a-z0-9._-]+$/`; FragmentSchema regex enforces stable string IDs; `loader.test.ts` has the numeric-id rejection case. |
| MEMR-04 | 02-03 | PASS | `Journal.tsx` full-screen modal grouped by Season; `JournalIcon` corner affordance gated by `selectJournalRevealed` (D-23 first-harvest reveal). 7 Journal.test.tsx + 3 journal-icon tests. |
| MEMR-05 | 02-03 | PASS | `Journal.tsx` + `FragmentRevealModal.tsx` both render fragment bodies inside `<pre>` with `userSelect: 'text'` (DOM, not canvas). Pinned by computed-style assertions. |
| MEMR-06 | 02-03 | PASS | `selector.ts` mulberry32 PRNG seeded from sim state (no Date.now); gating by Season + plant-type tonal-register tag; no-dup; sentinel fallback `season1.soil._exhaustion` for Pitfall 8. 16 selector tests. |
| STRY-01 | 02-04 | PASS (structural; tone needs human read) | 3 Ink beats authored at `/content/dialogue/season1/lura-{arrival,mid,farewell}.ink`; gated at 1/4/8 harvests via `lura-gate.ts`; `LuraDialogue.tsx` renders inkjs Story; gate-renderer at `(880, 384)` with soft alpha-pulse. 17 sim tests + 13 dialogue tests. **Tone-quality (warmth-anchor / contrast / not co-griever / specific-intermittent-funny) is structurally believable from the .ink content read but needs author confirmation.** |
| STRY-06 | 02-04 + 02-05 | PASS | `scripts/compile-ink.mjs` invokes bundled inklecate binary at build time; 5 .ink → .ink.json deterministically; `src/content/ink-loader.ts` lazy-loads compiled JSON; `npm run ci` runs compile:ink before tests + before build. RESEARCH Assumption A6 verified first-try on Windows. |
| STRY-07 | 02-04 | PASS (vacuous) | Phase 2 ships zero Keeper-spoken lines. The Keeper is the player; only Lura speaks (and the gardener-keeper voice acknowledges in compost beats, but is never personified as a named character). Phase 7 lands the binary choice surface. |
| STRY-10 | 02-04 | PASS | `lura-gate.ts:47-50` `advanceLuraBeatProgress(progress, harvestCount)` takes ONLY the harvest count — no clock parameter exists. STRY-10 test case advances FakeClock by 24 hours with zero harvests and confirms no beat fires. ESLint sim-purity rule mechanically prevents Date.now inside `src/sim/narrative/`. |
| AEST-07 | 02-02 | PASS | `BeginScreen.tsx:28` calls `bootstrapAudioContext()` synchronously inside the click handler; `use-audio-bootstrap.ts` constructs AudioContext + calls `resume()` (Pitfall 5 — iOS Safari construction-inside-gesture defended). 4 BeginScreen tests + first-interaction one-shot for D-22 returning players. |
| UX-01 | 02-02 + 02-03 | PASS | BeginScreen mounts as a single fixed-position dialog covering the canvas with only title + subtitle + Begin CTA; no HUD, no journal pre-first-harvest (D-23), no settings clutter. |
| UX-02 | 02-05 | PASS (structural; letter tone needs human read) | `letter-from-the-garden.ink` authored skeleton with VAR plants_bloomed / fragment_titles / lura_was_here per D-17/D-18; bible voice; anti-FOMO compliant (24h cap silent in voice per D-11 — verified zero numeric "28h" copy in any branch); `Letter.tsx` full-screen overlay (D-20 ≥5min trigger, single-tap dismiss with Pitfall 9 audio bootstrap); `buildLetterSlots` pure helper + 10 tests; Letter overlay 7 tests. Boot path threads silent catchup → offlineEvents → openLetter. |
| UX-10 | 02-01 + 02-05 | PASS | `registerSaveLifecycleHooks` synchronous handlers for visibilitychange→hidden + beforeunload (lifecycle.ts:29-42); `saveOnSeasonTransition()` callable. 6 lifecycle tests green. PhaserGame.tsx boot path wires saveSync via `clock.now()` (BLOCKER 3 wall-clock anchor) + synchronous LocalStorage write (Pitfall 7) + best-effort IDB write. W5 — lifecycle handle held in ref so the outer cleanup detaches across the async IIFE boundary. |
| UX-11 | 02-01 | PASS | `formatHumanReadable` handles K/M/B/T thresholds + 1e15 scientific + negative-sign branch; 11 format tests green. `BigQty.format()` delegates so all currency-grade numbers in the HUD route through this. |
| PIPE-02 | 02-02 + 02-03 | PASS (structural) | `loadSeasonFragments(seasonId)` lazy `import.meta.glob` surface in `src/content/loader.ts`; `scripts/check-bundle-split.mjs` exits 0 after build (chunkContentMatch=true). **Caveat (info-level):** the build emits 3 INEFFECTIVE_DYNAMIC_IMPORT warnings for `fragments.yaml`, `lura-first-letter.md`, `winter-rose-night.md` because Phase 2 keeps the eager `fragments` export alongside the lazy `loadSeasonFragments` for back-compat with Phase-1 loader tests. Phase 4+ will switch consumers to lazy-only when Season 2 onboards; the warnings will resolve naturally then. The current `chunkContentMatch=true` heuristic is structurally OK but `chunkNameMatch=false` is the expected eager-mode state, not a regression. **Bundle stays at 1.9MB; gate doesn't fire on size as that lands in Phase 8.** |
| PIPE-07 | 02-05 | PASS | `tests/e2e/season1-loop.spec.ts` covers load → Begin → plant rosemary → fast-forward FakeClock 3min → harvest → fragment-reveal modal → close → journal-icon visible → open journal → fragment present → reload → fragment persists. 1.6s test runtime. URL-flag FakeClock injection production-guarded by `import.meta.env.PROD`. |
**Coverage:** 24/24 Phase-2 REQ-IDs structurally PASS. Zero orphaned requirements; the requirement-ID set in REQUIREMENTS.md table-of-contents row exactly matches the union of `requirements-completed:` arrays across the 5 plans' frontmatter.
---
## Banner Concern Carry-Forward Checks
| # | Banner Concern | Status | Evidence |
|---|----------------|--------|----------|
| 4 | System-clock cheating | DEFENDED | `tick.ts:53-55` refuses negative `accumulatorMs`; `catchup.ts:36` clamps `cappedMs` at 0 for negative deltas; `lura-gate.ts:47-50` gates on harvest count never wall time; `eslint.config.js` Block 3 mechanically prevents Date.now inside `src/sim/**` (only `clock.ts` and the deliberate `__test_violation__` fixture violate); STRY-10 test pins behavior. |
| 7 | Web Audio user-gesture | DEFENDED | `BeginScreen.tsx:28` calls `bootstrapAudioContext()` synchronously inside the click stack frame (Pitfall 5 — iOS Safari construction-inside-gesture); `use-audio-bootstrap.ts` constructs AudioContext lazily inside the gesture (no useEffect indirection); `installFirstInteractionGestureHandler` covers returning-player path; `Letter.tsx:90` calls `bootstrapAudioContext()` on dismiss for the returning-player-via-letter path (Pitfall 9). |
| 6 | Anti-FOMO | DEFENDED | `letter-from-the-garden.ink` is contemplative, slot-based, no numeric "28h" copy, no nag, no streak, no daily-login pressure (verified by reading the .ink file); `uiStrings[1].settings.persistence_denied_toast` is "The garden may forget, if your browser asks it to." (in voice, not a stat); CompostToast lines are quiet acknowledgements ("The earth remembers.", "Something stayed.", "It rests where it grew."); `.planning/anti-fomo-doctrine.md` exists from Phase 1 and is review-enforced. |
| 10 | Authored content / code divergence | DEFENDED | All player-visible strings live in `/content/seasons/01-soil/ui-strings.yaml` + 17 yaml fragments + 2 markdown fragments + 5 .ink files. Stable-string fragment IDs (`/^season1\.[a-z0-9._-]+$/` regex enforced by FragmentSchema). Spot-check of `BeginScreen.tsx`, `SeedPicker.tsx`, `Letter.tsx`, `Settings.tsx`, `LuraDialogue.tsx` shows zero hardcoded English strings outside CSS values, ARIA roles, command kinds, and event names. |
| 1 | Story ends but the loop doesn't | NOT EXERCISED IN PHASE 2 | Phase 1 landed `season-7-end-state.md` doctrine doc; Roothold ceiling lands in Phase 4; credits/coda rest state lands in Phase 7. Phase 2 introduces nothing that forecloses the Season 7 end-state design. |
| 2 | 7-Season scope | DEFENDED VIA STANDALONE-PROLOGUE ESCAPE HATCH | The Phase 2 vertical slice now satisfies the "could plausibly ship as a free standalone Season 1 prologue" contract from ROADMAP overview. Plan 02-05's e2e proves the loop end-to-end on real authored content with real save round-trip. |
| 5 | AI asset style drift | NOT EXERCISED IN PHASE 2 | Phase 2 ships zero PNG assets — plant rendering uses Phaser primitive shapes (D-26). The provenance gate from Phase 1 is in place (validate-assets.mjs exits 0 with 2 placeholder assets); Phase 5+ first exercises it at production volume. |
| 8 | Tab throttling | DEFENDED | Sim advances by elapsed-time accumulator, never `setInterval` (banned by ESLint sim-purity rule). Save fires on `visibilitychange` to hidden + `beforeunload` (lifecycle.ts:29-42) + `saveOnSeasonTransition` callable. |
| 9 | Tonal failure | NEEDS HUMAN VERIFICATION | Lura's three Ink beats and the letter-from-the-garden Ink are structurally in voice based on a code-side read, but ROADMAP's "external readers gate every Season's tone" is the user's review responsibility. Plan 02-04 SUMMARY explicitly defers this to "next merge"; this verification surfaces it as a human_needed item. |
| 3 | Browser save fragility | DEFENDED | IDB primary path + LocalStorage synchronous fallback (Pitfall 7); `navigator.storage.persist()` always called from the boot path (D-30 toast on denied); CRC-32 checksum + canonical JSON; Base64 export/import in Settings; last-3 snapshot retention from Phase 1. |
---
## Anti-Pattern Scan (Phase 2 Files)
| File | Pattern | Severity | Notes |
|------|---------|----------|-------|
| `src/PhaserGame.tsx:152, 220, 250` | `console.error` / `console.warn` for boot-path failures | INFO | Defensive logging only; the actual UX is "fall through to first-run init" or "show toast." Not a stub. |
| `src/ui/letter/Letter.tsx:74` | `console.error('[Letter] failed to load', err)` | INFO | Fail-soft on Ink load failure with explicit `dismissLetter()` recovery. |
| `src/ui/letter/Letter.tsx:131` | Loading state renders `<p style={{ opacity: 0.4 }}>...</p>` | INFO | Genuine loading-state placeholder while `loadInkStory` resolves; replaced by InkRenderer once runtime is ready. Not a permanent stub. |
| `src/sim/garden/auto-harvest.ts` (cyclic import with commands.ts) | Benign ESM cycle | INFO | Documented at `auto-harvest.ts:32-37` and `commands.ts`; verified by all 312 tests passing. |
| Build: `INEFFECTIVE_DYNAMIC_IMPORT` warnings | 3 warnings on `fragments.yaml`, `lura-first-letter.md`, `winter-rose-night.md` | INFO | Inherited from Plan 02-02's eager-corpus + lazy-glob co-existence; documented as a Phase-4+ resolution path when consumers move to lazy-only. PIPE-02 structural verifier confirms `chunkContentMatch=true` so the lazy plumbing is genuinely there. |
| Bundle size 1.9MB > 500kB Vite warning | INFO | Acknowledged in 02-05 SUMMARY; tracked for Phase 3 (watercolor) or later when code-splitting becomes meaningful. |
| `gray-matter` package.json entry no longer used by code | INFO | Tracked in `.planning/phases/02-season-1-vertical-slice-soil/deferred-items.md`; cleanup-only, not blocking. |
| `src/sim/__test_violation__/date-now-violator.ts:13` | Deliberate `Date.now()` violation | EXPECTED FIXTURE | Excluded from `npm run lint` via Block 1's top-level ignores; the programmatic ESLint test in `lint-firewall.test.ts` overrides via `ignore: false` to verify the Block 3 sim-purity rule fires. |
**No blockers found. No warnings rise to gap-level. All info-level items are either documented deferrals or expected-by-design.**
---
## Behavioral Spot-Checks
| Behavior | Command | Result | Status |
|----------|---------|--------|--------|
| Vitest suite | `npm test` | 39 files, 312/312 passed (5.54s) | PASS |
| Lint | `npm run lint` | Exit 0; 0 errors, 0 warnings (2 informational stderr deprecation notices about boundaries v5→v6 plugin rename — non-blocking) | PASS |
| Build | `npm run build` | Exit 0; 1.9MB entry chunk; 5 lazy Ink chunks | PASS |
| Bundle split | `npm run check:bundle-split` | Exit 0; PIPE-02 OK | PASS |
| Asset provenance | `node scripts/validate-assets.mjs` | Exit 0; 2 valid assets | PASS |
| Compiled Ink output | `ls src/content/compiled-ink/season1/` | 5 .ink.json files | PASS |
| Playwright e2e | `npx playwright test tests/e2e/season1-loop.spec.ts` | 1 passed in 4.0s; test runtime 1.6s | PASS |
| Sim purity (no Date.now outside clock.ts) | `grep -rn "Date.now" src/sim/ --include="*.ts"` | Only matches: `clock.ts` (1 actual call, the documented exception) + deliberate `__test_violation__/date-now-violator.ts` fixture + doc-comment mentions in growth.ts/types.ts/etc. | PASS |
| Sim purity (no render/ui imports) | `grep -rn "from.*src/render\|from.*src/ui" src/sim/` | Only matches: deliberate `__test_violation__/violator.ts` fixture + a doc-comment | PASS |
All 9 spot-checks PASS.
---
## Human Verification Required (6 items)
See frontmatter `human_verification` for full structure. Headlines:
1. **Read the three Lura .ink files in voice** — confirm warmth-anchor / contrast / not co-griever; specific + intermittent + sometimes funny; "The garden persists." carries the farewell turn.
2. **Read letter-from-the-garden.ink in voice** — confirm contemplative, anti-FOMO compliant, not a stat dump.
3. **Run `npm run dev` and exercise the loop manually** — Begin → plant → grow → harvest → reveal → journal → reload → persist. Compose ~9 harvests to fire all 3 Lura beats and confirm cadence + visual indicator.
4. **Verify the Begin screen feels A-Dark-Room-clean** — single typographic placeholder, no clutter, returning-player path skips it.
5. **Verify offline catchup → letter overlay flow on a real ≥5min absence** — letter Ink composes correctly from offlineEvents block; Pitfall 9 audio bootstrap fires on dismiss.
6. **Confirm the gate visual indicator + LuraDialogue overlay flow** — soft alpha-pulse on pending beat, click → DOM dialogue overlay → drip cadence → close → resolved.
These are the items that the SUMMARY documents call out as "user reviews at next merge" or "Manual smoke test: not performed in this execution session." All are inherently subjective (tonal voice, visual cadence, A-Dark-Room-feel) and cannot be programmatically scored.
---
## Notes on Documentation Inaccuracies (Info-Level)
These are SUMMARY documentation errors that do NOT affect goal achievement:
1. **Plan 02-03 SUMMARY claims "14 yaml entries (9 warm + 3 contemplative + 2 heavy + 1 _meta)".** Actual count is 17 yaml entries (9 warm + 4 contemplative + 3 heavy + 1 _meta). The substantive constraint "warm pool depth ≥9" holds; the documentation undercounts other registers. Fix: tighten the SUMMARY count if/when next visited; not blocking.
2. **Plan 02-04 SUMMARY claims compile:ink emits "4 deterministic .ink.json files".** Actual count after Plan 02-05 lands is 5 files (the +1 is letter-from-the-garden.ink, added by Plan 02-05). Plan 02-05 SUMMARY corrects this to 5. The 02-04 SUMMARY is stale relative to the post-Plan-02-05 codebase. Not blocking; Plan 02-04 was correct at write-time.
3. **3 INEFFECTIVE_DYNAMIC_IMPORT build warnings** in `src/content/loader.ts` (fragments.yaml, lura-first-letter.md, winter-rose-night.md). These warnings indicate the dynamic-import path doesn't actually create a separate chunk because the same files are also statically imported. **PIPE-02 satisfaction**: `check-bundle-split.mjs` reports `chunkNameMatch=false, chunkContentMatch=true`, confirming the lazy plumbing is structurally there but eager-mode is the active code path. Phase 4+ will switch consumers to lazy-only when Season 2 onboarding lands; the warnings will resolve naturally then. The PIPE-02 verifier is structurally lenient on Day 1 (OR-of-three checks) by design — documented in Plan 02-03 SUMMARY. **The lazy-load contract is genuinely partial today; not "broken" but "not yet exercised." Verifier flags it as info-level for awareness but it does NOT block phase sign-off.**
---
## Phase 1 Regression Check
Phase 1 was verified at 16/16 PASS on 2026-05-09T00:15:00Z. Re-verifying invariants Phase 2 might have disturbed:
- ESLint boundary rule (CORE-10) — `npm run lint` exits 0; programmatic test `src/sim/__test_violation__/lint-firewall.test.ts` still green.
- Save layer (CORE-04 through CORE-09) — 9 envelope tests + 6 migration tests + 6 lifecycle tests all green; round-trip via Settings UI tested in Settings.test.tsx.
- Content pipeline (PIPE-01) — fragments.yaml (17 entries) + 2 .md files + ui-strings.yaml all parse via Vite's import.meta.glob; build fails on schema violation.
- Asset provenance (PIPE-03) — `validate-assets.mjs` still exits 0 with 2 valid assets.
- Doctrine docs (PIPE-05) — `.planning/anti-fomo-doctrine.md` + `.planning/season-7-end-state.md` still present; doc-lint test still green (in vitest run).
- CI workflow (PIPE-06) — `npm run ci` exits 0 end-to-end (lint + compile:ink + 312 tests + validate:assets + build + check:bundle-split).
**No Phase 1 regressions detected.**
---
## Verdict
**Phase 2 is STRUCTURALLY COMPLETE.** All 24 Phase-2 REQ-IDs PASS, all 5 ROADMAP success criteria are structurally satisfied, all banner-concern carry-forwards are defended in code, and every automated gate exits 0.
**6 human-verification items remain** — all subjective tone-quality and live-loop-feel checks that the SUMMARY documents already flagged as "user reviews at next merge" / "Manual smoke test: not performed in this execution session." These are not gaps or blockers; they are the canonical handoff points where the executor's structural verification ends and the developer's tone judgment begins.
The Phase-2 vertical slice could plausibly ship as a free standalone Season-1 prologue once items 13 in `human_verification` clear: a player can launch, plant, grow, harvest, meet Lura, leave, return to a letter, dismiss, and the save round-trip survives all of it. That's the project's escape hatch against the 7-Season scope risk (banner concern #2) realized.
---
_Verified: 2026-05-09T11:24:00Z_
_Verifier: Claude (gsd-verifier)_