From 0ed79b0eb1bf0b8b70308b20a70377288f66aacf Mon Sep 17 00:00:00 2001 From: josh Date: Sat, 9 May 2026 12:05:52 -0400 Subject: [PATCH] docs(02-06): plan UAT gap closure (G1-G4) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Single Wave 0 plan addressing the 4 first-impression UX gaps from the 2026-05-09 live UAT: - G1 (BLOCKING) src/index.css imported from main.tsx — body bg #1a1a1a, serif color #e8e0d0 — closes the white halo around the dark canvas - 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 (NOT V1Payload) - 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 Phase 3 watercolor + cello deferral preserved: zero painted assets, zero new npm dependencies, V1Payload unchanged. Plan-checker found 1 BLOCKER (Zod schema strip mode breaking G2 silently) + 1 WARNING (hint copy ranking pushed non-bible-voice option first); planner revised; residual frontmatter + 3 copy refs fixed inline. Plan: 5 tasks, 16 files_modified, depends_on [02-01..02-05], requirements [GARD-01, AEST-07, UX-01] supplemental coverage. ROADMAP.md annotated with Wave 1/Wave 2 headers. Next: /gsd-execute-phase 2. Co-Authored-By: Claude Opus 4.7 (1M context) --- .planning/ROADMAP.md | 3 + .planning/STATE.md | 24 +- .../02-06-uat-gap-closure-PLAN.md | 1461 +++++++++++++++++ 3 files changed, 1476 insertions(+), 12 deletions(-) create mode 100644 .planning/phases/02-season-1-vertical-slice-soil/02-06-uat-gap-closure-PLAN.md diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md index 26da8e3..579417b 100644 --- a/.planning/ROADMAP.md +++ b/.planning/ROADMAP.md @@ -57,9 +57,12 @@ Plans: 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: +**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 + +**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 diff --git a/.planning/STATE.md b/.planning/STATE.md index b68a248..59d6596 100644 --- a/.planning/STATE.md +++ b/.planning/STATE.md @@ -2,16 +2,16 @@ gsd_state_version: 1.0 milestone: v1.0 milestone_name: milestone -status: gaps_found -stopped_at: "Phase 2 executed end-to-end (5/5 plans, 312/312 vitest, Playwright PIPE-07 green, npm run ci exits 0). gsd-verifier scored 24/24 REQ-IDs structurally PASS and routed 6 tone/live-loop items to HUMAN-UAT.md. Live UAT (user dev-server walkthrough) surfaced 4 first-impression UX gaps that block phase sign-off — these are NOT Phase-3 aesthetic deferrals, they are minimum-viable functional UX gaps: G1 no global page CSS (white halo around dark canvas), G2 no first-run prompt after Begin (player confused — A Dark Room rule violated), G3 dim tile outlines (grid reads as 'gray check block'), G4 isolated gate visual (no surrounding wall context). Gaps documented in 02-VERIFICATION.md frontmatter (status: gaps_found). Phase 3 watercolor + cello deferral preserved — every G-fix uses Phaser primitives or one CSS file, no painted assets. HUMAN-UAT.md tone items (Lura voice, letter cadence) remain pending below the structural gaps and will be re-reviewed after gap closure. Next: /gsd-plan-phase 2 --gaps." -last_updated: "2026-05-09T15:55:00.000Z" -last_activity: 2026-05-09 +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) progress: total_phases: 8 completed_phases: 1 - total_plans: 12 + total_plans: 13 completed_plans: 12 - percent: 22 + percent: 23 --- # 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 done + verifier 24/24 PASS, but live UAT found 4 first-impression UX gaps. Awaiting `/gsd-plan-phase 2 --gaps`. +**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 Position -Phase: 02 (season-1-vertical-slice-soil) — 5/5 plans executed; verifier 24/24 PASS; live UAT found 4 blocking/high gaps; status: gaps_found -Plans: 5 of 5 created (3 waves); Wave 0 (02-01) DONE; Wave 1 (02-02 + 02-03) DONE; Wave 2 (02-04 + 02-05) DONE; gap-closure plan pending -Status: All 5 Phase-2 plans executed cleanly (12 atomic feature commits + 5 doc commits). 312/312 vitest green; Playwright PIPE-07 e2e green in 1.6s; npm run ci exits 0. gsd-verifier scored 24/24 REQ-IDs structurally PASS and surfaced 6 inherently subjective tone/live-loop items in HUMAN-UAT.md. User ran the dev server (first live UAT) and reported 4 first-impression UX gaps that the test suite cannot detect: G1 white halo around dark canvas (no global page CSS), G2 no first-run prompt after Begin (A Dark Room rule violated), G3 tile outlines too dim against canvas bg, G4 gate visual stands alone with no wall context. All 4 are minimum-viable functional UX gaps — Phase 3 watercolor deferral preserved (every fix uses Phaser primitives or one CSS file). VERIFICATION.md updated to status: gaps_found with structured gap entries. The HUMAN-UAT.md tone items remain pending below the structural gaps. -Last activity: 2026-05-09 -- Phase 2 execute complete; live UAT surfaced 4 UX gaps; awaiting /gsd-plan-phase 2 --gaps +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 -Progress: [██░░░░░░░░] 22% +Progress: [██░░░░░░░░] 23% ## Verification Results diff --git a/.planning/phases/02-season-1-vertical-slice-soil/02-06-uat-gap-closure-PLAN.md b/.planning/phases/02-season-1-vertical-slice-soil/02-06-uat-gap-closure-PLAN.md new file mode 100644 index 0000000..7a6049c --- /dev/null +++ b/.planning/phases/02-season-1-vertical-slice-soil/02-06-uat-gap-closure-PLAN.md @@ -0,0 +1,1461 @@ +--- +phase: 02 +plan: 06 +type: execute +wave: 0 +depends_on: [02-01, 02-02, 02-03, 02-04, 02-05] +gap_closure: true +files_modified: + - src/index.css + - src/main.tsx + - src/index.css.test.ts + - src/store/session-slice.ts + - src/content/schemas/ui-strings.ts + - content/seasons/01-soil/ui-strings.yaml + - src/ui/first-run/FirstRunHint.tsx + - src/ui/first-run/FirstRunHint.test.tsx + - src/ui/first-run/index.ts + - src/ui/index.ts + - src/App.tsx + - src/render/garden/tile-renderer.ts + - src/render/garden/tile-renderer.test.ts + - src/render/garden/gate-renderer.ts + - src/render/garden/gate-renderer.test.ts + - tests/e2e/season1-loop.spec.ts +autonomous: true +requirements: [GARD-01, AEST-07, UX-01] +tags: [gap-closure, uat, css, first-run-hint, tile-contrast, gate-context, mvp] + +must_haves: + truths: + - "G1 closed: has background #1a1a1a + serif color #e8e0d0 + zero margin from frame one — no white halo around the dark canvas (ROADMAP SC1 supplemental — UX-01 lived experience)" + - "G2 closed: after BeginScreen dismisses on first run, a single bible-voice line is visible (e.g. 'Begin where the soil is bare.') from `season1.ui_strings.first_run_hint` in ui-strings.yaml — never hardcoded (CLAUDE.md tone constraint, GARD-01 lived experience)" + - "G2 closed: FirstRunHint auto-dismisses on the first successful plantSeed dispatch and stays dismissed for the session; reload shows it again until first plant (A-Dark-Room first-run-of-this-tab UX, NOT save-state)" + - "G2 closed: `firstRunHintDismissed` lives in src/store/session-slice.ts (NOT in V1Payload — no migrations[2]); resets on hard reload by design" + - "G3 closed: empty-tile outline is brighter than 0x4d4d52 (final value ~0x5a5a60) AND the hover state contrasts the resting state (~0x7a7a82 outline + slight fill alpha bump) so the 4×4 grid reads as legible interactive surfaces against #1a1a1a — no painted assets (Phase 3 deferral preserved, GARD-01 lived experience)" + - "G4 closed: gate-renderer.ts adds a faint vertical wall band (Phaser primitive — alpha 0.15-0.20 against #1a1a1a) at the gate's column connecting top-to-bottom of the canvas, so the gate reads as part of a wall rather than a floating rectangle (Bible 'walled garden', AEST-07 supplemental coverage)" + - "Phase 3 watercolor + cello deferral preserved: every fix uses Phaser primitives or one CSS file. NO painted assets. NO new image loads. NO new npm dependencies." + - "Tests landed for all 4 gaps: src/index.css.test.ts (G1), src/ui/first-run/FirstRunHint.test.tsx (G2), src/render/garden/tile-renderer.test.ts (G3), src/render/garden/gate-renderer.test.ts (G4)." + - "Playwright tests/e2e/season1-loop.spec.ts extended with two assertions: (a) document.body computed style backgroundColor is rgb(26, 26, 26) after navigation, (b) the FirstRunHint line is visible after Begin dismiss, (c) the FirstRunHint is gone after the first plantSeed dispatches." + - "After execution: gsd-verifier handoff is unblocked. The 4 gaps in 02-VERIFICATION.md frontmatter `gaps:` block clear (status gaps_found → verified). The 6 HUMAN-UAT.md tone items remain pending (out of scope for this plan)." + - "All 24 Phase-2 REQ-IDs remain structurally PASS — none of these gap-fix changes regress any existing test (full `npm run ci` exits 0 + Playwright e2e exits 0)." + + artifacts: + - path: src/index.css + provides: "Global page styles — body bg #1a1a1a, color #e8e0d0, zero margin, full viewport height, serif family, #game-container centered. Imported once from src/main.tsx so Vite bundles it into the entry chunk. ~15 lines." + - path: src/index.css.test.ts + provides: "Vitest smoke test asserting the CSS rules are present in the source file (file-read assertion; sufficient for a single-file static stylesheet)." + - path: src/ui/first-run/FirstRunHint.tsx + provides: "FirstRunHint component — renders a single bible-voice line when session.firstRunHintDismissed is false AND session.beginGateDismissed is true; null otherwise. Reads the line from uiStrings[1].first_run_hint (externalized per STRY-09)." + exports: ["FirstRunHint"] + - path: src/ui/first-run/FirstRunHint.test.tsx + provides: "Vitest cases: hidden when beginGateDismissed=false; visible after Begin dismiss; hidden when firstRunHintDismissed=true; renders the externalized string (not hardcoded); auto-dismisses when a plantSeed command commits (selectFirstPlantHasOccurred subscription)." + - path: content/seasons/01-soil/ui-strings.yaml + provides: "first_run_hint: added under the season-1 UI strings tree. Candidate copy ranked per Step 1: 'Begin where the soil is bare.' (Recommended — bible-voice) / 'The soil is waiting.' (alternative — quieter) / 'Click a tile to plant.' (functional fallback)." + - path: src/store/session-slice.ts + provides: "Adds `firstRunHintDismissed: boolean` + `dismissFirstRunHint()` action. Session-state only — NEVER added to V1Payload (per scope_constraint #3 — no migrations[2])." + - path: src/render/garden/tile-renderer.ts + provides: "OUTLINE_COLOR brightened to ~0x5a5a60; OUTLINE_HOVER brightened to ~0x7a7a82; hover adds a slight fill alpha bump on the hit rectangle (no animation noise — pointer-driven, reduced-motion-friendly)." + - path: src/render/garden/tile-renderer.test.ts + provides: "Vitest cases: drawTiles produces 16 tile groups; outline draw call uses OUTLINE_COLOR=0x5a5a60 (assert via mocked Phaser.Scene.add.graphics call args); pointerover handler uses OUTLINE_HOVER=0x7a7a82 (assert via stubbed event)." + - path: src/render/garden/gate-renderer.ts + provides: "drawGate() additionally adds a faint vertical wall band Phaser primitive at the gate's column (x ≈ GATE_X) spanning the canvas height (y=0 → y=768), color GATE_COLOR with alpha ~0.18. Stored on GateGameObjects as `wall: Phaser.GameObjects.Rectangle`." + - path: src/render/garden/gate-renderer.test.ts + provides: "Vitest case: drawGate adds the wall primitive at the expected x with low alpha (assert via mocked Phaser.Scene.add.rectangle call args matching the wall band signature: x near GATE_X, full canvas height, GATE_COLOR, alpha ~0.18)." + - path: tests/e2e/season1-loop.spec.ts + provides: "Three new assertions threaded into the existing PIPE-07 spec: (a) body computed bg is rgb(26, 26, 26) on first nav, (b) FirstRunHint visible after Begin click, (c) FirstRunHint gone after the first plantSeed enqueue." + + key_links: + - from: src/main.tsx + to: src/index.css + via: "import './index.css'; — Vite bundles the CSS into the entry chunk so body styles apply before React mounts" + pattern: "import.*index\\.css" + - from: src/App.tsx + to: src/ui/first-run/FirstRunHint.tsx + via: " mounted alongside Letter/Settings/etc." + pattern: " +**Gap-closure plan. Depends on Plans 02-01..02-05 (all already executed).** + +This plan closes the 4 first-impression UX gaps surfaced by the 2026-05-09 live UAT walkthrough on the dev server. All 24 Phase-2 REQ-IDs are structurally PASS; the test suite cannot detect "what does a new player see on frame one?" — the gaps are minimum-viable functional UX, NOT Phase-3 aesthetic deferrals. + +**Scope discipline (from 02-VERIFICATION.md frontmatter `gaps:` block):** +- G1 BLOCKING — no global page CSS — fix shape: src/index.css ~15 lines. +- G2 BLOCKING — no first-run prompt after Begin — fix shape: tiny FirstRunHint component + session flag. +- G3 HIGH — tile outlines too dim — fix shape: brighten OUTLINE_COLOR + clearer hover in tile-renderer.ts. +- G4 MEDIUM — gate visual stands alone — fix shape: faint vertical wall band primitive in gate-renderer.ts. + +**What this plan must NOT do:** +- No painted assets (Phase 3 watercolor deferral preserved). +- No new npm dependencies (CSS is plain CSS imported via Vite native). +- No V1Payload changes / no migrations[2] (firstRunHintDismissed is session state, not save state). +- No re-litigation of typographic Begin screen tone (HUMAN-UAT.md item 4 covers that — out of scope here). +- No tone judgment on Lura's voice or letter cadence (HUMAN-UAT.md items 1-2 — out of scope). + +**Wave structure: single wave (Wave 0) since all 4 gap-fix tasks are file-disjoint and parallel-safe within the plan.** G1, G2, G3, G4 do not overlap on `files_modified`: +- G1 owns: src/index.css, src/main.tsx, src/index.css.test.ts. +- G2 owns: content/seasons/01-soil/ui-strings.yaml, src/store/session-slice.ts, src/ui/first-run/* (new files), src/ui/index.ts, src/App.tsx. +- G3 owns: src/render/garden/tile-renderer.ts, src/render/garden/tile-renderer.test.ts. +- G4 owns: src/render/garden/gate-renderer.ts, src/render/garden/gate-renderer.test.ts. +- The Playwright e2e extension touches a shared file (tests/e2e/season1-loop.spec.ts) and runs LAST as Task 5 — the integration/verification task. + +**Tasks: 5** (one per gap + a final integration task that extends the e2e and runs full ci). Estimated context cost ~30-40% (well within budget for a gap-closure plan). + + + +Close the 4 first-impression UX gaps that the 2026-05-09 live UAT surfaced. Each fix uses Phaser primitives or a single CSS file — no painted assets, no new dependencies, no V1Payload changes. After execution: the dark canvas no longer floats in a white viewport (G1), a first-time player sees one bible-voice instructional line after Begin dismisses (G2), the 4×4 tile grid reads as legible interactive surfaces with a clearer hover state (G3), and the gate reads as part of a wall via a faint vertical primitive band (G4). + +Purpose: Unblocks `/gsd-verify-work` Phase 2 sign-off. The 4 gaps in 02-VERIFICATION.md frontmatter `gaps:` block clear; the 6 HUMAN-UAT.md tone items remain pending (separate workflow). The Phase-2 vertical slice that "could plausibly ship as a free standalone Season-1 prologue" actually feels like one to a brand-new player — the loop is intuitive on frame one. + +Output: 4 gap fixes + 4 unit tests + 3 e2e assertions threaded into tests/e2e/season1-loop.spec.ts. `npm run ci && npm run test:e2e` exits 0. 02-VERIFICATION.md ready for re-verifier pass to flip status from gaps_found → verified. + + + +@$HOME/.claude/get-shit-done/workflows/execute-plan.md +@$HOME/.claude/get-shit-done/templates/summary.md + + + +@.planning/PROJECT.md +@.planning/ROADMAP.md +@CLAUDE.md +@.planning/anti-fomo-doctrine.md +@.planning/phases/02-season-1-vertical-slice-soil/02-CONTEXT.md +@.planning/phases/02-season-1-vertical-slice-soil/02-PATTERNS.md +@.planning/phases/02-season-1-vertical-slice-soil/02-VERIFICATION.md +@.planning/phases/02-season-1-vertical-slice-soil/02-05-letter-settings-e2e-SUMMARY.md + + + + + +From src/main.tsx (currently 14 lines, no CSS import): +```typescript +import { StrictMode } from 'react'; +import { createRoot } from 'react-dom/client'; +import App from './App.tsx'; + +const rootEl = document.getElementById('root'); +if (!rootEl) { + throw new Error('Root element #root not found in index.html'); +} + +createRoot(rootEl).render( + + + +); +``` + +From index.html (the body has only `
` — Phaser parents to `#game-container` rendered by `` which renders `
` inside `
` from App.tsx): +```html + +
+ + +``` + +From src/game/main.ts (Phaser config — backgroundColor #1a1a1a; parent 'game-container'): +```typescript +const config: Phaser.Types.Core.GameConfig = { + type: Phaser.AUTO, + width: 1024, + height: 768, + parent: 'game-container', + backgroundColor: '#1a1a1a', + scale: { mode: Phaser.Scale.FIT, autoCenter: Phaser.Scale.CENTER_BOTH }, + scene: [Boot, Garden], +}; +``` + +From src/store/session-slice.ts (Plan 02-01 + 02-05 — current SessionSlice): +```typescript +export interface SessionSlice { + beginGateDismissed: boolean; + persistenceToastShown: boolean; + showPersistenceToast: boolean; + letterOverlayOpen: boolean; + pendingLetterEventBlock: unknown | null; + compostBeatTick: number; + // ... actions + dismissBeginGate: () => void; + setPersistenceToastShown: (v: boolean) => void; + setShowPersistenceToast: (v: boolean) => void; + openLetter: (block: unknown) => void; + dismissLetter: () => void; + bumpCompostBeat: () => void; +} +// Default state: beginGateDismissed=false, ... +``` + +From src/App.tsx (Plan 02-05 — currently mounts BeginScreen, SeedPicker, FragmentRevealModal, JournalIcon, LuraDialogue, Letter, Settings, PersistenceToast, CompostToast): +```typescript +return ( +
+ + + + + + + + setSettingsOpen(false)} /> + + + +
+); +``` + +From src/ui/begin/BeginScreen.tsx (Plan 02-02 — single fixed-position dialog covering canvas; dismissed via session.beginGateDismissed; existing pattern to copy for FirstRunHint shape): +```typescript +export function BeginScreen(): JSX.Element | null { + const dismissed = useAppStore((s) => s.beginGateDismissed); + const dismissBeginGate = useAppStore((s) => s.dismissBeginGate); + if (dismissed) return null; + // ... fixed-position dialog with title/subtitle/Begin button +} +``` + +From src/render/garden/tile-renderer.ts (Plan 02-02 — current dim values that G3 fixes): +```typescript +const OUTLINE_COLOR = 0x4d4d52; // ← G3: brighten to 0x5a5a60 +const OUTLINE_HOVER = 0x6e6e75; // ← G3: brighten to 0x7a7a82 +const OUTLINE_ALPHA = 0.6; +// drawTiles creates 16 tile graphics + hit rectangles; pointerover/pointerout +// swap the outline color via drawOutline(). +``` + +From src/render/garden/gate-renderer.ts (Plan 02-04 — current gate primitives; G4 adds a wall band): +```typescript +const GATE_X = 880; +const GATE_Y = 384; +const GATE_COLOR = 0x6e6e75; +const GATE_HIT_W = 80; +const GATE_HIT_H = 120; +// drawGate adds: body (rectangle), glow (rectangle, alpha 0), hit (transparent +// interactive rectangle). G4 adds a 4th: wall (vertical band at GATE_X, +// full canvas height 768, low alpha ~0.18). +``` + +From content/seasons/01-soil/ui-strings.yaml (Plan 02-02 + 02-05 current shape — G2 adds first_run_hint): +```yaml +season: 1 +begin: + title: "The Last Garden" + subtitle: "tend" + cta: "Begin" +seed_picker: + title: "Sow" + cancel: "Not yet" +post_harvest_beat: + - "The earth remembers." + - "Something stayed." + - "It rests where it grew." +journal: + empty_state: "Nothing yet. Plant something." + back: "Close" +settings: + title: "Settings" + export: "Save to a copy" + import: "Restore from a copy" + restore_snapshot: "Earlier garden" + persistence_denied_toast: "The garden may forget, if your browser asks it to." +plants: + rosemary: "Rosemary" + yarrow: "Yarrow" + winter-rose: "Winter-rose" +# ← G2 ADDS: +# first_run_hint: "" +``` + +From src/content/schemas/* (Plan 01-04 — UiStringsSchema validates ui-strings.yaml at build time): +The Zod schema is currently typed for the structure above. Task 2 may need to add `first_run_hint: z.string()` to the schema if strict-typed parsing rejects unknown keys. Read `src/content/schemas/index.ts` to confirm whether the schema is strict (z.object().strict()) or lenient (default — extra keys ignored). Almost certainly lenient, in which case no schema change is needed; if strict, Task 2 must extend the schema. + +From src/store/index.ts (Plan 02-01 + 02-05 barrel — actions exposed to React via useAppStore): +useAppStore is a vanilla zustand store; subscribing to a single field (e.g. `useAppStore(s => s.beginGateDismissed)`) is the canonical pattern. The Garden scene also subscribes via `appStore.subscribe(selector, callback)` for non-React tick-driven repaints. + +From src/sim/garden/commands.ts (Plan 02-02/02-03/02-04 — plantSeed dispatch path): +The store enqueues a command via `enqueueCommand({kind: 'plantSeed', tileIdx, plantTypeId})`. The Garden scene's update loop drains the queue once per tick. Detection of "first plant has occurred" can be done by subscribing to a derived selector — simplest reliable signal: subscribe to `harvestedFragmentIds.length OR tiles.some(t => t?.plant !== null)`. The cleanest path: subscribe to `tiles` and dismiss FirstRunHint the first time any tile has `plant !== null`. (Subscribing to plantSeed enqueues directly is brittle; the queue may apply asynchronously.) + +From tests/e2e/season1-loop.spec.ts (Plan 02-05 — current full-loop spec; Task 5 extends with 3 assertions): +```typescript +test('load → begin → plant → fast-forward → harvest → reveal → journal → reload → persist', async ({ page }) => { + await page.goto('/?devtime=fake'); + await ensureFreshSave(page); + await page.goto('/?devtime=fake'); + // Begin button visible, click it → bootstrap audio → tiles drain. + // ... existing 16-step assertion chain. +}); +``` +Task 5 threads: +- After step 1 (initial nav): assert document.body computed background-color is `rgb(26, 26, 26)`. +- After step 3 (Begin dismissed): assert the FirstRunHint element is visible. +- After step 5/6 (plantSeed enqueued + applied to tile 0): assert the FirstRunHint is gone. + +From .planning/phases/02-season-1-vertical-slice-soil/02-VERIFICATION.md frontmatter `gaps:` block: +- G1: src/main.tsx + no src/index.css; fix CSS body bg #1a1a1a + zero margin + serif color #e8e0d0. +- G2: post-BeginScreen state — no instruction; fix tiny FirstRunHint with copy from ui-strings.yaml; auto-dismiss on first plant; session-state only (no migrations[2]). +- G3: tile outlines too dim — brighten OUTLINE_COLOR (~0x4d4d52 → ~0x5a5a60) + add clearer hover (~0x7a7a82 + slight fill alpha bump). +- G4: gate stands alone — add faint vertical band Phaser primitive (alpha 0.15-0.20 against #1a1a1a) at the gate's column connecting top-to-bottom of canvas. + + + + + + + Task 1 (G1 — BLOCKING): Add src/index.css imported from main.tsx — close the white-halo gap + + - .planning/phases/02-season-1-vertical-slice-soil/02-VERIFICATION.md (frontmatter `gaps:` G1 entry — surface, evidence, fix_shape verbatim) + - src/main.tsx (current 14-line entry — needs an `import './index.css';` line added after the StrictMode/createRoot imports) + - src/game/main.ts (Phaser canvas backgroundColor #1a1a1a — body must match for tonal coherence) + - index.html (currently no `