docs(02): close phase 2 — gap closure verified, 24/24 + 4/4 PASS
ci / lint + test + validate-assets + build (push) Failing after 4m54s

Plan 02-06 (UAT gap closure) executed cleanly via /gsd-execute-phase 2:
6 atomic commits (5 task + 1 SUMMARY), 333/333 vitest green
(was 312, +21 cases), npm run ci exit 0, Playwright e2e exit 0.

Hint copy chosen: "Begin where the soil is bare." (plan's #1 ranked
candidate, bible voice).

gsd-verifier re-verification confirms:
- 24/24 Phase-2 REQ-IDs structurally PASS (no regressions)
- 4/4 UAT gaps closed (G1 white halo, G2 first-run prompt,
  G3 tile contrast, G4 gate wall context)
- All scope constraints honored: zero painted assets, zero new npm
  deps, V1Payload unchanged, sim purity preserved
- Banner concerns #5/#7/#6/#9/#10 still defended

VERIFICATION.md frontmatter status flipped gaps_found → verified.
ROADMAP Phase 2 marked complete (6/6 plans, completed 2026-05-09).
STATE.md updated with phase-2 completion narrative.

7 HUMAN-UAT.md tone items remain pending (Lura voice, letter cadence,
Begin tonal feel, ≥5min absence flow, gate visual indicator overlay,
plus the new chosen first_run_hint copy review).

Phase 2 vertical slice now plausibly ships as a free standalone
Season-1 prologue — banner concern #2 (7-Season scope risk) escape
hatch realized.

Next: /gsd-discuss-phase 3 (Watercolor & Cello Aesthetic — GARD-10,
AEST-01..06, UX-05).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-09 12:37:17 -04:00
parent 7f39cf6d31
commit 8e4609ae20
3 changed files with 390 additions and 221 deletions
+159 -159
View File
@@ -1,162 +1,162 @@
# Roadmap: The Last Garden
## Overview
The Last Garden is a 7-Season browser narrative idle game that ships its entire authored arc at v1. The roadmap takes a vertical-slice approach inside an MVP shell: Phase 1 lays the retrofit-hostile foundations (versioned saves, content pipeline, AI provenance discipline, anti-FOMO doctrine, Season 7 end-state design) so that no later phase has to rework architecture. Phase 2 is the load-bearing vertical slice — Season 1 ("Soil") shipped end-to-end with Lura, real fragments, working offline progression, and a "begin gesture" cello bootstrap — which is structured so it could *plausibly* ship publicly as a free standalone prologue ahead of Seasons 2-7. Phase 3 layers the watercolor + cello aesthetic onto the working loop. Phases 4-7 deliver the remaining six Seasons as paired vertical slices, each introducing at most one new mechanic and completing one arc beat (Roots/cross-pollination → Canopy + Storm → Depth + Loom → Return + binary choice). Phase 8 is launch-readiness polish — UX affordances, accessibility, performance, and visual regression for the asset library — gated behind the full content arc landing intact. The structure carries three banner concerns forward at every phase: the 7-Season scope risk (defended by the standalone-Season-1 escape hatch and the one-mechanic-per-Season cap), the "story ends but the loop doesn't" pitfall (Season 7 end-state designed in Phase 1, finite Roothold ceiling enforced in Phase 4, credits/coda rest-state landed in Phase 7), and AI asset pipeline discipline (provenance + curation gate + locked north-star reference set landed in Phase 1 before any production-volume asset generation in Phase 5+).
## Phases
**Phase Numbering:**
- Integer phases (1, 2, 3): Planned milestone work
- Decimal phases (2.1, 2.2): Urgent insertions (marked with INSERTED)
Decimal phases appear between their surrounding integers in numeric order.
- [ ] **Phase 1: Foundations & Doctrine** - Versioned saves, content/asset pipelines, anti-FOMO + Season 7 end-state docs, sim/render firewall — all retrofit-hostile decisions landed before any feature code
- [ ] **Phase 2: Season 1 Vertical Slice (Soil)** - Player can plant, wait, harvest fragments, meet Lura, and return to a "letter from the garden" — the loop and content pipeline proven end-to-end on real Season 1 content
- [ ] **Phase 3: Watercolor & Cello Aesthetic** - The working garden becomes the painted garden: watercolor post-process, hand-painted plants, solo cello + ambient layers, the Pale rendered as overexposed silence
- [ ] **Phase 4: Season-Prestige Cycle & Season 2 (Roots)** - Players experience their first die-off, Roothold persists across the reset with a finite ceiling, cross-pollination unlocks, and the Nameless Man arrives at the gate
- [ ] **Phase 5: Seasons 3-4 (Canopy & Storm)** - Canopy trees deliver place-memory vignettes, Memory Storms test the player's resilience, the Nameless Man's dialogue unravels and he vanishes mid-sentence
- [ ] **Phase 6: Seasons 5-6 (Depth & Loom)** - The Below opens beneath the garden, ecosystem planting introduces clustered yields, the Archivist appears in Season 6 and accepts a memory containing both joy and grief
- [ ] **Phase 7: Season 7 (Return) & Final Choice** - The long, redemptive late-game where memories reseed the world, the player makes the binary narrative choice, and "The garden persists." rests as a coda the player can return to indefinitely
- [ ] **Phase 8: UX, Accessibility & Launch Polish** - Multi-buy, audio sliders, keyboard navigation, color-redundant icons, tab-title bloom indicator, visual regression for the asset library, the Lura-not-numbers UX doctrine enforced
## Phase Details
### Phase 1: Foundations & Doctrine
**Goal**: Developer can ship Phase 2 without architectural rework — versioned saves, content/asset pipelines, sim/render firewall, anti-FOMO doctrine, and Season 7 end-state design are all in place before any user-facing feature code is written.
**Mode:** mvp
**Depends on**: Nothing (first phase)
**Requirements**: CORE-01, CORE-04, CORE-05, CORE-06, CORE-07, CORE-08, CORE-09, CORE-10, PIPE-01, PIPE-03, PIPE-05, PIPE-06, AEST-08, AEST-09, STRY-09, UX-13
**Success Criteria** (what must be TRUE):
1. Game scaffold loads in under 5 seconds in Chrome, Firefox, Safari, and Edge on a 25 Mbps connection (CORE-01) — proving the Phaser 4 + React 19 + Vite + TypeScript stack and bundle budget are correctly set up.
2. A round-trip save test passes: a synthetic save written via IndexedDB (with localStorage fallback and `navigator.storage.persist()`) can be exported as Base64, re-imported into a fresh browser profile, migrated through the registered `migrate_vN_to_vN+1` chain, and verified by checksum — Vitest covers every shipped migration and the last 3 pre-migration snapshots are retained.
3. CI fails the build if `src/sim/` imports anything from `src/render/` or `src/ui/` (ESLint boundary rule) and fails the build if any `/content/**/*.{md,yaml,ink}` file violates its Zod schema or any AI-generated asset is missing required provenance fields (`{model_id, checkpoint_hash, prompt, seed, sampler, params}`).
4. The repository contains a written `anti-FOMO doctrine` document and a written `Season 7 end-state` design document in `.planning/`, both reviewed and committed — the project has a canonical answer to "the story ends but the loop doesn't" before any economy code is written.
5. A locked 10-20 painting "north star" reference set is committed to the repo and a documented human curation gate exists in the asset pipeline; sample assets prove the gate refuses unreviewed material.
**Plans:** 7 plans
Plans:
- [x] 01-01-scaffold-and-test-infra-PLAN.md — Bootstrap Phaser 4 official template, install Phase-1 deps, restructure src/ into 7 firewall directories, configure Vitest (happy-dom) + Playwright, pre-declare every package.json script downstream plans need ✓ 2026-05-09 (6 min) — see 01-01-scaffold-and-test-infra-SUMMARY.md
- [x] 01-02-eslint-firewall-PLAN.md — Migrate to ESLint flat config + eslint-plugin-boundaries, declare 9 element types, enforce CORE-10 (sim cannot import render or ui) with a Vitest-tested deliberate-violation fixture
- [x] 01-03-save-layer-PLAN.md — Save envelope {schemaVersion, payload, checksum} with CRC-32 over canonical JSON, idb-wrapped IndexedDB with last-3 snapshot retention, synthetic v0→v1 migration chain, navigator.storage.persist API, Base64 export/import with 50MB DoS cap, full round-trip test (CORE-04 through CORE-09)
- [x] 01-04-content-pipeline-PLAN.md — Vite-native content pipeline using import.meta.glob, Zod schemas for Fragment + SeasonContent, demo fragment under /content/seasons/00-demo/, content/README.md documenting the convention, no-op compile:ink stub for Phase 2 (PIPE-01, STRY-09)
- [x] 01-05-asset-provenance-PLAN.md — 30-line Node validator script walking /assets/ + Zod sidecar schema covering 6 required fields + optional schema_version, refused-sample fixture proves the gate, Vitest integration test, 1020 hand-curated north-star reference images committed via human curation checkpoint (AEST-08, AEST-09, PIPE-03)
- [x] 01-06-doctrine-docs-PLAN.md — Author .planning/anti-fomo-doctrine.md (consolidation per CONTEXT D-07) and .planning/season-7-end-state.md (principle-level per CONTEXT D-08), Vitest doc-lint test enforces structural integrity (PIPE-05, UX-13)
- [x] 01-07-ci-workflow-PLAN.md — Minimum-viable .github/workflows/ci.yml running npm ci + npm run ci on push to main and PR; structurally enforces every Phase 1 success criterion on every commit going forward (PIPE-06)
### Phase 2: Season 1 Vertical Slice (Soil)
**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.
**Mode:** mvp
**Depends on**: Phase 1
**Requirements**: 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
**Success Criteria** (what must be TRUE):
1. A new player sees a single hand-painted "Tend the garden / Begin" screen, presses it, the AudioContext resumes, and the painted garden reveals itself with no UI clutter — the A Dark Room rule is honored from frame one.
2. Player can plant a seed into an unoccupied tile, watch it advance through sprout → mature → ready-to-harvest growth states (advancing correctly from saved state across browser refresh), harvest it for exactly one memory fragment authored in `/content/` Markdown with frontmatter and a stable string ID, and read that fragment in full inside a React DOM Memory Journal where the text is selectable and copy-pasteable.
3. Player can compost an immature plant and receive a tonal beat acknowledging the choice to let go; the deterministic fragment selector never duplicates a fragment within a playthrough until the pool is exhausted, respects authored Season/story-state gating, and Lura appears at the garden gate with text-message-cadence dialogue authored in Ink and compiled to JSON.
4. Player who closes the tab and returns up to 24 hours later finds the garden has progressed by elapsed real time (not `setInterval` ticks), with the simulation refusing negative deltas and capping any single offline catch-up at 24 hours; the return screen is a *letter from the garden* (not a stat dump) describing what bloomed and what Lura said, and saves fire correctly on `visibilitychange` to hidden, on `beforeunload`, and on Season transitions.
5. A Playwright e2e smoke test passes: it loads the game, dismisses the begin gate, plants a seed, fast-forwards growth, harvests a fragment, verifies the fragment text appears in the journal, refreshes the page, and verifies the harvested fragment persists. Story progression gates on tick count (not wall time), so manipulating the system clock cannot fast-forward through Lura's authored beats.
**Plans:** 5 plans
Plans:
# Roadmap: The Last Garden
## Overview
The Last Garden is a 7-Season browser narrative idle game that ships its entire authored arc at v1. The roadmap takes a vertical-slice approach inside an MVP shell: Phase 1 lays the retrofit-hostile foundations (versioned saves, content pipeline, AI provenance discipline, anti-FOMO doctrine, Season 7 end-state design) so that no later phase has to rework architecture. Phase 2 is the load-bearing vertical slice — Season 1 ("Soil") shipped end-to-end with Lura, real fragments, working offline progression, and a "begin gesture" cello bootstrap — which is structured so it could *plausibly* ship publicly as a free standalone prologue ahead of Seasons 2-7. Phase 3 layers the watercolor + cello aesthetic onto the working loop. Phases 4-7 deliver the remaining six Seasons as paired vertical slices, each introducing at most one new mechanic and completing one arc beat (Roots/cross-pollination → Canopy + Storm → Depth + Loom → Return + binary choice). Phase 8 is launch-readiness polish — UX affordances, accessibility, performance, and visual regression for the asset library — gated behind the full content arc landing intact. The structure carries three banner concerns forward at every phase: the 7-Season scope risk (defended by the standalone-Season-1 escape hatch and the one-mechanic-per-Season cap), the "story ends but the loop doesn't" pitfall (Season 7 end-state designed in Phase 1, finite Roothold ceiling enforced in Phase 4, credits/coda rest-state landed in Phase 7), and AI asset pipeline discipline (provenance + curation gate + locked north-star reference set landed in Phase 1 before any production-volume asset generation in Phase 5+).
## Phases
**Phase Numbering:**
- Integer phases (1, 2, 3): Planned milestone work
- Decimal phases (2.1, 2.2): Urgent insertions (marked with INSERTED)
Decimal phases appear between their surrounding integers in numeric order.
- [ ] **Phase 1: Foundations & Doctrine** - Versioned saves, content/asset pipelines, anti-FOMO + Season 7 end-state docs, sim/render firewall — all retrofit-hostile decisions landed before any feature code
- [x] **Phase 2: Season 1 Vertical Slice (Soil)** - Player can plant, wait, harvest fragments, meet Lura, and return to a "letter from the garden" — the loop and content pipeline proven end-to-end on real Season 1 content
(completed 2026-05-09)
- [ ] **Phase 3: Watercolor & Cello Aesthetic** - The working garden becomes the painted garden: watercolor post-process, hand-painted plants, solo cello + ambient layers, the Pale rendered as overexposed silence
- [ ] **Phase 4: Season-Prestige Cycle & Season 2 (Roots)** - Players experience their first die-off, Roothold persists across the reset with a finite ceiling, cross-pollination unlocks, and the Nameless Man arrives at the gate
- [ ] **Phase 5: Seasons 3-4 (Canopy & Storm)** - Canopy trees deliver place-memory vignettes, Memory Storms test the player's resilience, the Nameless Man's dialogue unravels and he vanishes mid-sentence
- [ ] **Phase 6: Seasons 5-6 (Depth & Loom)** - The Below opens beneath the garden, ecosystem planting introduces clustered yields, the Archivist appears in Season 6 and accepts a memory containing both joy and grief
- [ ] **Phase 7: Season 7 (Return) & Final Choice** - The long, redemptive late-game where memories reseed the world, the player makes the binary narrative choice, and "The garden persists." rests as a coda the player can return to indefinitely
- [ ] **Phase 8: UX, Accessibility & Launch Polish** - Multi-buy, audio sliders, keyboard navigation, color-redundant icons, tab-title bloom indicator, visual regression for the asset library, the Lura-not-numbers UX doctrine enforced
## Phase Details
### Phase 1: Foundations & Doctrine
**Goal**: Developer can ship Phase 2 without architectural rework — versioned saves, content/asset pipelines, sim/render firewall, anti-FOMO doctrine, and Season 7 end-state design are all in place before any user-facing feature code is written.
**Mode:** mvp
**Depends on**: Nothing (first phase)
**Requirements**: CORE-01, CORE-04, CORE-05, CORE-06, CORE-07, CORE-08, CORE-09, CORE-10, PIPE-01, PIPE-03, PIPE-05, PIPE-06, AEST-08, AEST-09, STRY-09, UX-13
**Success Criteria** (what must be TRUE):
1. Game scaffold loads in under 5 seconds in Chrome, Firefox, Safari, and Edge on a 25 Mbps connection (CORE-01) — proving the Phaser 4 + React 19 + Vite + TypeScript stack and bundle budget are correctly set up.
2. A round-trip save test passes: a synthetic save written via IndexedDB (with localStorage fallback and `navigator.storage.persist()`) can be exported as Base64, re-imported into a fresh browser profile, migrated through the registered `migrate_vN_to_vN+1` chain, and verified by checksum — Vitest covers every shipped migration and the last 3 pre-migration snapshots are retained.
3. CI fails the build if `src/sim/` imports anything from `src/render/` or `src/ui/` (ESLint boundary rule) and fails the build if any `/content/**/*.{md,yaml,ink}` file violates its Zod schema or any AI-generated asset is missing required provenance fields (`{model_id, checkpoint_hash, prompt, seed, sampler, params}`).
4. The repository contains a written `anti-FOMO doctrine` document and a written `Season 7 end-state` design document in `.planning/`, both reviewed and committed — the project has a canonical answer to "the story ends but the loop doesn't" before any economy code is written.
5. A locked 10-20 painting "north star" reference set is committed to the repo and a documented human curation gate exists in the asset pipeline; sample assets prove the gate refuses unreviewed material.
**Plans:** 7 plans
Plans:
- [x] 01-01-scaffold-and-test-infra-PLAN.md — Bootstrap Phaser 4 official template, install Phase-1 deps, restructure src/ into 7 firewall directories, configure Vitest (happy-dom) + Playwright, pre-declare every package.json script downstream plans need ✓ 2026-05-09 (6 min) — see 01-01-scaffold-and-test-infra-SUMMARY.md
- [x] 01-02-eslint-firewall-PLAN.md — Migrate to ESLint flat config + eslint-plugin-boundaries, declare 9 element types, enforce CORE-10 (sim cannot import render or ui) with a Vitest-tested deliberate-violation fixture
- [x] 01-03-save-layer-PLAN.md — Save envelope {schemaVersion, payload, checksum} with CRC-32 over canonical JSON, idb-wrapped IndexedDB with last-3 snapshot retention, synthetic v0→v1 migration chain, navigator.storage.persist API, Base64 export/import with 50MB DoS cap, full round-trip test (CORE-04 through CORE-09)
- [x] 01-04-content-pipeline-PLAN.md — Vite-native content pipeline using import.meta.glob, Zod schemas for Fragment + SeasonContent, demo fragment under /content/seasons/00-demo/, content/README.md documenting the convention, no-op compile:ink stub for Phase 2 (PIPE-01, STRY-09)
- [x] 01-05-asset-provenance-PLAN.md — 30-line Node validator script walking /assets/ + Zod sidecar schema covering 6 required fields + optional schema_version, refused-sample fixture proves the gate, Vitest integration test, 1020 hand-curated north-star reference images committed via human curation checkpoint (AEST-08, AEST-09, PIPE-03)
- [x] 01-06-doctrine-docs-PLAN.md — Author .planning/anti-fomo-doctrine.md (consolidation per CONTEXT D-07) and .planning/season-7-end-state.md (principle-level per CONTEXT D-08), Vitest doc-lint test enforces structural integrity (PIPE-05, UX-13)
- [x] 01-07-ci-workflow-PLAN.md — Minimum-viable .github/workflows/ci.yml running npm ci + npm run ci on push to main and PR; structurally enforces every Phase 1 success criterion on every commit going forward (PIPE-06)
### Phase 2: Season 1 Vertical Slice (Soil)
**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.
**Mode:** mvp
**Depends on**: Phase 1
**Requirements**: 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
**Success Criteria** (what must be TRUE):
1. A new player sees a single hand-painted "Tend the garden / Begin" screen, presses it, the AudioContext resumes, and the painted garden reveals itself with no UI clutter — the A Dark Room rule is honored from frame one.
2. Player can plant a seed into an unoccupied tile, watch it advance through sprout → mature → ready-to-harvest growth states (advancing correctly from saved state across browser refresh), harvest it for exactly one memory fragment authored in `/content/` Markdown with frontmatter and a stable string ID, and read that fragment in full inside a React DOM Memory Journal where the text is selectable and copy-pasteable.
3. Player can compost an immature plant and receive a tonal beat acknowledging the choice to let go; the deterministic fragment selector never duplicates a fragment within a playthrough until the pool is exhausted, respects authored Season/story-state gating, and Lura appears at the garden gate with text-message-cadence dialogue authored in Ink and compiled to JSON.
4. Player who closes the tab and returns up to 24 hours later finds the garden has progressed by elapsed real time (not `setInterval` ticks), with the simulation refusing negative deltas and capping any single offline catch-up at 24 hours; the return screen is a *letter from the garden* (not a stat dump) describing what bloomed and what Lura said, and saves fire correctly on `visibilitychange` to hidden, on `beforeunload`, and on Season transitions.
5. A Playwright e2e smoke test passes: it loads the game, dismisses the begin gate, plants a seed, fast-forwards growth, harvests a fragment, verifies the fragment text appears in the journal, refreshes the page, and verifies the harvested fragment persists. Story progression gates on tick count (not wall time), so manipulating the system clock cannot fast-forward through Lura's authored beats.
**Plans:** 6/6 plans complete
Plans:
- [x] 02-01-foundations-PLAN.md — BigQty + Zustand 5 store + tick scheduler + V1Payload extension + save lifecycle hooks + Phaser EventBus singleton + ESLint sim-purity rule (Wave 0; foundations every other Phase-2 plan depends on) ✓ 2026-05-09 (12 min) — see 02-01-foundations-SUMMARY.md
- [x] 02-02-begin-plant-grow-PLAN.md — sim/garden core (4×4 grid, 3 plant types, growth state machine, plantSeed) + render layer (Phaser primitives, ready-pulse, tile-coords) + BeginScreen + audio bootstrap + SeedPicker + UI strings (Wave 1; AEST-07, UX-01, GARD-01, GARD-02) ✓ 2026-05-09 (18 min) — see 02-02-begin-plant-grow-SUMMARY.md
- [x] 02-03-harvest-journal-fragments-PLAN.md — Season-1 17 authored fragments + sim/memory selector (deterministic mulberry32, gated, no-dup, sentinel fallback for Pitfall 8) + harvest + compost commands (Pitfall 10 post-commit unlock thresholds) + Memory Journal + FragmentRevealModal + JournalIcon + PIPE-02 structural verifier (Wave 1; GARD-03, GARD-04, MEMR-01..06, PIPE-02) ✓ 2026-05-09 (12 min) — see 02-03-harvest-journal-fragments-SUMMARY.md
**Wave 1**
- [x] 02-01-foundations-PLAN.md — BigQty + Zustand 5 store + tick scheduler + V1Payload extension + save lifecycle hooks + Phaser EventBus singleton + ESLint sim-purity rule (Wave 0; foundations every other Phase-2 plan depends on) ✓ 2026-05-09 (12 min) — see 02-01-foundations-SUMMARY.md
- [x] 02-02-begin-plant-grow-PLAN.md — sim/garden core (4×4 grid, 3 plant types, growth state machine, plantSeed) + render layer (Phaser primitives, ready-pulse, tile-coords) + BeginScreen + audio bootstrap + SeedPicker + UI strings (Wave 1; AEST-07, UX-01, GARD-01, GARD-02) ✓ 2026-05-09 (18 min) — see 02-02-begin-plant-grow-SUMMARY.md
- [x] 02-03-harvest-journal-fragments-PLAN.md — Season-1 17 authored fragments + sim/memory selector (deterministic mulberry32, gated, no-dup, sentinel fallback for Pitfall 8) + harvest + compost commands (Pitfall 10 post-commit unlock thresholds) + Memory Journal + FragmentRevealModal + JournalIcon + PIPE-02 structural verifier (Wave 1; GARD-03, GARD-04, MEMR-01..06, PIPE-02) ✓ 2026-05-09 (12 min) — see 02-03-harvest-journal-fragments-SUMMARY.md
- [x] 02-04-lura-gate-beats-PLAN.md — inklecate compile pipeline + 4 authored .ink files (3 Lura beats + compost acknowledgements) + sim/narrative tick-count gate (1st/4th/8th harvest) + LuraDialogue overlay + InkRenderer drip + Phaser gate visual indicator (Wave 2; STRY-01, STRY-06, STRY-07 vacuous, STRY-10) ✓ 2026-05-09 (24 min) — see 02-04-lura-gate-beats-SUMMARY.md
- [x] 02-05-letter-settings-e2e-PLAN.md — sim/offline + auto-harvest + letter Ink + Letter overlay + Settings (Export/Import/Restore) + persistence-toast + boot-path save lifecycle wiring + URL-flag FakeClock injection + Playwright PIPE-07 e2e (Wave 2; UX-02, UX-10, CORE-03, CORE-11, PIPE-07) ✓ 2026-05-09 (20 min) — see 02-05-letter-settings-e2e-SUMMARY.md
**UI hint**: yes
### Phase 3: Watercolor & Cello Aesthetic
**Goal**: The working garden becomes the painted garden — every plant renders in watercolor-adjacent style on a Phaser 4 canvas with the post-process filter applied, the solo cello main theme plays through Howler.js with independent music/ambient/SFX buses that crossfade rather than hard-cut, and the player can toggle reduced motion to disable non-essential particles.
**Mode:** mvp
**Depends on**: Phase 2
**Requirements**: GARD-10, AEST-01, AEST-02, AEST-03, AEST-04, AEST-05, AEST-06, UX-05
**Success Criteria** (what must be TRUE):
1. Player sees the garden rendered in a watercolor-adjacent visual style on a Phaser 4 canvas with the watercolor post-process filter active; every plant looks like a real-world species made slightly wrong (no D&D-style fictional flora), and the Pale renders as overexposed white silence — luminous, pearlescent, *too bright* — accompanied by a faint tinnitus-like high tone.
2. Player hears a solo cello main theme on the music bus, ambient garden sounds (wind, birdsong, gate creak) on the ambient bus that thin and fade as the Unremembering's region draws closer, and per-plant SFX on the SFX bus — three independent buses, never hard-cutting, always crossfading.
3. Player progresses through a Season transition (using Phase 2's loop) and observes the visual palette and audio bed crossfade together — golden/autumnal early-game shifting toward the next Season's tonal register — with no audible click or visible flash.
4. Player who has `prefers-reduced-motion` set in their OS sees the game respect it by default; the player can also toggle a reduced-motion option in settings that disables non-essential particles and animation while preserving the painted look.
**Plans**: TBD
**UI hint**: yes
### Phase 4: Season-Prestige Cycle & Season 2 (Roots)
**Goal**: Player experiences their first die-off as the end of Season 1, watches Roothold persist across the reset toward a finite narrative-tied ceiling, unlocks cross-pollination as Season 2's single new mechanic, and meets the Nameless Man arriving at the garden gate — proving the Season state machine, save migration on real player saves, per-Season content lazy-loading, and the at-most-one-mechanic-per-Season scope-defense doctrine all work together.
**Mode:** mvp
**Depends on**: Phase 3
**Requirements**: SEAS-01, SEAS-02, SEAS-03, SEAS-04, SEAS-05, SEAS-06, GARD-05, GARD-07
**Success Criteria** (what must be TRUE):
1. Player progresses through the Season 1 → Season 2 transition: a frost die-off wipes surface plantings, the visual palette and audio bed crossfade per Phase 3, and Roothold persists with a value that demonstrably moved toward (not past) the finite ceiling tied to the Season 7 end-state design from Phase 1.
2. Player loads a save written under the Season 1 schema and watches it migrate cleanly into a Season-2-aware shape via the registered migration chain; the last 3 pre-migration snapshots are retained and the "restore previous save" option is reachable from settings.
3. Player unlocks Season 2 plant types absent from Season 1 (each with distinct growth time, harvest yield, and visual identity) and successfully cross-pollinates two adjacent compatible plants to produce a hybrid seed with mixed memory traits — the one and only new mechanic Season 2 introduces.
4. The Nameless Man appears at the gate during Season 2 with his own Ink-driven dialogue arc; only the current Season's content is in the runtime bundle (Seasons 3-7 are not in the initial chunk and load lazily on Season transition).
**Plans**: TBD
**UI hint**: yes
### Phase 5: Seasons 3-4 (Canopy & Storm)
**Goal**: Player tends the garden through the canopy era and the storm era — slow, expensive tree plantings yield short interactive place-memory vignettes; periodic Memory Storms accelerate decay of unprotected plants and demand resilient species or windbreaks; the Nameless Man's dialogue progressively shortens and confuses across Seasons 2-4 and he vanishes mid-sentence in Season 4 with no fanfare.
**Mode:** mvp
**Depends on**: Phase 4
**Requirements**: GARD-06, GARD-09, MEMR-07, STRY-03
**Success Criteria** (what must be TRUE):
1. Player can plant a tree in Season 3+ — slow and expensive — and when it matures, harvesting yields a place-memory vignette delivered as a short interactive scene the player walks through (not just a text block).
2. Player encounters a Memory Storm in Season 4+ as a periodic event with visual + audio cues; unprotected plants decay faster, and the player can plant resilient species, build windbreaks, or time harvests around storms to mitigate the damage.
3. Player witnesses the Nameless Man's full arc: his Season 2 introduction, his progressively shortening and confusing dialogue across Seasons 2-4, and his Season 4 vanishing mid-sentence — no cutscene, no fanfare, the narrative weight carried by his absence at the next gate visit.
4. The Memory Storm event mechanic respects the "at most one new mechanic per Season" cap from Phase 4 (Storm is Season 4's one new mechanic; place-memory vignettes are Season 3's one new mechanic), validating that the scope-defense doctrine holds at the midpoint of the arc.
**Plans**: TBD
**UI hint**: yes
### Phase 6: Seasons 5-6 (Depth & Loom)
**Goal**: Player descends into the Below in Season 5 — root structures grow into ancient memory layers, ecosystem planting introduces clustered-yield depth, content from a pre-Archivist civilization surfaces — and Season 6 shifts the primary loop to feeding the Loom; the Archivist appears, never gendered, and responds when the player feeds the Loom a memory containing both joy and grief.
**Mode:** mvp
**Depends on**: Phase 5
**Requirements**: GARD-08, SEAS-07, SEAS-08, STRY-04, STRY-05
**Success Criteria** (what must be TRUE):
1. Player accesses the Below in Season 5+ and grows root structures into ancient memory layers, harvesting fragments that read as content from a pre-Archivist civilization (tonally distinct from surface-garden fragments).
2. Player plants clusters of compatible species and observes ecosystem planting yield bonuses kick in — the Season 5 mechanic that rewards thinking in ecosystems rather than individual crops, the one and only new mechanic Season 5 introduces.
3. In Season 6, the primary gameplay loop shifts to the Below: instead of harvesting fragments, the player feeds memories to the Loom; the Archivist appears, is never gendered (they/them) in any UI string or dialogue, speaks softly and reflectively, and asks the player a thematic question without forcing an answer.
4. Player feeds the Loom a memory containing both joy and grief and observes the Archivist's mechanical and tonal response: the Loom holds the contradiction, ending the Unremembering's advance — the load-bearing Season 6 narrative beat that gates Season 7.
**Plans**: TBD
**UI hint**: yes
### Phase 7: Season 7 (Return) & Final Choice
**Goal**: Player experiences the long, satisfying late-game of Season 7 where collected memories become seeds that automatically reconstitute the world, the Pale recedes, the Heartsoil expands beyond the garden walls, Lura's complete 7-Season arc resolves, the player makes the binary narrative choice and reads "The garden persists." — and the game transitions to a credits/coda *rest* state the player can return to indefinitely without grinding.
**Mode:** mvp
**Depends on**: Phase 6
**Requirements**: SEAS-09, SEAS-10, STRY-02, STRY-08
**Success Criteria** (what must be TRUE):
1. Player who reaches Season 7 sees collected memories automatically reconstitute the world as seeds, the Pale recede, and the Heartsoil expand beyond the garden walls — a long, satisfying late-game pace, not a sprint to credits.
2. Lura's dialogue spans all 7 Seasons with her complete arc resolved in Season 7; her dialogue across the entire game has reflected player progression in Ink-driven branches tied to Zustand variables (validating Phase 4-6's narrative-state plumbing on the longest arc in the game).
3. The final scene presents the binary narrative choice (*"They help us remember"* / *"They help us grow"*); both endings display the line *"The garden persists."* and both are tonally complete; neither unlocks alternate post-credits content (the Keeper, having no name, no backstory, and no dialogue beyond this choice, projects onto the player throughout).
4. After credits, the game enters a credits/coda *rest* state — not infinite prestige tiers — that the player can return to indefinitely without grinding; the finite Roothold ceiling from Phase 4 has held the line, and the game has *ended* the way A Dark Room and Universal Paperclips ended.
**Plans**: TBD
**UI hint**: yes
### Phase 8: UX, Accessibility & Launch Polish
**Goal**: The complete arc that landed in Phases 2-7 becomes a launch-ready product — multi-buy and audio sliders for power affordances, keyboard navigation and color-redundant icons for accessibility, a tab-title bloom for backgrounded play, the "what Lura said yesterday" UX doctrine enforced over any "fragments per hour" temptation, and visual regression coverage for the asset library so future model migrations cannot drift the watercolor consistency.
**Mode:** mvp
**Depends on**: Phase 7
**Requirements**: UX-03, UX-04, UX-06, UX-07, UX-08, UX-09, UX-12, PIPE-04
**Success Criteria** (what must be TRUE):
1. Player can buy plants and upgrades in multi-buy increments (×1 / ×10 / ×100 / Max) when meaningful for the current scaling, and can adjust separate Music, Ambient, and SFX volume sliders with a master mute keybind; settings persist across saves.
2. Player can navigate every menu and game surface using only the keyboard (Tab, Enter, Escape, arrow keys) with a always-visible focus indicator; all UI text is selectable, copy-pasteable, and supports browser zoom up to 200% without breaking layout; color is never the sole carrier of information — icons, labels, or patterns provide a redundant channel for color-blind players.
3. When the tab is backgrounded, the tab title and favicon update to reflect the backgrounded state (e.g., a small bloom appears when a fragment is ready), and returning-player UI affordances surface *what Lura said yesterday* — never *fragments per hour* or *optimization metrics* (mechanic-as-metaphor doctrine enforced in the final UX review).
4. Visual regression testing covers the full asset library and would flag any style drift before a model migration is merged — the AI asset pipeline discipline established in Phase 1 has end-to-end CI coverage by launch.
**Plans**: TBD
**UI hint**: yes
## Progress
**Execution Order:**
Phases execute in numeric order: 1 → 2 → 3 → 4 → 5 → 6 → 7 → 8
| Phase | Plans Complete | Status | Completed |
|-------|----------------|--------|-----------|
| 1. Foundations & Doctrine | 7/7 (01-05 Task 2 partial — north-star images awaiting human curation; CI shippable today) | In Progress | - |
| 2. Season 1 Vertical Slice (Soil) | 4/5 (Wave 0 + Wave 1 + Plan 02-04 complete; 02-05 final) | In Progress | - |
| 3. Watercolor & Cello Aesthetic | 0/TBD | Not started | - |
| 4. Season-Prestige Cycle & Season 2 (Roots) | 0/TBD | Not started | - |
| 5. Seasons 3-4 (Canopy & Storm) | 0/TBD | Not started | - |
| 6. Seasons 5-6 (Depth & Loom) | 0/TBD | Not started | - |
| 7. Season 7 (Return) & Final Choice | 0/TBD | Not started | - |
| 8. UX, Accessibility & Launch Polish | 0/TBD | Not started | - |
**Wave 2** *(blocked on Wave 1 completion)*
- [x] 02-04-lura-gate-beats-PLAN.md — inklecate compile pipeline + 4 authored .ink files (3 Lura beats + compost acknowledgements) + sim/narrative tick-count gate (1st/4th/8th harvest) + LuraDialogue overlay + InkRenderer drip + Phaser gate visual indicator (Wave 2; STRY-01, STRY-06, STRY-07 vacuous, STRY-10) ✓ 2026-05-09 (24 min) — see 02-04-lura-gate-beats-SUMMARY.md
- [x] 02-05-letter-settings-e2e-PLAN.md — sim/offline + auto-harvest + letter Ink + Letter overlay + Settings (Export/Import/Restore) + persistence-toast + boot-path save lifecycle wiring + URL-flag FakeClock injection + Playwright PIPE-07 e2e (Wave 2; UX-02, UX-10, CORE-03, CORE-11, PIPE-07) ✓ 2026-05-09 (20 min) — see 02-05-letter-settings-e2e-SUMMARY.md
**UI hint**: yes
### Phase 3: Watercolor & Cello Aesthetic
**Goal**: The working garden becomes the painted garden — every plant renders in watercolor-adjacent style on a Phaser 4 canvas with the post-process filter applied, the solo cello main theme plays through Howler.js with independent music/ambient/SFX buses that crossfade rather than hard-cut, and the player can toggle reduced motion to disable non-essential particles.
**Mode:** mvp
**Depends on**: Phase 2
**Requirements**: GARD-10, AEST-01, AEST-02, AEST-03, AEST-04, AEST-05, AEST-06, UX-05
**Success Criteria** (what must be TRUE):
1. Player sees the garden rendered in a watercolor-adjacent visual style on a Phaser 4 canvas with the watercolor post-process filter active; every plant looks like a real-world species made slightly wrong (no D&D-style fictional flora), and the Pale renders as overexposed white silence — luminous, pearlescent, *too bright* — accompanied by a faint tinnitus-like high tone.
2. Player hears a solo cello main theme on the music bus, ambient garden sounds (wind, birdsong, gate creak) on the ambient bus that thin and fade as the Unremembering's region draws closer, and per-plant SFX on the SFX bus — three independent buses, never hard-cutting, always crossfading.
3. Player progresses through a Season transition (using Phase 2's loop) and observes the visual palette and audio bed crossfade together — golden/autumnal early-game shifting toward the next Season's tonal register — with no audible click or visible flash.
4. Player who has `prefers-reduced-motion` set in their OS sees the game respect it by default; the player can also toggle a reduced-motion option in settings that disables non-essential particles and animation while preserving the painted look.
**Plans**: TBD
**UI hint**: yes
### Phase 4: Season-Prestige Cycle & Season 2 (Roots)
**Goal**: Player experiences their first die-off as the end of Season 1, watches Roothold persist across the reset toward a finite narrative-tied ceiling, unlocks cross-pollination as Season 2's single new mechanic, and meets the Nameless Man arriving at the garden gate — proving the Season state machine, save migration on real player saves, per-Season content lazy-loading, and the at-most-one-mechanic-per-Season scope-defense doctrine all work together.
**Mode:** mvp
**Depends on**: Phase 3
**Requirements**: SEAS-01, SEAS-02, SEAS-03, SEAS-04, SEAS-05, SEAS-06, GARD-05, GARD-07
**Success Criteria** (what must be TRUE):
1. Player progresses through the Season 1 → Season 2 transition: a frost die-off wipes surface plantings, the visual palette and audio bed crossfade per Phase 3, and Roothold persists with a value that demonstrably moved toward (not past) the finite ceiling tied to the Season 7 end-state design from Phase 1.
2. Player loads a save written under the Season 1 schema and watches it migrate cleanly into a Season-2-aware shape via the registered migration chain; the last 3 pre-migration snapshots are retained and the "restore previous save" option is reachable from settings.
3. Player unlocks Season 2 plant types absent from Season 1 (each with distinct growth time, harvest yield, and visual identity) and successfully cross-pollinates two adjacent compatible plants to produce a hybrid seed with mixed memory traits — the one and only new mechanic Season 2 introduces.
4. The Nameless Man appears at the gate during Season 2 with his own Ink-driven dialogue arc; only the current Season's content is in the runtime bundle (Seasons 3-7 are not in the initial chunk and load lazily on Season transition).
**Plans**: TBD
**UI hint**: yes
### Phase 5: Seasons 3-4 (Canopy & Storm)
**Goal**: Player tends the garden through the canopy era and the storm era — slow, expensive tree plantings yield short interactive place-memory vignettes; periodic Memory Storms accelerate decay of unprotected plants and demand resilient species or windbreaks; the Nameless Man's dialogue progressively shortens and confuses across Seasons 2-4 and he vanishes mid-sentence in Season 4 with no fanfare.
**Mode:** mvp
**Depends on**: Phase 4
**Requirements**: GARD-06, GARD-09, MEMR-07, STRY-03
**Success Criteria** (what must be TRUE):
1. Player can plant a tree in Season 3+ — slow and expensive — and when it matures, harvesting yields a place-memory vignette delivered as a short interactive scene the player walks through (not just a text block).
2. Player encounters a Memory Storm in Season 4+ as a periodic event with visual + audio cues; unprotected plants decay faster, and the player can plant resilient species, build windbreaks, or time harvests around storms to mitigate the damage.
3. Player witnesses the Nameless Man's full arc: his Season 2 introduction, his progressively shortening and confusing dialogue across Seasons 2-4, and his Season 4 vanishing mid-sentence — no cutscene, no fanfare, the narrative weight carried by his absence at the next gate visit.
4. The Memory Storm event mechanic respects the "at most one new mechanic per Season" cap from Phase 4 (Storm is Season 4's one new mechanic; place-memory vignettes are Season 3's one new mechanic), validating that the scope-defense doctrine holds at the midpoint of the arc.
**Plans**: TBD
**UI hint**: yes
### Phase 6: Seasons 5-6 (Depth & Loom)
**Goal**: Player descends into the Below in Season 5 — root structures grow into ancient memory layers, ecosystem planting introduces clustered-yield depth, content from a pre-Archivist civilization surfaces — and Season 6 shifts the primary loop to feeding the Loom; the Archivist appears, never gendered, and responds when the player feeds the Loom a memory containing both joy and grief.
**Mode:** mvp
**Depends on**: Phase 5
**Requirements**: GARD-08, SEAS-07, SEAS-08, STRY-04, STRY-05
**Success Criteria** (what must be TRUE):
1. Player accesses the Below in Season 5+ and grows root structures into ancient memory layers, harvesting fragments that read as content from a pre-Archivist civilization (tonally distinct from surface-garden fragments).
2. Player plants clusters of compatible species and observes ecosystem planting yield bonuses kick in — the Season 5 mechanic that rewards thinking in ecosystems rather than individual crops, the one and only new mechanic Season 5 introduces.
3. In Season 6, the primary gameplay loop shifts to the Below: instead of harvesting fragments, the player feeds memories to the Loom; the Archivist appears, is never gendered (they/them) in any UI string or dialogue, speaks softly and reflectively, and asks the player a thematic question without forcing an answer.
4. Player feeds the Loom a memory containing both joy and grief and observes the Archivist's mechanical and tonal response: the Loom holds the contradiction, ending the Unremembering's advance — the load-bearing Season 6 narrative beat that gates Season 7.
**Plans**: TBD
**UI hint**: yes
### Phase 7: Season 7 (Return) & Final Choice
**Goal**: Player experiences the long, satisfying late-game of Season 7 where collected memories become seeds that automatically reconstitute the world, the Pale recedes, the Heartsoil expands beyond the garden walls, Lura's complete 7-Season arc resolves, the player makes the binary narrative choice and reads "The garden persists." — and the game transitions to a credits/coda *rest* state the player can return to indefinitely without grinding.
**Mode:** mvp
**Depends on**: Phase 6
**Requirements**: SEAS-09, SEAS-10, STRY-02, STRY-08
**Success Criteria** (what must be TRUE):
1. Player who reaches Season 7 sees collected memories automatically reconstitute the world as seeds, the Pale recede, and the Heartsoil expand beyond the garden walls — a long, satisfying late-game pace, not a sprint to credits.
2. Lura's dialogue spans all 7 Seasons with her complete arc resolved in Season 7; her dialogue across the entire game has reflected player progression in Ink-driven branches tied to Zustand variables (validating Phase 4-6's narrative-state plumbing on the longest arc in the game).
3. The final scene presents the binary narrative choice (*"They help us remember"* / *"They help us grow"*); both endings display the line *"The garden persists."* and both are tonally complete; neither unlocks alternate post-credits content (the Keeper, having no name, no backstory, and no dialogue beyond this choice, projects onto the player throughout).
4. After credits, the game enters a credits/coda *rest* state — not infinite prestige tiers — that the player can return to indefinitely without grinding; the finite Roothold ceiling from Phase 4 has held the line, and the game has *ended* the way A Dark Room and Universal Paperclips ended.
**Plans**: TBD
**UI hint**: yes
### Phase 8: UX, Accessibility & Launch Polish
**Goal**: The complete arc that landed in Phases 2-7 becomes a launch-ready product — multi-buy and audio sliders for power affordances, keyboard navigation and color-redundant icons for accessibility, a tab-title bloom for backgrounded play, the "what Lura said yesterday" UX doctrine enforced over any "fragments per hour" temptation, and visual regression coverage for the asset library so future model migrations cannot drift the watercolor consistency.
**Mode:** mvp
**Depends on**: Phase 7
**Requirements**: UX-03, UX-04, UX-06, UX-07, UX-08, UX-09, UX-12, PIPE-04
**Success Criteria** (what must be TRUE):
1. Player can buy plants and upgrades in multi-buy increments (×1 / ×10 / ×100 / Max) when meaningful for the current scaling, and can adjust separate Music, Ambient, and SFX volume sliders with a master mute keybind; settings persist across saves.
2. Player can navigate every menu and game surface using only the keyboard (Tab, Enter, Escape, arrow keys) with a always-visible focus indicator; all UI text is selectable, copy-pasteable, and supports browser zoom up to 200% without breaking layout; color is never the sole carrier of information — icons, labels, or patterns provide a redundant channel for color-blind players.
3. When the tab is backgrounded, the tab title and favicon update to reflect the backgrounded state (e.g., a small bloom appears when a fragment is ready), and returning-player UI affordances surface *what Lura said yesterday* — never *fragments per hour* or *optimization metrics* (mechanic-as-metaphor doctrine enforced in the final UX review).
4. Visual regression testing covers the full asset library and would flag any style drift before a model migration is merged — the AI asset pipeline discipline established in Phase 1 has end-to-end CI coverage by launch.
**Plans**: TBD
**UI hint**: yes
## Progress
**Execution Order:**
Phases execute in numeric order: 1 → 2 → 3 → 4 → 5 → 6 → 7 → 8
| Phase | Plans Complete | Status | Completed |
|-------|----------------|--------|-----------|
| 1. Foundations & Doctrine | 7/7 (01-05 Task 2 partial — north-star images awaiting human curation; CI shippable today) | In Progress | - |
| 2. Season 1 Vertical Slice (Soil) | 6/6 | Complete | 2026-05-09 |
| 3. Watercolor & Cello Aesthetic | 0/TBD | Not started | - |
| 4. Season-Prestige Cycle & Season 2 (Roots) | 0/TBD | Not started | - |
| 5. Seasons 3-4 (Canopy & Storm) | 0/TBD | Not started | - |
| 6. Seasons 5-6 (Depth & Loom) | 0/TBD | Not started | - |
| 7. Season 7 (Return) & Final Choice | 0/TBD | Not started | - |
+13 -13
View File
@@ -2,16 +2,16 @@
gsd_state_version: 1.0
milestone: v1.0
milestone_name: milestone
status: ready_to_execute
stopped_at: "Phase 2 gap-closure plan (Plan 02-06 uat-gap-closure) created via /gsd-plan-phase 2 --gaps. Single new plan addresses 4 first-impression UX gaps from 2026-05-09 live UAT: G1 (BLOCKING) src/index.css for global page bg #1a1a1a / no white halo; G2 (BLOCKING) FirstRunHint component reading externalized 'Begin where the soil is bare.' from ui-strings.yaml + UiStringsSchema extension (Zod default strip mode would otherwise drop the key) + session-slice firstRunHintDismissed flag; G3 (HIGH) tile-renderer outline brightening 0x4d4d52→0x5a5a60 + hover bump 0x7a7a82; G4 (MEDIUM) gate-renderer wall-band Phaser primitive at gate column with alpha 0.15-0.20. Plan is Wave 0, depends_on [02-01..02-05], 5 tasks (4 gap fixes + e2e integration), 16 files_modified, requirements [GARD-01, AEST-07, UX-01] supplemental coverage. Phase 3 watercolor + cello deferral preserved (zero painted assets, zero new npm deps, V1Payload unchanged). Plan-checker found 1 BLOCKER + 1 WARNING on first pass; planner revised; residual frontmatter + 3 copy refs fixed inline. ROADMAP.md annotated with Wave 1/Wave 2 headers (annotate-dependencies). Next: /gsd-execute-phase 2 to run Plan 02-06; then /gsd-verify-work to flip 02-VERIFICATION.md status gaps_found → verified and re-route to the 6 HUMAN-UAT.md tone items."
last_updated: "2026-05-09T16:10:00.000Z"
last_activity: 2026-05-09 -- Phase 2 gap-closure plan created (Plan 02-06)
status: completed
stopped_at: "Phase 2 COMPLETE. Plan 02-06 (UAT gap closure) executed cleanly — 5 atomic feature/test commits + 1 docs commit (f52de0b G1 src/index.css, c46fc75 G2 FirstRunHint + UiStringsSchema extension + session-slice flag, ab48c7e G3 tile outline brightening + hover bump, 88adc4f G4 gate wall band primitive, 47b5b8d Playwright e2e G1+G2 assertions, 7f39cf6 SUMMARY). 333/333 vitest green (was 312, +21 new cases); npm run ci exits 0; Playwright e2e exits 0 in 1.5s. Hint copy chosen: 'Begin where the soil is bare.' (plan's #1 ranked candidate, bible voice). gsd-verifier re-verified 24/24 REQ-IDs structurally PASS + 4/4 UX gaps closed; 02-VERIFICATION.md frontmatter status flipped gaps_found → verified. Phase 2 vertical slice now plausibly ships as a free standalone Season-1 prologue (banner concern #2 escape hatch realized). Phase 3 watercolor + cello deferral preserved (zero painted assets, zero new npm deps, V1Payload unchanged). 7 HUMAN-UAT.md tone items remain pending (Lura voice, letter cadence, Begin tonal feel, ≥5min absence flow, gate visual indicator + LuraDialogue overlay, plus the new chosen first_run_hint copy review)."
last_updated: "2026-05-09T16:40:00.000Z"
last_activity: 2026-05-09 -- Phase 2 complete (24/24 REQ-IDs PASS + 4/4 UAT gaps closed); 7 HUMAN-UAT tone items pending
progress:
total_phases: 8
completed_phases: 1
completed_phases: 2
total_plans: 13
completed_plans: 12
percent: 23
completed_plans: 13
percent: 100
---
# Project State
@@ -21,16 +21,16 @@ progress:
See: .planning/PROJECT.md (updated 2026-05-08)
**Core value:** Every idle mechanic must function as a metaphor that the player absorbs without being told. When economy and meaning conflict, meaning wins.
**Current focus:** Phase 02 — Season 1 Vertical Slice (Soil) — 5/5 plans executed + 24/24 REQ-IDs PASS; gap-closure Plan 02-06 created and ready to execute. Next: `/gsd-execute-phase 2`.
**Current focus:** Phase 2 COMPLETE (24/24 REQ-IDs PASS + 4/4 UAT gaps closed). 7 HUMAN-UAT tone items pending. Next: `/gsd-discuss-phase 3` (Watercolor & Cello Aesthetic — GARD-10, AEST-01..06, UX-05).
## Current Position
Phase: 02 (season-1-vertical-slice-soil) — 5/5 prior plans executed; verifier 24/24 PASS; gap-closure Plan 02-06 created; status: ready_to_execute
Plans: 6 of 6 created (Wave 0 + Wave 1 + Wave 2 done; new Wave 0 gap-closure plan pending execution)
Status: Ready to execute (Plan 02-06)
Last activity: 2026-05-09 -- Phase 2 gap-closure plan created via /gsd-plan-phase 2 --gaps
Phase: 2 (season-1-vertical-slice-soil) — COMPLETE
Plans: 6 of 6 executed (Wave 1: 02-01 + 02-02 + 02-03; Wave 2: 02-04 + 02-05; gap-closure: 02-06)
Status: Phase 2 complete; awaiting human tone review of 7 HUMAN-UAT items + `/gsd-discuss-phase 3` to begin Phase 3
Last activity: 2026-05-09 -- Phase 2 complete via /gsd-execute-phase 2 (Plan 02-06 gap closure)
Progress: [██░░░░░░░] 23%
Progress: [██░░░░░░░] 25% (2/8 phases complete; 13/13 created plans executed)
## Verification Results
@@ -1,42 +1,45 @@
---
phase: 02-season-1-vertical-slice-soil
verified: 2026-05-09T11:24:00Z
verified: 2026-05-09T17:35:00Z
verifier_run_at: 2026-05-09T11:24:00Z
uat_run_at: 2026-05-09T15:50:00Z
status: gaps_found
score: 24/24 must-haves structurally verified; 4 first-impression UX gaps found in live UAT
re_verifier_run_at: 2026-05-09T17:35:00Z
status: verified
score: 24/24 REQ-IDs structurally PASS + 4/4 UX gaps closed (G1, G2, G3, G4); 6 HUMAN-UAT tone items remain pending
overrides_applied: 0
re_verification: false
gaps:
re_verification: true
re_verification_meta:
previous_status: gaps_found
previous_score: 24/24 REQ-IDs structurally PASS; 4 UX gaps open
gaps_closed:
- G1 — white halo around dark canvas (BLOCKING)
- G2 — no first-run prompt after Begin (BLOCKING)
- G3 — tile outlines too dim (HIGH)
- G4 — gate visual stands alone with no surrounding context (MEDIUM)
gaps_remaining: []
regressions: []
closing_plan: 02-06-uat-gap-closure
gaps_closed:
- id: G1
severity: blocking
title: "No global page CSS — white halo around dark canvas"
surface: "src/main.tsx, no src/index.css exists"
evidence: "User screenshot 2026-05-09: 1024×768 dark canvas centered in a fully-white viewport. Canvas backgroundColor is #1a1a1a (game/main.ts:16); body has no styling, defaults to white. Visual tonal break is jarring on every page load."
fix_shape: "Add src/index.css imported from main.tsx. body { margin: 0; min-height: 100vh; background: #1a1a1a; color: #e8e0d0; font-family: serif; } #game-container centered. ~15 lines."
must_have_link: "ROADMAP SC1 — 'no UI clutter' implies tonal coherence between canvas and surrounding chrome, not 'untreated browser default white'."
closed_by: 02-06-uat-gap-closure (commit f52de0b)
evidence: "src/index.css carries the 6 load-bearing rules (body bg #1a1a1a, color #e8e0d0, margin 0, min-height 100vh, font-family serif, #game-container flex centering); src/main.tsx:4 imports it; Playwright assertion at season1-loop.spec.ts:75-78 confirms `getComputedStyle(document.body).backgroundColor === 'rgb(26, 26, 26)'` from frame one in real Chromium; 6 file-read smoke tests in src/index.css.test.ts pin the rules."
- id: G2
severity: blocking
title: "No first-run prompt after Begin — player has no idea what to do"
surface: "src/App.tsx + the post-BeginScreen state"
evidence: "User feedback 2026-05-09: 'I am very confused.' After dismissing Begin, the only persistent UI is a settings cog at bottom-right. SeedPicker is gated behind clicking a tile; JournalIcon is invisible until first harvest. A first-time player sees an empty 4×4 grid + a stray gray gate rectangle and nothing else. The 'A Dark Room rule' the bible cites means 'one prompt at a time, minimal but always present' — not 'zero UI'."
fix_shape: "Tiny FirstRunHint component — single bible-voice line ('Click a tile to plant', or similar from ui-strings.yaml). Auto-dismisses on first plant. New `firstRunHintDismissed` flag in session-slice."
must_have_link: "ROADMAP SC1 + GARD-01 — A Dark Room rule honored requires at minimum the canonical first-prompt; player must know what to do on frame one."
closed_by: 02-06-uat-gap-closure (commit c46fc75)
evidence: "src/ui/first-run/FirstRunHint.tsx mounted in App.tsx:56 between BeginScreen and SeedPicker; copy externalized in content/seasons/01-soil/ui-strings.yaml:21 as `first_run_hint: \"Begin where the soil is bare.\"`; src/content/schemas/ui-strings.ts:38 extends UiStringsSchema with `first_run_hint: z.string().min(1)` (defeats Zod strip mode); src/store/session-slice.ts:44+51+68 carries firstRunHintDismissed flag + dismissFirstRunHint action; auto-dismisses on first plant !== null transition via tiles-slice subscription; grep confirms zero candidate strings hardcoded in FirstRunHint.tsx; firstRunHintDismissed does NOT appear in src/save/migrations.ts (session-state only — V1Payload uncontaminated, no migrations[2]); 6 behavioral tests in FirstRunHint.test.tsx; Playwright assertions B+C at season1-loop.spec.ts:91+133 confirm the live-loop visibility/dismissal flow."
- id: G3
severity: high
title: "Tile outlines too dim — 4×4 grid reads as 'gray check block'"
surface: "src/render/garden/tile-renderer.ts"
evidence: "User feedback: 'a confusing gray check block.' Tile outlines render with low contrast against the #1a1a1a canvas background; in the screenshot the 16 tiles read as faint suggestions rather than legible interactive surfaces. Hover state (if it exists) is similarly underexposed."
fix_shape: "Brighten empty-tile outline color (~#3a3a40 → ~#5a5a60); add a clearer hover state (~#7a7a82 outline + slight fill alpha bump). No visual style change beyond contrast — Phase 3 watercolor still owns the painted look."
must_have_link: "GARD-01 — 'Player can plant a seed into an unoccupied tile' presumes the player can SEE which tiles are unoccupied. Currently they cannot."
closed_by: 02-06-uat-gap-closure (commit ab48c7e)
evidence: "src/render/garden/tile-renderer.ts:14 OUTLINE_COLOR=0x5a5a60 (was 0x4d4d52); :15 OUTLINE_HOVER=0x7a7a82 (was 0x6e6e75); :17 HOVER_FILL_ALPHA=0.06 added; pointerover handler swaps outline + bumps hit rectangle's fill alpha; pointerout reverses; constants exported for testability; 5 phaser-mocked tests in tile-renderer.test.ts pin constants and pointerover behavior. NO new sprites, NO painted assets — Phase 3 watercolor deferral preserved."
- id: G4
severity: medium
title: "Gate visual stands alone with no surrounding context"
surface: "src/render/garden/gate-renderer.ts"
evidence: "User screenshot: gate at canvas (880, 384) appears as a stray gray rectangle floating to the right of the grid, with nothing connecting it to the garden. The bible's 'walled garden' framing requires the gate to read as part of a wall, not a free-floating element. Phase 3 paints the watercolor wall — this gap asks only for a faint structural primitive so the gate has visual context in Phase 2."
fix_shape: "Add a faint vertical line/band in gate-renderer connecting top-to-bottom of the canvas at the gate's column (Phaser primitive — alpha ~0.15-0.20 against #1a1a1a). Phase 3 paints over without changing the structural intent."
must_have_link: "Bible — 'walled garden at the fraying edge of a world.' AEST-07 only requires Begin-screen restraint, not no-context gate placement."
per_req:
closed_by: 02-06-uat-gap-closure (commit 88adc4f)
evidence: "src/render/garden/gate-renderer.ts:34-38 exports WALL_BAND_X=880 (matches GATE_X) + WALL_BAND_HEIGHT=768 (full canvas height) + WALL_BAND_ALPHA=0.18 (mid of 0.15-0.20 fix_shape range) + WALL_BAND_COLOR=0x6e6e75 + WALL_BAND_WIDTH=44; drawGate adds the wall as the FIRST rectangle (z-order: behind body / glow / hit) so the gate body remains the focal element; GateGameObjects interface gains a `wall` field (additive — Garden.ts unchanged); 4 phaser-mocked tests in gate-renderer.test.ts pin the alpha range, the first-rectangle geometry, the 4-rectangle total, and the GateGameObjects exposure. NO painted asset — Phaser primitive only — Phase 3 watercolor deferral preserved."
per_req:
CORE-02: PASS
CORE-03: PASS
@@ -81,35 +84,36 @@ human_verification:
- 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)."
- test: "Read the chosen first_run_hint copy in context — 'Begin where the soil is bare.'"
expected: "Copy lands in bible voice — warm, specific, contemplative, intermittent (one beat, no follow-on); echoes the BeginScreen CTA without redundancy; not a nag, not a tutorial, not a FOMO surface; player feels guided not instructed. The plan's #1 ranked candidate; if tone-review surfaces it as too elliptical, fallback to candidate #2 ('The soil is waiting.') or #3 ('Click a tile to plant.')."
why_human: "Tonal compliance of player-visible copy is inherently subjective; the line is now structurally externalized + visible after Begin. Banner concern #9 (tonal failure) requires the user's eyes on the words themselves."
---
# 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 (automated) → 2026-05-09T15:50:00Z (live UAT update)
**Status:** GAPS_FOUND — all 24 REQ-IDs structurally PASS, but live UAT surfaced 4 first-impression UX gaps that block phase sign-off.
**Re-verification:** No — initial verification.
**Overall verdict:** PHASE STRUCTURALLY COMPLETE BUT NOT SHIPPABLE — code passes every automated gate, but a brand-new player launching the dev server cannot intuit the loop and the page reads as "broken UI" (white halo around dark canvas, no first-run prompt, dim tile outlines, isolated gate visual). Route through `/gsd-plan-phase 2 --gaps` to spec a small focused gap-closure plan before marking the phase complete.
**Verified:** 2026-05-09T11:24:00Z (automated) → 2026-05-09T15:50:00Z (live UAT update) → 2026-05-09T17:35:00Z (re-verification after Plan 02-06 gap closure)
**Status:** VERIFIED — all 24 REQ-IDs structurally PASS, all 4 first-impression UX gaps now closed by Plan 02-06 (commits f52de0b, c46fc75, ab48c7e, 88adc4f, 47b5b8d). 6 HUMAN-UAT tone items remain pending below the now-cleared structural surfaces.
**Re-verification:** Yes — initial verification at 11:24:00Z found 0 structural gaps; live UAT at 15:50:00Z surfaced 4 first-impression UX gaps; Plan 02-06 closed all 4 in ~30 min; this re-verification at 17:35:00Z confirms closure with no regressions.
**Overall verdict:** PHASE STRUCTURALLY COMPLETE AND SHIPPABLE — code passes every automated gate (333/333 vitest + Playwright e2e + lint + build + asset provenance + bundle-split), the four first-impression UX gaps are closed at the file-evidence level, no Phase-2 banner concerns regressed, no painted assets added (Phase 3 deferral preserved), no V1Payload contamination, no new npm dependencies, no edits in src/sim/**. Tonal sign-off on Lura's voice + letter cadence + Begin tone + first_run_hint copy remains the user's call at next merge.
---
## Gaps Found in Live UAT (2026-05-09T15:50:00Z)
## Gaps Found in Live UAT (2026-05-09T15:50:00Z) — NOW CLOSED
The 5 plans + automated verifier all passed; human live-loop walkthrough on a fresh dev server surfaced first-impression UX gaps NOT visible in the test suite. See frontmatter `gaps:` for the structured list. Summary:
The 5 plans + automated verifier all passed; human live-loop walkthrough on a fresh dev server surfaced first-impression UX gaps NOT visible in the test suite. See frontmatter `gaps_closed:` for the structured list. Summary:
| Gap | Severity | What user sees | Fix shape (one-line) |
|-----|----------|---------------|----------------------|
| G1 | blocking | Dark canvas floats in a sea of white = visually broken on every page load | Add `src/index.css` with body bg `#1a1a1a`, import in `main.tsx` |
| G2 | blocking | After dismissing Begin, no instruction visible — player confused | Add `FirstRunHint` overlay with one bible-voice line, auto-dismiss on first plant |
| G3 | high | 4×4 grid reads as "gray check block" — outlines too dim against canvas | Brighten empty-tile outline + hover state contrast in `tile-renderer.ts` |
| G4 | medium | Gate visual at canvas (880, 384) reads as stray gray rectangle | Add faint vertical wall primitive in `gate-renderer.ts` for Phase-2 context |
| Gap | Severity | What user saw | Fix shape (one-line) | Status (2026-05-09T17:35:00Z) |
|-----|----------|---------------|----------------------|-------------------------------|
| G1 | blocking | Dark canvas floats in a sea of white = visually broken on every page load | Add `src/index.css` with body bg `#1a1a1a`, import in `main.tsx` | CLOSED (commit f52de0b) |
| G2 | blocking | After dismissing Begin, no instruction visible — player confused | Add `FirstRunHint` overlay with one bible-voice line, auto-dismiss on first plant | CLOSED (commit c46fc75) |
| G3 | high | 4×4 grid reads as "gray check block" — outlines too dim against canvas | Brighten empty-tile outline + hover state contrast in `tile-renderer.ts` | CLOSED (commit ab48c7e) |
| G4 | medium | Gate visual at canvas (880, 384) reads as stray gray rectangle | Add faint vertical wall primitive in `gate-renderer.ts` for Phase-2 context | CLOSED (commit 88adc4f) |
**Why the automated verifier missed all 4:** the 312 vitest cases pin behavioral correctness (state transitions, schema, determinism, save round-trip); the Playwright e2e drives the loop programmatically (it doesn't *look* at the screen). First-impression "what does a new player see?" is a category the test suite cannot cover. The HUMAN-UAT.md tone items capture the next layer (Lura's voice, letter cadence) — the gaps above are a layer beneath those, structurally simpler but visually load-bearing.
**Why the automated verifier missed all 4:** the 312 vitest cases pin behavioral correctness (state transitions, schema, determinism, save round-trip); the Playwright e2e drives the loop programmatically (it doesn't *look* at the screen). First-impression "what does a new player see?" is a category the test suite cannot cover. The HUMAN-UAT.md tone items capture the next layer (Lura's voice, letter cadence) — the gaps above were a layer beneath those, structurally simpler but visually load-bearing.
**Phase 3 deferral preserved:** the watercolor + cello + painted plants the bible describes remain Phase 3 scope. These gaps are minimum-viable functional UX (page-bg coherence, first-prompt presence, grid legibility, gate context) — every fix uses Phaser primitives or a single CSS file, no painted assets.
**Next:** `/gsd-plan-phase 2 --gaps` to create the gap-closure plan.
**Phase 3 deferral preserved:** the watercolor + cello + painted plants the bible describes remain Phase 3 scope. Every fix uses Phaser primitives or a single CSS file, no painted assets.
---
@@ -154,7 +158,7 @@ All automated gates green.
| 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-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. **Plan 02-06 G3 supplemental:** tile-renderer brightens OUTLINE_COLOR + adds hover fill bump so the planting affordance is visually legible from frame one. |
| 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. |
@@ -168,8 +172,8 @@ All automated gates green.
| 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. |
| 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. **Plan 02-06 G1 supplemental:** body bg now matches BeginScreen overlay so there is no tonal break at any moment of the gesture flow. |
| 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. **Plan 02-06 G2 supplemental:** after Begin dismisses, FirstRunHint surfaces a single bible-voice line ("Begin where the soil is bare.") so the A-Dark-Room first-prompt rule is honored — the player sees one prompt at a time, minimal but always present until acted upon. |
| 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. |
@@ -185,15 +189,15 @@ All automated gates green.
| # | 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. |
| 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). **Plan 02-06 verification:** FirstRunHint mounts AFTER BeginScreen in App.tsx render tree (App.tsx:55-56) and uses `pointerEvents: 'none'`; Begin → audio-bootstrap path is unaltered. |
| 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. **Plan 02-06 verification:** the chosen first_run_hint copy ("Begin where the soil is bare.") is one quiet imperative — no nag, no streak, no time pressure, no urgency; tonal sign-off remains a human-verification item. |
| 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`, `FirstRunHint.tsx` shows zero hardcoded English strings outside CSS values, ARIA roles, command kinds, and event names. **Plan 02-06 verification:** grep for the three candidate hint strings inside FirstRunHint.tsx returns ZERO matches; copy lives in ui-strings.yaml + UiStringsSchema is extended with `first_run_hint: z.string().min(1)` to defeat Zod strip mode. |
| 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. |
| 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. **Plan 02-06 strengthens this** — first-impression UX gaps closed, page-bg coherent, first-prompt present, grid legible, gate has wall context. The vertical slice now actually feels like a shippable prologue to a brand-new player on frame one. |
| 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. **Plan 02-06 verification:** `git log f52de0b~1..HEAD --diff-filter=A --name-only -- '*.png' '*.jpg' '*.webp' '*.svg' '*.gif' '*.bmp'` returns empty — gap-closure plan added zero painted assets. Phase 3 watercolor deferral preserved. |
| 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. |
| 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. **Plan 02-06 adds one more line for review:** the chosen first_run_hint copy "Begin where the soil is bare." (now player-visible) joins the queue for tonal sign-off. |
| 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. **Plan 02-06 verification:** firstRunHintDismissed lives in src/store/session-slice.ts (NOT V1Payload); migrations.ts is unchanged; no migrations[2] entry; the new flag is session-state only as the doctrine requires. |
---
@@ -232,7 +236,7 @@ All 9 spot-checks PASS.
---
## Human Verification Required (6 items)
## Human Verification Required (7 items)
See frontmatter `human_verification` for full structure. Headlines:
@@ -242,6 +246,7 @@ See frontmatter `human_verification` for full structure. Headlines:
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.
7. **Read the chosen first_run_hint copy in context — "Begin where the soil is bare."** — bible voice; warm, specific, contemplative, intermittent; not a nag, not a tutorial. Plan's #1 candidate; fallbacks #2 ("The soil is waiting.") and #3 ("Click a tile to plant.") available if tone-review surfaces #1 as too elliptical.
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.
@@ -286,3 +291,167 @@ The Phase-2 vertical slice could plausibly ship as a free standalone Season-1 pr
_Verified: 2026-05-09T11:24:00Z_
_Verifier: Claude (gsd-verifier)_
---
## Gap Closure Verification (2026-05-09T17:35:00Z re-verification)
**Re-verifier run at:** 2026-05-09T17:35:00Z
**Closing plan:** `02-06-uat-gap-closure-PLAN.md``02-06-uat-gap-closure-SUMMARY.md`
**Closing commits:** `f52de0b` (G1) → `c46fc75` (G2) → `ab48c7e` (G3) → `88adc4f` (G4) → `47b5b8d` (e2e integration) → `7f39cf6` (docs)
**Mode:** goal-backward — start from each gap's `fix_shape`, verify codebase satisfies it.
---
### Gap closure: G1 — white halo around dark canvas (BLOCKING)
**Fix shape:** "Add `src/index.css` imported from `main.tsx`. body { margin: 0; min-height: 100vh; background: #1a1a1a; color: #e8e0d0; font-family: serif; } #game-container centered. ~15 lines."
| Check | Evidence | Status |
|-------|----------|--------|
| `src/index.css` exists | File present, 27 lines (close to ~15 estimate; the extra are explanatory comments) | PASS |
| body bg = #1a1a1a | `src/index.css:17` `background: #1a1a1a;` (inside the `html, body` rule) | PASS |
| body color = #e8e0d0 | `src/index.css:18` `color: #e8e0d0;` | PASS |
| body margin = 0 | `src/index.css:14` `margin: 0;` | PASS |
| body min-height = 100vh | `src/index.css:16` `min-height: 100vh;` | PASS |
| body font-family = serif | `src/index.css:19` `font-family: serif;` | PASS |
| #game-container centered | `src/index.css:22-26` `#game-container { display: flex; justify-content: center; align-items: center; }` | PASS |
| `src/main.tsx` imports it | `src/main.tsx:4` `import './index.css';` (with explanatory comment "Plan 02-06 G1") | PASS |
| Playwright e2e proves the bundled CSS applies in real Chromium | `tests/e2e/season1-loop.spec.ts:75-78` evaluates `getComputedStyle(document.body).backgroundColor` and asserts it equals `'rgb(26, 26, 26)'` (= #1a1a1a) from frame one | PASS |
| File-read smoke tests | `src/index.css.test.ts` — 6 cases pinning each load-bearing rule | PASS |
**G1 verdict:** CLOSED. The dark canvas no longer floats in a sea of white at any frame.
---
### Gap closure: G2 — no first-run prompt after Begin (BLOCKING)
**Fix shape:** "Tiny FirstRunHint component — single bible-voice line ('Click a tile to plant', or similar from ui-strings.yaml). Auto-dismisses on first plant. New `firstRunHintDismissed` flag in session-slice."
| Check | Evidence | Status |
|-------|----------|--------|
| `src/ui/first-run/FirstRunHint.tsx` exists | File present, 75 lines | PASS |
| Component reads externalized line via `uiStrings[1]?.first_run_hint` | `FirstRunHint.tsx:47` `const hint = uiStrings[1]?.first_run_hint;` — no hardcoded English in component | PASS |
| **No hardcoded candidate strings in component** | `grep "Begin where the soil is bare\|The soil is waiting\|Click a tile to plant" src/ui/first-run/FirstRunHint.tsx` → 0 matches | PASS |
| `content/seasons/01-soil/ui-strings.yaml` carries `first_run_hint` key | Line 21: `first_run_hint: "Begin where the soil is bare."` (the plan's #1 ranked candidate, unchanged) | PASS |
| `src/content/schemas/ui-strings.ts` extends UiStringsSchema | Line 38: `first_run_hint: z.string().min(1),` — defeats Zod default strip mode (without this, the YAML key would silently drop from parsed.data and FirstRunHint would render null in production) | PASS |
| `src/store/session-slice.ts` adds `firstRunHintDismissed` + `dismissFirstRunHint` action | Line 44: `firstRunHintDismissed: boolean;` (interface), Line 51: `dismissFirstRunHint: () => void;` (interface), Line 61: `firstRunHintDismissed: false,` (initial state), Line 68: `dismissFirstRunHint: () => set({ firstRunHintDismissed: true }),` (action) | PASS |
| **NO V1Payload contamination** (CRITICAL doctrine check) | `grep firstRunHintDismissed src/save/migrations.ts` → 0 matches; `git diff f52de0b~1 HEAD -- src/save/migrations.ts` → empty diff; flag is session-state ONLY | PASS |
| **NO migrations[2] entry added** (CRITICAL doctrine check) | `git diff f52de0b~1 HEAD -- src/save/` → empty diff; the only `migrations[2]` mentions in migrations.ts are doc-comments confirming "no migrations[2]" | PASS |
| FirstRunHint mounted in App.tsx between BeginScreen and SeedPicker | `src/App.tsx:55` `<BeginScreen />`, `:56` `<FirstRunHint />`, `:57` `<SeedPicker />` — exactly the spec | PASS |
| Auto-dismiss on first plant (subscribe to tiles slice) | `FirstRunHint.tsx:35-42` `useEffect` checks `tiles.some((t) => t?.plant !== null)` and calls `dismissFirstRunHint()` when true | PASS |
| Re-shows on hard reload (session state, not save state) | `firstRunHintDismissed: false` is the initial value in `createSessionSlice`; on reload the slice resets, so a fresh tab pre-first-plant sees the hint again — correct A-Dark-Room first-run UX | PASS |
| Behavioral test coverage | `FirstRunHint.test.tsx` — 6 cases: hidden when Begin still up, hidden when dismissed, renders externalized line, reads from uiStrings (not hardcoded), auto-dismisses on first plant, stays dismissed on subsequent tile changes | PASS |
| Playwright e2e proves the live-loop visibility/dismissal | `season1-loop.spec.ts:91` asserts `getByTestId('first-run-hint').toBeVisible()` after Begin click; `:133` asserts `.not.toBeVisible()` after first plant lands | PASS |
| `src/ui/index.ts` re-exports `./first-run` | Line 9: `export * from './first-run';` | PASS |
| `pointerEvents: 'none'` on the hint root | `FirstRunHint.tsx:68` — hint doesn't intercept pointer events, so the underlying canvas receives clicks for tile interaction; banner concern #7 (Web Audio user-gesture) preserved | PASS |
| Component uses `aria-live="polite"` + `role="status"` | `FirstRunHint.tsx:53-54` — accessible to screen readers without interrupting | PASS |
**G2 verdict:** CLOSED with all doctrine constraints honored. firstRunHintDismissed is session-state only; copy is externalized; schema is extended to defeat Zod strip mode; FirstRunHint mounts between BeginScreen and SeedPicker; banner concern #7 (Web Audio user-gesture) is preserved by `pointerEvents: 'none'`.
---
### Gap closure: G3 — tile outlines too dim (HIGH)
**Fix shape:** "Brighten empty-tile outline color (~#3a3a40 → ~#5a5a60); add a clearer hover state (~#7a7a82 outline + slight fill alpha bump). No visual style change beyond contrast — Phase 3 watercolor still owns the painted look."
| Check | Evidence | Status |
|-------|----------|--------|
| `src/render/garden/tile-renderer.ts` exists | File present, 62 lines | PASS |
| `OUTLINE_COLOR` brightened to ~0x5a5a60 | Line 14: `export const OUTLINE_COLOR = 0x5a5a60;` (was 0x4d4d52) — exactly matches fix_shape | PASS |
| `OUTLINE_HOVER` brightened to ~0x7a7a82 | Line 15: `export const OUTLINE_HOVER = 0x7a7a82;` (was 0x6e6e75) — exactly matches fix_shape | PASS |
| Hover fill alpha bump | Line 17: `const HOVER_FILL_ALPHA = 0.06;` — slight bump exactly as fix_shape specifies "slight fill alpha bump" | PASS |
| Pointerover swaps outline + bumps fill | Lines 46-49: `hit.on('pointerover', () => { drawOutline(g, ..., OUTLINE_HOVER); hit.setFillStyle(0xffffff, HOVER_FILL_ALPHA); });` | PASS |
| Pointerout reverses | Lines 50-53: `hit.on('pointerout', () => { drawOutline(g, ..., OUTLINE_COLOR); hit.setFillStyle(0xffffff, 0); });` | PASS |
| **NO new sprites or painted assets** | `git log f52de0b~1..HEAD --diff-filter=A --name-only -- '*.png' '*.jpg' '*.webp' '*.svg' '*.gif' '*.bmp'` → empty; only color + alpha values changed; Phase 3 deferral preserved | PASS |
| Constants exported for testability | `OUTLINE_COLOR` and `OUTLINE_HOVER` are `export const`, allowing the test file to import and assert them | PASS |
| Test coverage | `tile-renderer.test.ts` — 5 cases via Phaser-mock pattern: constants pinned, 16 tile groups created, initial draw uses OUTLINE_COLOR, pointerover swaps to OUTLINE_HOVER + fill bump (>0, ≤0.1) | PASS |
| Reduced-motion-safe | Hover is steady-state outline + fill swap — no tweens, no animations; banner-concern adjacent UX restraint preserved | PASS |
**G3 verdict:** CLOSED. The 4×4 grid now reads as legible interactive surfaces against the #1a1a1a canvas; hover state contrasts the resting state visibly without animation noise.
---
### Gap closure: G4 — gate visual stands alone with no surrounding context (MEDIUM)
**Fix shape:** "Add a faint vertical line/band in gate-renderer connecting top-to-bottom of the canvas at the gate's column (Phaser primitive — alpha ~0.15-0.20 against #1a1a1a). Phase 3 paints over without changing the structural intent."
| Check | Evidence | Status |
|-------|----------|--------|
| `src/render/garden/gate-renderer.ts` adds wall band | Lines 34-38: WALL_BAND_X / WALL_BAND_WIDTH / WALL_BAND_HEIGHT / WALL_BAND_ALPHA / WALL_BAND_COLOR all exported | PASS |
| Wall is a Phaser Rectangle primitive (not a painted asset) | Lines 56-64: `scene.add.rectangle(WALL_BAND_X, WALL_BAND_HEIGHT / 2, WALL_BAND_WIDTH, WALL_BAND_HEIGHT, WALL_BAND_COLOR, WALL_BAND_ALPHA)` | PASS |
| Wall at gate's column | `WALL_BAND_X = GATE_X = 880` (the gate column) | PASS |
| Wall spans full canvas height | `WALL_BAND_HEIGHT = 768` (matches Phaser canvas height in `src/game/main.ts`) — top-to-bottom span as fix_shape requires | PASS |
| Alpha in 0.15-0.20 range | `WALL_BAND_ALPHA = 0.18` — mid of the fix_shape range | PASS |
| Wall drawn FIRST (z-order: behind body / glow / hit) | `drawGate` adds `wall` first (lines 56-64), then `body` (66-72), `glow` (73-80), `hit` (84-91); the gate body remains the visual focal point | PASS |
| `GateGameObjects` exposes `wall` field (additive, Garden.ts unchanged) | Line 42: `wall: Phaser.GameObjects.Rectangle;` — the destructuring at the call site captures the whole returned object, so the new field is structurally safe | PASS |
| **NO painted asset added** | `git log f52de0b~1..HEAD --diff-filter=A --name-only -- '*.png' '*.jpg' '*.webp' '*.svg'` → empty; Phaser primitive only; Phase 3 watercolor deferral preserved | PASS |
| Wall does NOT pulse | Lines 99-122 `updateGateIndicator` is unchanged from Phase 2; only `glow` pulses; wall is steady-state alpha — reduced-motion-safe | PASS |
| Test coverage | `gate-renderer.test.ts` — 4 cases via Phaser-mock pattern: constants in fix_shape range (alpha 0.15-0.20), wall is FIRST rectangle with full canvas height, 4 total rectangles (wall + body + glow + hit), GateGameObjects exposes `wall` handle | PASS |
**G4 verdict:** CLOSED. The gate now has structural wall context — it reads as part of a wall, not a free-floating element — using only a single Phaser Rectangle primitive at alpha 0.18. Phase 3 paints the watercolor wall over this primitive without changing the structural intent.
---
### Constraint compliance (CRITICAL_CONSTRAINTS from re-verification request)
| Constraint | Verification command | Status |
|------------|---------------------|--------|
| **No painted assets added in 02-06 commits** (Phase 3 deferral) | `git log f52de0b~1..HEAD --diff-filter=A --name-only -- '*.png' '*.jpg' '*.webp' '*.svg' '*.gif' '*.bmp'` → empty | PASS |
| **No new npm dependencies** | `git diff f52de0b~1 HEAD -- package.json package-lock.json` → empty | PASS |
| **No edits in src/sim/** (sim purity preserved) | `git diff f52de0b~1 HEAD -- 'src/sim/**'` → empty | PASS |
| **firstRunHintDismissed is session-state only** | `grep firstRunHintDismissed src/save/migrations.ts` → 0 matches | PASS |
| **No migrations[2] entry** | `git diff f52de0b~1 HEAD -- src/save/migrations.ts` → empty | PASS |
| **Hint copy externalized (not hardcoded)** | `grep "Begin where the soil is bare\|The soil is waiting\|Click a tile to plant" src/ui/first-run/FirstRunHint.tsx` → 0 matches | PASS |
| **UiStringsSchema extended** (defeats Zod strip mode) | `grep -E 'first_run_hint:\s*z\.string\(\)' src/content/schemas/ui-strings.ts` → matches at line 38 | PASS |
---
### Re-run gates (post-closure)
| Gate | Command | Result | Status |
|------|---------|--------|--------|
| Vitest | `npm test` (run via `npm run ci`) | **43 test files, 333/333 passed** (was 312 → +21 new cases for G1/G2/G3/G4) | PASS |
| Lint | `npm run lint` (run via `npm run ci`) | Exit 0 | PASS |
| Compile Ink | `npm run compile:ink` | 5 .ink → 5 .ink.json (unchanged) | PASS |
| Asset provenance | `node scripts/validate-assets.mjs` | Exit 0; 2 valid assets (unchanged) | PASS |
| Build | `npm run build` | Exit 0; 1.9MB entry chunk (unchanged — no new deps, no new image assets) | PASS |
| Bundle split | `npm run check:bundle-split` | Exit 0; PIPE-02 OK; chunkContentMatch=true | PASS |
| Playwright e2e | `npm run test:e2e` | 1 passed in 1.51.6s (test runtime; was 1.6s, plan SUMMARY noted 1.7s with the +3 new assertions); confirmed across 2 consecutive runs | PASS |
**Note on first-run e2e flake:** the very first `npm run test:e2e` after a long-idle dev server hit a 30s timeout on the `waitForFunction` for the plantSeed dispatch. Two immediate consecutive re-runs both passed in 1.51.6s. This matches the documented dev-server cold-start pattern (Playwright config `webServer` with `reuseExistingServer: false` triggers Vite's first-load module graph build). It is not a regression introduced by Plan 02-06; it is a pre-existing cold-start timing characteristic of the test harness. Recorded as info-level — not actionable.
---
### Phase-2 banner concerns — re-checked under Plan 02-06's diff
| # | Banner concern | Closure-plan effect | Verdict |
|---|----------------|----------------------|---------|
| 5 | AI asset style drift | Plan adds 0 new image assets (only Phaser primitives + 1 CSS file) | DEFENDED — no provenance bypass risk |
| 7 | Web Audio user-gesture | FirstRunHint mounts AFTER BeginScreen (App.tsx:55-56) and uses `pointerEvents: 'none'`; Begin → audio-bootstrap path is unaltered | DEFENDED — bootstrapAudioContext still synchronous-inside-click |
| 6 | Anti-FOMO | Chosen first_run_hint copy "Begin where the soil is bare." is one quiet imperative — no nag, no streak, no time pressure, no urgency | DEFENDED — tonal sign-off remains a HUMAN-UAT item but the structural shape is anti-FOMO compliant |
| 9 | Tonal failure | The chosen first_run_hint copy is now player-visible and joins the queue for tonal sign-off — added as item #7 in `human_verification` | NEEDS HUMAN VERIFICATION (added to the 6→7 HUMAN-UAT item list) |
| 10 | Authored content / code divergence | All player-visible Plan-02-06 copy lives in `content/seasons/01-soil/ui-strings.yaml`; UiStringsSchema is extended so the YAML key actually reaches runtime; FirstRunHint reads `uiStrings[1]?.first_run_hint` and renders null if missing — no hardcoded English | DEFENDED |
---
### Re-verification verdict
**ALL 4 first-impression UX gaps are CLOSED.** The 24 Phase-2 REQ-IDs remain structurally PASS with no regressions detected (sim purity preserved, V1Payload uncontaminated, no new dependencies, no painted assets, no migrations[2]).
The Phase-2 vertical slice now actually delivers the "could plausibly ship as a free standalone Season-1 prologue" contract that ROADMAP cites as the project's escape hatch against the 7-Season scope risk (banner concern #2). A brand-new player launching `npm run dev` on frame one sees:
1. The dark canvas in a tonally-coherent dark viewport (no white halo).
2. The Begin gate as a single typographic placeholder.
3. After Begin clicks, a single bible-voice instructional line.
4. A legible 4×4 tile grid against the canvas background.
5. The gate as part of a wall, not a free-floating gray rectangle.
**6 → 7 HUMAN-UAT tone items remain pending** below the now-cleared structural surfaces — the chosen first_run_hint copy "Begin where the soil is bare." joins the queue. These remain the user's call at next merge / playtest.
**Phase 2 is structurally complete and shippable.**
---
_Re-verified: 2026-05-09T17:35:00Z_
_Re-verifier: Claude (gsd-verifier)_