test(02): persist human verification items as UAT (6 tone/live-loop items)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,236 @@
|
||||
---
|
||||
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 1–3 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)_
|
||||
Reference in New Issue
Block a user