Files
josh 7f39cf6d31 docs(02-06): complete uat-gap-closure plan
5 tasks executed sequentially; all 4 first-impression UX gaps from
2026-05-09 live UAT structurally closed (G1 BLOCKING white halo, G2
BLOCKING no first-run prompt, G3 HIGH dim tile grid, G4 MEDIUM floating
gate). 21 new Vitest cases (312 → 333 green); 3 new Playwright assertions
(16 → 19); npm run ci + npm run test:e2e both exit 0. Phase 3 watercolor
deferral preserved (no painted assets, no new dependencies); V1Payload
unchanged (firstRunHintDismissed is session-state only, no migrations[2]).

Hint copy chosen: "Begin where the soil is bare." (plan's #1 ranked
candidate; bible voice — warm, specific, contemplative). Externalized in
content/seasons/01-soil/ui-strings.yaml; UiStringsSchema extended with
first_run_hint: z.string().min(1) so Zod strip mode does not silently
drop the YAML key from parsed.data.

Verifier handoff unblocked: 02-VERIFICATION.md frontmatter `gaps:` block
ready to flip status from gaps_found → verified. The 6 HUMAN-UAT.md tone
items remain pending (out of scope; addressed by separate workflow).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 12:21:42 -04:00

21 KiB
Raw Permalink Blame History


phase: 02-season-1-vertical-slice-soil plan: 06 subsystem: uat-gap-closure tags: [gap-closure, uat, css, first-run-hint, tile-contrast, gate-context, mvp, wave-0]

Dependency graph

requires:

  • phase: 02-01 provides: Zustand store + V1Payload + session slice (extended in this plan with firstRunHintDismissed)
  • phase: 02-02 provides: BeginScreen (analog component for FirstRunHint shape) + tile-renderer (G3 modifies its constants) + ui-strings.yaml shape
  • phase: 02-03 provides: JournalIcon (analog corner-affordance pattern) + Journal modal
  • phase: 02-04 provides: gate-renderer (G4 adds wall band primitive) + Lura gate location at canvas (880, 384)
  • phase: 02-05 provides: tests/e2e/season1-loop.spec.ts (Playwright PIPE-07 full-loop smoke — Task 5 threads 3 new gap-closure assertions into it) + App.tsx render tree (FirstRunHint mounts alongside Letter / Settings / etc.) provides:
  • src/index.css — global page styles (body bg #1a1a1a, color #e8e0d0, zero margin, 100vh, serif, #game-container flex centering). Imported once from src/main.tsx so Vite bundles it into the entry chunk; body styles apply before React mounts.
  • src/ui/first-run/FirstRunHint.tsx — single-line bible-voice hint surfaced after BeginScreen dismisses, auto-dismisses on first plant !== null transition. Reads externalized line from uiStrings[1]?.first_run_hint per STRY-09.
  • src/store/session-slice.ts extended — firstRunHintDismissed: boolean + dismissFirstRunHint() action. Session state ONLY; NEVER added to V1Payload (no migrations[2]).
  • src/content/schemas/ui-strings.ts extended — UiStringsSchema gains first_run_hint: z.string().min(1) so Zod's default strip mode does NOT silently drop the YAML key from parsed.data at runtime.
  • content/seasons/01-soil/ui-strings.yaml — first_run_hint key added with bible-voice copy "Begin where the soil is bare." (the plan's #1 ranked candidate; rationale documented in §Decisions Made).
  • src/render/garden/tile-renderer.ts — OUTLINE_COLOR brightened 0x4d4d52 → 0x5a5a60 + OUTLINE_HOVER 0x6e6e75 → 0x7a7a82 + HOVER_FILL_ALPHA=0.06 fill bump on the hit rectangle. Constants exported for testability.
  • src/render/garden/gate-renderer.ts — adds 4th Phaser primitive (wall band) at GATE_X column spanning the full 768px canvas height with alpha=0.18 (mid of 0.15-0.20 fix_shape range). GateGameObjects interface gains a wall field — additive, Garden.ts unchanged.
  • tests/e2e/season1-loop.spec.ts — extended with 3 new assertions covering G1 + G2 end-to-end (body bg = rgb(26, 26, 26), FirstRunHint visible after Begin dismiss, FirstRunHint gone after first plant).
  • 21 new Vitest cases across 4 test files (G1: 6 file-read smoke, G2: 6 behavioral, G3: 5 phaser-mocked, G4: 4 phaser-mocked).
  • 4 first-impression UX gaps from 2026-05-09 live UAT structurally closed (G1 BLOCKING, G2 BLOCKING, G3 HIGH, G4 MEDIUM). affects: [/gsd-verify-work (re-verifier consumes this SUMMARY to flip 02-VERIFICATION.md status from gaps_found → verified), Phase 3 (Watercolor & Cello — paints over the structural primitives without changing the layout intent)]

Tech tracking

tech-stack: added: [] patterns: - "Single CSS file imported from main.tsx as the global-style anchor: Vite bundles plain CSS imports natively, no build-config change needed. body bg + color + font-family all match the BeginScreen overlay so there is no tonal break at any frame." - "Session-state-only first-run gate: firstRunHintDismissed lives in src/store/session-slice.ts (NOT V1Payload). The hint reappears on hard reload until the first plantSeed commits — that is the correct A-Dark-Room first-run UX (re-prompt on a fresh tab, dismiss on first action)." - "Schema extension MANDATORY when adding YAML keys: Zod's default z.object() strip mode silently drops unknown keys from parsed.data. Without extending UiStringsSchema, the runtime would have rendered first_run_hint as undefined and FirstRunHint would have rendered null — production-only failure that unit tests mocking the store directly would not catch." - "Phaser-mock pattern for renderer unit tests: vi.mock('phaser', ...) short-circuits the Phaser bundle import so the renderer module loads under happy-dom (Phaser 4's checkInverseAlpha boot probe crashes on canvas.getContext returning null). Combined with a mocked Phaser.Scene surface (add.graphics + add.rectangle returning vi.fn() spies), the test pins constants and call args without needing a real Chromium canvas. Reusable for plant-renderer and ready-pulse coverage in future phases." - "Tile hover as steady-state outline + fill swap (NOT animation): pointerover swaps OUTLINE_COLOR → OUTLINE_HOVER and bumps the hit rectangle's fill alpha from 0 → 0.06; pointerout reverses. No tweens, no setInterval. Reduced-motion-safe by construction; Phase 8's global motion-preference owner has no work to do here." - "Gate wall band as structural primitive (Phase 3 deferral preserved): a single Phaser Rectangle at GATE_X with alpha=0.18 spans the canvas height to give the gate visual context (the bible's 'walled garden' framing). Phase 3 paints the watercolor wall over this primitive without changing the geometry or interaction surface."

key-files: created: - src/index.css (G1 — global page styles, ~25 lines) - src/index.css.test.ts (G1 — 6 file-read smoke cases pinning the load-bearing CSS rules) - src/ui/first-run/FirstRunHint.tsx (G2 — single-line hint component, externalized copy, auto-dismiss subscription) - src/ui/first-run/FirstRunHint.test.tsx (G2 — 6 behavioral cases: hidden when Begin still up, hidden when dismissed, renders externalized line, reads uiStrings, auto-dismisses on first plant, stays dismissed) - src/ui/first-run/index.ts (G2 — barrel) - src/render/garden/tile-renderer.test.ts (G3 — 5 cases via Phaser-Scene-mock pattern: constants pinned, 16 tile groups, initial draw uses OUTLINE_COLOR, pointerover swaps to OUTLINE_HOVER + fill bump) - src/render/garden/gate-renderer.test.ts (G4 — 4 cases via Phaser-Scene-mock pattern: constants in fix_shape range, wall is first rectangle with full canvas height, 4 total rectangles, GateGameObjects exposes wall handle) - .planning/phases/02-season-1-vertical-slice-soil/02-06-uat-gap-closure-SUMMARY.md (this file) modified: - src/main.tsx (G1 — single import './index.css'; line added) - src/store/session-slice.ts (G2 — firstRunHintDismissed + dismissFirstRunHint added; session state only, NOT in V1Payload) - src/content/schemas/ui-strings.ts (G2 — UiStringsSchema gains first_run_hint: z.string().min(1) so Zod strip mode does not drop the YAML key) - content/seasons/01-soil/ui-strings.yaml (G2 — first_run_hint key with bible-voice copy) - src/ui/index.ts (G2 — re-exports ./first-run) - src/App.tsx (G2 — mounted between BeginScreen and SeedPicker) - src/render/garden/tile-renderer.ts (G3 — OUTLINE_COLOR + OUTLINE_HOVER brightened, HOVER_FILL_ALPHA=0.06 added; constants exported) - src/render/garden/gate-renderer.ts (G4 — wall band primitive added; WALL_BAND_X / WALL_BAND_WIDTH / WALL_BAND_HEIGHT / WALL_BAND_ALPHA / WALL_BAND_COLOR exported; GateGameObjects gains wall field) - tests/e2e/season1-loop.spec.ts (Task 5 — 3 new assertions threaded into PIPE-07 happy path: body bg, FirstRunHint visible after Begin, FirstRunHint gone after first plant)

decisions:

  • id: 02-06-D1 decision: "First-run hint copy: 'Begin where the soil is bare.' (plan's #1 ranked candidate)" rationale: "CLAUDE.md tone constraint says player-visible copy must match the bible's voice — warm, specific, intermittent, sometimes funny, sometimes devastating. Of the three ranked candidates, #1 has all four bible markers — soil + bare are specific and contemplative; the imperative 'Begin' echoes the BeginScreen CTA without redundancy; the construction is intermittent (one beat, no follow-on). #2 ('The soil is waiting.') is quieter but more elliptical for a brand-new player on frame one. #3 ('Click a tile to plant.') is the functional fallback and would only be chosen if HUMAN-UAT review surfaced #1 as too elliptical. The plan's recommended choice was #1 and there was no reason to deviate."
  • id: 02-06-D2 decision: "Session-state for firstRunHintDismissed (NOT V1Payload — no migrations[2])" rationale: "Plan scope_constraint #3 (also CLAUDE.md hard constraint). The hint is a first-run-of-this-tab affordance, like A Dark Room's '...the room is empty' or '...the fire is dead' surfaces. The player should see it again if they hard-reload before planting; once they plant it stays down for the session. Persisting to save would (1) require migrations[2] which Phase 1 has shipped zero v1 saves to migrate forward and is structurally premature; (2) force a one-time 'permanent dismissal' UX that loses the cozy re-onboarding signal across reloads. Session state is the cleaner shape."
  • id: 02-06-D3 decision: "UiStringsSchema extended with first_run_hint: z.string().min(1) — schema edit was MANDATORY, not optional" rationale: "Zod's default object mode is 'strip' — unknown keys parse SUCCESSFULLY but are SILENTLY DROPPED from parsed.data. Without the schema edit, content/seasons/01-soil/ui-strings.yaml could carry the first_run_hint key but uiStrings[1].first_run_hint would be undefined at runtime, FirstRunHint would render null in production, and only the unit tests that mock the store directly would catch it. The plan's Step 2 calls this out explicitly. The edit is one line in src/content/schemas/ui-strings.ts; the cost of skipping it is a production-only failure mode that unit tests cannot detect. .min(1) defends against an accidental empty-string in YAML."
  • id: 02-06-D4 decision: "Phaser-mock pattern via vi.mock('phaser') for tile-renderer + gate-renderer tests" rationale: "First attempt at tile-renderer test imported the source file directly; Phaser 4's checkInverseAlpha boot probe (canvas.getContext('2d') returning null under happy-dom) crashed the test setup. The plan acknowledged this risk via the SeedPicker mock pattern reference. vi.mock('phaser', () => ({ default: {} })) at module top short-circuits the bundle load entirely; the test then mocks the Scene's add.graphics + add.rectangle surface to capture call args. For gate-renderer, BlendModes.ADD is mocked as the sentinel value 1 so setBlendMode receives a non-undefined argument. The pattern is reusable for plant-renderer + ready-pulse coverage in future phases."
  • id: 02-06-D5 decision: "WALL_BAND_ALPHA = 0.18 (mid of the 0.15-0.20 fix_shape range)" rationale: "The plan's fix_shape says alpha 0.15-0.20. 0.18 is the mid of that range — low enough that the gate body remains the visual focal point (the load-bearing element), high enough that the wall actually reads against the #1a1a1a canvas. Lower (0.15) would be invisible at the edge of the gate; higher (0.20) would compete with the body. Phase 3 paints over without changing this geometry."

metrics: duration: ~30 min (5 tasks: ~6 min/task average; G1 fastest at ~3 min; G2 longest at ~10 min due to 7-step shape) completed: 2026-05-09 tests-added: 21 (was 312 → 333) tests-green: 333/333 e2e-assertions-added: 3 (was 16 → 19) e2e-runtime: 1.7s (was 1.6s — 0.1s growth from 3 cheap evaluations + 1 visibility + 1 negation) ci-runtime: ~30s (lint + compile:ink + 333 vitest + validate:assets + build + check:bundle-split) bundle-size: 1.9MB (unchanged — no new dependencies, no new image assets) commits: 5 (one per task; conventional-commit format with fix(02-06,GN): / test(02-06): scopes)

requirements-completed: [GARD-01, AEST-07, UX-01]

Phase 2 Plan 06: UAT Gap Closure (G1G4) Summary

Closed the 4 first-impression UX gaps that the 2026-05-09 live UAT walkthrough surfaced — the dark canvas no longer floats in a white viewport (G1), a first-time player sees a single bible-voice instructional line after Begin dismisses (G2), the 4×4 tile grid reads as legible interactive surfaces (G3), and the gate has structural wall context instead of floating as a stray gray rectangle (G4). All fixes use Phaser primitives or one CSS file; Phase 3 watercolor deferral preserved.

Tasks Executed

# Gap Severity Files Commit Tests
1 G1 — white halo BLOCKING src/index.css, src/main.tsx, src/index.css.test.ts f52de0b 6 file-read smoke
2 G2 — no first-run prompt BLOCKING content/seasons/01-soil/ui-strings.yaml, src/content/schemas/ui-strings.ts, src/store/session-slice.ts, src/ui/first-run/{FirstRunHint.tsx, FirstRunHint.test.tsx, index.ts}, src/ui/index.ts, src/App.tsx c46fc75 6 behavioral
3 G3 — dim tile grid HIGH src/render/garden/tile-renderer.ts, src/render/garden/tile-renderer.test.ts ab48c7e 5 phaser-mocked
4 G4 — floating gate MEDIUM src/render/garden/gate-renderer.ts, src/render/garden/gate-renderer.test.ts 88adc4f 4 phaser-mocked
5 Integration tests/e2e/season1-loop.spec.ts 47b5b8d 3 e2e assertions

Hint Copy Chosen

Begin where the soil is bare.

This is the plan's #1 ranked candidate (recommended). Rationale documented in decision 02-06-D1: bible voice (warm + specific + contemplative), echoes the BeginScreen CTA without redundancy, intermittent construction (one beat, no follow-on). The candidate was committed unchanged; no deviation from the plan's recommendation.

Test & Gate Results

  • Vitest: 312 → 333 (+21 new cases) — 333/333 green.
  • Playwright e2e: 16 → 19 assertions (+3 gap-closure) — 1.6s → 1.7s runtime; 1 passed in 4.7s end-to-end.
  • npm run ci: Exit 0 (lint + compile:ink + 333 vitest + validate:assets + build + check:bundle-split).
  • npm run test:e2e: Exit 0 (Playwright PIPE-07 with all 3 new assertions green).
  • Bundle size: 1.9MB unchanged — no new dependencies, no new image assets.
  • V1Payload: Unchanged — firstRunHintDismissed is session-state only; migrations[2] does NOT exist; no migrations.ts edits.

Constraint Compliance Confirmation

Constraint Verification Status
No painted assets (Phase 3 watercolor deferral) git diff main~5 HEAD -- '*.png' '*.jpg' '*.webp' is empty
No new npm dependencies git diff main~5 HEAD -- package.json package-lock.json is empty
firstRunHintDismissed is session-state, not save-state grep -c firstRunHintDismissed src/save/migrations.ts = 0
No migrations[2] entry grep -E 'migrations\[2\]\s*=' src/save/migrations.ts returns no match
Hint copy externalized (not hardcoded) grep -L "Begin where the soil is bare|The soil is waiting|Click a tile to plant" src/ui/first-run/FirstRunHint.tsx matches the file (i.e. the candidate strings do NOT appear in the component)
UiStringsSchema extended for first_run_hint grep -E 'first_run_hint:\s*z\.string\(\)' src/content/schemas/ui-strings.ts matches
Tile outline brightened to 0x5a5a60 / 0x7a7a82 tile-renderer.ts exports OUTLINE_COLOR=0x5a5a60 + OUTLINE_HOVER=0x7a7a82 (old hex literals appear ONLY in comment annotations documenting the change, not in active code paths)
Wall band alpha in 0.15-0.20 range gate-renderer.ts exports WALL_BAND_ALPHA=0.18
Wall band height = canvas height gate-renderer.ts exports WALL_BAND_HEIGHT=768
Sim purity preserved (no edits in src/sim/**) git diff main~5 HEAD -- 'src/sim/**' is empty
No motion-only affordances Tile hover is pointer-driven steady-state (color + alpha swap, no tweens); wall band is steady-state alpha, no pulse

Gap Closure Evidence (vs 02-VERIFICATION.md frontmatter gaps: block)

Gap Fix verification
G1 white halo src/index.css exists with the 6 required rules (body bg #1a1a1a, color #e8e0d0, margin 0, min-height 100vh, serif, #game-container flex). src/main.tsx imports it (line 4). Playwright Assertion A confirms document.body.backgroundColor === 'rgb(26, 26, 26)' from frame one in real Chromium.
G2 no first-run prompt src/ui/first-run/FirstRunHint.tsx exists; mounted in App.tsx between BeginScreen and SeedPicker. content/seasons/01-soil/ui-strings.yaml carries first_run_hint: "Begin where the soil is bare.". src/store/session-slice.ts carries firstRunHintDismissed + dismissFirstRunHint. src/content/schemas/ui-strings.ts extended with first_run_hint: z.string().min(1). Playwright Assertion B confirms the hint is visible after Begin click; Assertion C confirms it auto-dismisses after the first plantSeed lands. 6 unit tests pin behavior.
G3 dim tile grid src/render/garden/tile-renderer.ts exports OUTLINE_COLOR=0x5a5a60 (was 0x4d4d52) + OUTLINE_HOVER=0x7a7a82 (was 0x6e6e75) + HOVER_FILL_ALPHA=0.06 (new). 5 unit tests pin the constants and the pointerover behavior via Phaser-Scene-mock.
G4 floating gate src/render/garden/gate-renderer.ts exports WALL_BAND_X=880 + WALL_BAND_HEIGHT=768 + WALL_BAND_ALPHA=0.18 + WALL_BAND_COLOR=0x6e6e75 + WALL_BAND_WIDTH=44. drawGate adds the wall as the first rectangle (z-order: behind body / glow / hit). GateGameObjects exposes the new wall handle. 4 unit tests pin constants in fix_shape range + first-rectangle geometry + 4-rectangle count + wall handle exposure.

Deviations from Plan

None — plan executed exactly as written.

The plan's 5 tasks landed in order with no Rule 1-4 deviations triggered. The plan's anticipated risks were all addressed by the plan's own structure:

  • Phaser 4 / happy-dom incompatibility (G3 + G4 tests) — plan called out via SeedPicker analog reference; Phaser-mock pattern (vi.mock('phaser', () => ({ default: {} }))) landed cleanly first try.
  • Schema-strip mode silently dropping unknown keys (G2 plan Step 2) — plan called out as MANDATORY; schema edit landed first try, content/loader.test.ts continued green.
  • Garden.ts integration breakage from additive GateGameObjects.wall field — plan called out as risk; verified by reading Garden.ts line 110 (this.gate = drawGate(this)) which stores the whole returned object so the additive field is structurally safe; npm run ci confirmed end-to-end.

Auth Gates

None — the plan introduces no auth surfaces; all 5 tasks ran fully autonomously.

Decisions Made

(Captured in frontmatter decisions: block above — 02-06-D1 through 02-06-D5.)

Handoff to Verifier

The 4 gap entries in .planning/phases/02-season-1-vertical-slice-soil/02-VERIFICATION.md frontmatter gaps: block are structurally closed. The verifier (gsd-verifier) consumes this SUMMARY + re-runs verification to flip status from gaps_foundverified.

Out of scope for this plan (carried forward):

  • 6 HUMAN-UAT.md tone items (Lura voice in the .ink files, letter cadence, Begin tonal feel, ≥5min absence flow, gate visual indicator + LuraDialogue overlay flow). These are inherently subjective and remain pending. They are addressed by the user's tone-review workflow at the next merge / playtest, not by code.
  • 3 INEFFECTIVE_DYNAMIC_IMPORT build warnings (inherited from Plan 02-02's eager-corpus + lazy-glob co-existence). Phase 4+ resolves these when consumers move to lazy-only.
  • gray-matter package.json cleanup (tracked in .planning/phases/02-season-1-vertical-slice-soil/deferred-items.md).

REQ-IDs reinforced (not flipped — those were already structurally PASS in 02-VERIFICATION.md):

  • GARD-01 (supplemental — first-frame legibility of the planting affordance)
  • AEST-07 (supplemental — tonal coherence between body and canvas; Begin dismissal lands on a guided rather than empty surface)
  • UX-01 (supplemental — first-run prompt presence honors the A-Dark-Room rule the bible cites)

The Phase-2 vertical slice that "could plausibly ship as a free standalone Season-1 prologue" now actually feels like one to a brand-new player on frame one.

Self-Check: PASSED

All claimed files exist:

  • src/index.css ✓
  • src/index.css.test.ts ✓
  • src/ui/first-run/FirstRunHint.tsx ✓
  • src/ui/first-run/FirstRunHint.test.tsx ✓
  • src/ui/first-run/index.ts ✓
  • src/render/garden/tile-renderer.test.ts ✓
  • src/render/garden/gate-renderer.test.ts ✓

All claimed commits exist (verified via git log --oneline -8):

  • f52de0b fix(02-06,G1): add src/index.css and import from main.tsx ✓
  • c46fc75 fix(02-06,G2): first-run hint after Begin ✓
  • ab48c7e fix(02-06,G3): brighten tile outline and hover state ✓
  • 88adc4f fix(02-06,G4): add wall band primitive in gate-renderer ✓
  • 47b5b8d test(02-06): playwright e2e assertions for G1+G2 ✓

Final gates:

  • npm run ci: exit 0, 333/333 vitest green ✓
  • npm run test:e2e: exit 0, 1 passed in 4.7s ✓
  • No new npm deps: ✓
  • V1Payload unchanged: ✓
  • No painted assets: ✓