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

32 KiB
Raw Blame History

phase, verified, verifier_run_at, status, score, overrides_applied, re_verification, per_req, human_verification
phase verified verifier_run_at status score overrides_applied re_verification per_req human_verification
02-season-1-vertical-slice-soil 2026-05-09T11:24:00Z 2026-05-09T11:24:00Z human_needed 24/24 must-haves structurally verified 0 false
CORE-02 CORE-03 CORE-11 GARD-01 GARD-02 GARD-03 GARD-04 MEMR-01 MEMR-02 MEMR-03 MEMR-04 MEMR-05 MEMR-06 STRY-01 STRY-06 STRY-07 STRY-10 AEST-07 UX-01 UX-02 UX-10 UX-11 PIPE-02 PIPE-07
PASS PASS PASS PASS PASS PASS PASS PASS PASS PASS PASS PASS PASS PASS (structural; tone needs human read) PASS PASS (vacuous — Phase 2 ships zero Keeper-spoken lines) PASS PASS PASS PASS (structural; letter tone needs human read) PASS PASS PASS (structural; chunkContentMatch=true; chunkNameMatch deferred to Phase 4+ when consumers move to lazy-only) PASS
test expected why_human
Read the three Lura .ink files in voice Lura reads as warmth-anchor / contrast / not co-griever; specific + intermittent + sometimes funny; the farewell carries 'The garden persists.' as the load-bearing turn 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 expected why_human
Read the letter-from-the-garden.ink in voice 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 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 expected why_human
Run npm run dev and exercise the loop manually 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. 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 expected why_human
Verify the Begin screen feels A-Dark-Room-clean 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. AEST-07 + UX-01 are about visual restraint; the typographic placeholder gets a human pass on whether it lands tonally.
test expected why_human
Verify offline catchup → letter overlay flow on a returning save 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). 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 expected why_human
Confirm the gate visual indicator + LuraDialogue overlay flow 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. 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)