Commit Graph

86 Commits

Author SHA1 Message Date
josh 8e4609ae20 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>
2026-05-09 12:37:17 -04:00
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
josh 47b5b8d6b0 test(02-06): playwright e2e assertions for G1+G2 — phase-2 gap closure complete
Threads 3 new assertions into the existing PIPE-07 spec to verify the
G1 + G2 gap fixes hold end-to-end in a real Chromium browser:

- Assertion A (G1, after initial nav): document.body computed
  background-color is rgb(26, 26, 26) — proves src/index.css bundled
  cleanly into the entry chunk and applies before React mounts.
- Assertion B (G2, after Begin click): the FirstRunHint element
  (data-testid="first-run-hint") is visible with non-empty externalized
  text content from ui-strings.yaml.
- Assertion C (G2, after first plant): the FirstRunHint is gone —
  proves the component's tiles subscription dismisses on the first
  plant !== null transition.

The existing 16-step assertion chain continues to pass unchanged. Test
runtime grew from 1.6s → 1.7s (3 cheap evaluations + 1 visibility +
1 negation). All 4 first-impression UX gaps are now structurally closed.

npm run ci exits 0 (333/333 vitest green; ~21 new tests across G1+G2+G3+G4).
npm run test:e2e exits 0 (Playwright PIPE-07 + 3 new gap-closure assertions).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 12:17:55 -04:00
josh 88adc4f623 fix(02-06,G4): add wall band primitive in gate-renderer — close floating-gate gap
Adds a 4th Phaser primitive to drawGate: a faint vertical wall band
at the gate's column (x=880) spanning the full 768px canvas height
with alpha=0.18 (mid of the 0.15-0.20 fix_shape range). Closes G4
first-impression UX gap from 2026-05-09 live UAT — the gate now reads
as part of a wall rather than a floating gray rectangle, honoring the
bible's "walled garden" framing.

Z-order: wall (behind) → body → glow → hit. The wall band shares the
GATE_COLOR hue (0x6e6e75); the low alpha is what visually distinguishes
the wall (structural context) from the body (the load-bearing element).
WALL_BAND_WIDTH = GATE_HIT_W * 0.55 = 44px — narrower than the gate so
the gate body still reads as the focal point. The wall does NOT pulse;
updateGateIndicator continues to manage only the glow's alpha tween.

Phase 3 watercolor deferral preserved: this is a single Phaser primitive,
no painted texture, no animation. GateGameObjects interface gains a `wall`
field — additive, so the existing Garden.ts consumer continues to work
unchanged (it stores the whole returned object in this.gate).

Vitest: 4 new cases green via Phaser-Scene-mock pattern (constants in
range, first rectangle call has wall geometry, 4 total rectangles, gate
exposes the wall handle). npm run ci exits 0; 333/333 total green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 12:16:42 -04:00
josh ab48c7ef30 fix(02-06,G3): brighten tile outline and hover state — close dim-grid gap
Brightens OUTLINE_COLOR 0x4d4d52 → 0x5a5a60 and OUTLINE_HOVER
0x6e6e75 → 0x7a7a82, plus adds a subtle HOVER_FILL_ALPHA=0.06 fill bump
on the hit rectangle. Closes G3 first-impression UX gap from 2026-05-09
live UAT — the 4×4 grid now reads as legible interactive surfaces
against the #1a1a1a canvas background.

The hover state is pointer-driven steady-state (color + fill alpha
swap), not animation — reduced-motion-safe per Phase 8 ownership of
global motion preferences. Phase 3 watercolor deferral preserved: this
is color values + a fill alpha; no painted assets, no new sprites.

Constants OUTLINE_COLOR / OUTLINE_HOVER are exported so the test pins
the brightened values directly. drawTiles function signature unchanged —
Garden.ts continues to work without modification.

Vitest: 5 new cases green via Phaser-Scene-mock pattern (vi.mock('phaser')
short-circuits the Phaser 4 / happy-dom checkInverseAlpha boot crash;
the mocked scene's add.graphics / add.rectangle capture the call args).
npm run ci exits 0; 329/329 total green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 12:15:11 -04:00
josh c46fc75549 fix(02-06,G2): first-run hint after Begin — close A-Dark-Room first-prompt gap
Adds a single bible-voice line ("Begin where the soil is bare.") that
surfaces immediately after BeginScreen dismisses on first run and
auto-dismisses when the player makes their first plant. Closes G2
first-impression UX gap from 2026-05-09 live UAT — the post-Begin state
no longer leaves a brand-new player staring at a 4×4 grid with no
instruction.

Implementation:
- content/seasons/01-soil/ui-strings.yaml: first_run_hint key added
  (recommended copy from plan; bible voice — warm, specific, contemplative)
- src/content/schemas/ui-strings.ts: UiStringsSchema extended with
  first_run_hint: z.string().min(1) — MANDATORY because Zod default strip
  mode silently drops unknown keys from parsed.data
- src/store/session-slice.ts: firstRunHintDismissed + dismissFirstRunHint
  added (session state ONLY — NOT persisted to V1Payload, no migrations[2])
- src/ui/first-run/FirstRunHint.tsx: subscribes to tiles slice, dismisses
  on first plant !== null transition; renders externalized line via
  uiStrings[1]?.first_run_hint
- src/ui/first-run/{index.ts}, src/ui/index.ts: barrel + re-export wired
- src/App.tsx: <FirstRunHint /> mounted between BeginScreen and SeedPicker

Vitest: 6 new behavioral cases green (hidden when Begin still up, hidden
when dismissed, renders externalized line, reads uiStrings, auto-dismisses
on first plant, stays dismissed on subsequent tile changes). 324/324
total green; npm run ci exits 0.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 12:13:04 -04:00
josh f52de0bdbb fix(02-06,G1): add src/index.css and import from main.tsx — close white-halo gap
Adds src/index.css with body bg #1a1a1a + serif color #e8e0d0 + zero
margin + 100vh min-height + #game-container flex centering, imported
once from src/main.tsx so Vite bundles it into the entry chunk. Closes
G1 first-impression UX gap from 2026-05-09 live UAT — the dark canvas
no longer floats in a sea of white.

Phase 3 (Watercolor & Cello) layers a painted treatment over this base
without changing the structural intent. No new npm dependencies, no
painted assets.

Vitest smoke: 6/6 cases green via file-read assertion (jsdom does not
bundle CSS imports; the Playwright e2e in Task 5 proves end-to-end that
the bundled CSS actually applies in a real browser).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 12:10:18 -04:00
josh 0ed79b0eb1 docs(02-06): plan UAT gap closure (G1-G4)
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) <noreply@anthropic.com>
2026-05-09 12:05:52 -04:00
josh 6f680f4731 test(02): VERIFICATION → gaps_found after live UAT (4 first-impression UX gaps)
User dev-server walkthrough surfaced 4 functional UX gaps beneath the verifier's
24/24 structural PASS:

  G1 (blocking) — no global page CSS → white halo around #1a1a1a canvas
  G2 (blocking) — no first-run prompt after Begin → player confused
                  (A Dark Room rule needs the canonical first-prompt)
  G3 (high)     — tile outlines too dim → grid reads as 'gray check block'
  G4 (medium)   — gate visual at canvas (880, 384) reads as stray rectangle

Phase 3 watercolor deferral preserved — every fix uses Phaser primitives or
one CSS file, not painted assets. STATE.md → status: gaps_found.

Next: /gsd-plan-phase 2 --gaps

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 11:37:44 -04:00
josh 286b4ba446 test(02): persist human verification items as UAT (6 tone/live-loop items)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 11:27:16 -04:00
josh e5d449095d docs(02-05): complete letter-settings-e2e plan
- 02-05-letter-settings-e2e-SUMMARY.md: full plan summary (frontmatter
  + decisions + REQ table + self-check). All 24 Phase-2 REQ-IDs
  structurally satisfied across the 5-plan set.
- STATE.md: marked Plan 02-05 complete; Phase 2 ready for
  /gsd-verify-work; progress 19% → 22%; next action set to verifier.
- ROADMAP.md: Plan 02-05 row marked [x] with duration + SUMMARY ref.
- REQUIREMENTS.md: UX-02 / UX-10 / CORE-03 / PIPE-07 marked complete
  with traceability annotations citing Plan 02-05's contribution;
  per-row Plan 02-05 references added to UX-02, UX-10, CORE-03;
  PIPE-07 traceability table row updated.
2026-05-09 11:16:02 -04:00
josh 31f8ede9ac feat(02-05): wire compost beat toast (Plan 02-04 deferral)
- src/ui/settings/compost-toast.tsx: thin transient toast (D-07,
  GARD-04). Reads a rotating line from uiStrings[1].post_harvest_beat
  on each compost dispatch; fades out after 3.5s. Co-located with
  PersistenceToast as the plan specified ('folded into the persistence-
  toast UI surface').
- src/store/session-slice.ts: compostBeatTick monotonic counter +
  bumpCompostBeat action. Counter (vs boolean) ensures consecutive
  composts re-fire the toast without dedup.
- src/game/scenes/Garden.ts: handleTilePointerDown's compost branch
  calls bumpCompostBeat after enqueueCommand.
- src/App.tsx: mounts <CompostToast />.
- 4 new compost-toast tests; 312/312 vitest green; e2e still 1.6s
  green; npm run ci exits 0.

Implementation choice (per plan 'surfaced in SUMMARY'): minimum-viable
toast surface chosen over the Ink runtime path. The Ink-authored
compost-acknowledgements.ink content remains compiled + runtime-
loadable via loadInkStory + InkRenderer, so a future plan can swap
this component for the richer voice without touching sim or store.
2026-05-09 11:07:43 -04:00
josh dd486969a9 test(02-05): playwright e2e for PIPE-07 — full Phase-2 loop
- tests/e2e/season1-loop.spec.ts: PIPE-07 smoke covering load → Begin
  → plant rosemary → fast-forward FakeClock 3min → harvest →
  fragment-reveal modal → close → journal-icon visible → open journal
  → fragment present → reload page → fragment persists. Sidesteps
  Phaser canvas pixel-clicking via window.__tlgStore command dispatch
  (test-only window slot, production-guarded by import.meta.env.PROD).
- playwright.config.ts: bumped webServer timeout 30s → 60s for cold
  Vite startup; pinned port 5273 + --strictPort to avoid collisions
  with other dev servers on the user's machine; reuseExistingServer
  false so the spec always starts a fresh Vite against this project.
- package.json: added test:e2e script (npx playwright test). Not
  added to npm run ci — Playwright is slower than Vitest; manual run
  before /gsd-verify-work + future v1 release pipeline.
- src/content/loader.ts (Rule 3 — Blocking): replaced gray-matter
  with a 15-line parseFrontmatter helper. gray-matter pulls in Node's
  Buffer global which is undefined in Vite's browser bundle; the
  build emits a 'Module "buffer" externalized' warning that masks
  the runtime ReferenceError. Surfaced under Vite dev mode while
  running the e2e — Plan 02-03's Markdown loader path (lura-first-
  letter.md + winter-rose-night.md) was effectively broken in real
  browsers since shipping. parseFrontmatter handles the strict
  '---<yaml>---<body>' shape the .md fragments use; bundle dropped
  from 2.2MB to 1.9MB as a side effect of dropping the unused dep.
- deferred-items.md: tracks the gray-matter package.json cleanup
  (the dep is now unused but kept in package.json for now, scoped
  out of this plan).
- npx playwright test exits 0 (1 spec, 1.5s test runtime); npm run
  ci exits 0; 308/308 vitest still green. PIPE-07 satisfied
  end-to-end.
2026-05-09 11:04:32 -04:00
josh 5d58d6cc7b feat(02-05): letter overlay + settings UI + boot save lifecycle + clock injection
- src/save/payload.ts (W2): shared buildPayloadFromStore (state, nowMs)
  + hydrateStoreFromPayload (state, payload). Two-arg signature unifies
  Settings.tsx (passes Date.now()) and PhaserGame.tsx saveSync (passes
  clock.now()). BLOCKER 3 — lastTickAt is wall-clock ms, owned by the
  application layer; the sim never writes it.
- src/ui/letter/Letter.tsx + test: D-20 full-screen overlay (UX-02);
  loads compiled letter Ink, binds plants_bloomed/fragment_titles/
  lura_was_here slots from session.pendingLetterEventBlock, dismisses
  via Tend the garden button or backdrop click. Pitfall 9 — dismiss
  calls bootstrapAudioContext synchronously inside the click handler.
- src/ui/settings/Settings.tsx + test: D-28 save-management UI
  (Export/Import/Restore). BLOCKER 2 — Import pipeline is
  importFromBase64 -> unwrap (CRC verify) -> migrate -> hydrate. No
  audio sliders, no a11y polish (Phase 8 owns those).
- src/ui/settings/persistence-toast.tsx: D-30 one-time soft toast in
  voice when navigator.storage.persist() denies. Reads
  showPersistenceToast from session slice; sets persistenceToastShown
  after the timeout fires.
- src/PhaserGame.tsx: full boot path rewrite. Clock selection
  (?devtime=fake URL flag, production-guarded by import.meta.env.PROD),
  save load (BLOCKER 1 — unwrap then migrate), silent offline catchup
  via drainTicks(silent=true), letter overlay open at >=5min absence,
  requestPersistence + showPersistenceToast wiring, Phaser start AFTER
  hydration, registerSaveLifecycleHooks with synchronous LocalStorage
  saveSync (Pitfall 7) + best-effort IDB write. W5 — lifecycle handle
  held in ref so outer cleanup detaches.
- src/store/session-slice.ts: showPersistenceToast transient flag +
  setShowPersistenceToast action.
- src/ui/journal/journal-icon.tsx: 'j' hotkey listener via
  tlg:toggle-journal CustomEvent (D-29).
- src/game/scenes/Garden.ts: formalized clock read via readClockSlot()
  helper; falls back to wallClock if window.__tlgClock missing.
- src/App.tsx: mount Letter, Settings, PersistenceToast, SettingsIcon
  (corner button); D-29 keyboard shortcuts (',' toggles Settings, 'j'
  toggles Journal via window event).
- 308/308 tests green (was 295; +13 new — 7 Letter + 6 Settings).
  npm run ci exits 0; Vite emits letter Ink as a separate lazy chunk.
2026-05-09 10:57:09 -04:00
josh 26eb77a216 feat(02-05): sim/offline + auto-harvest + letter Ink + letter-renderer
- src/sim/offline/: OfflineEventBlockSchema (Zod) + EMPTY_OFFLINE_EVENTS
  + aggregateOfflineEvent pure aggregator (D-19); 14 tests green
- src/sim/garden/auto-harvest.ts: autoHarvestReadyPlants silent-mode
  branch (D-10); reuses harvest() pipeline so selector + Pitfall 10
  unlocks + STRY-10 Lura gate all run identically; BLOCKER 3 invariant
  preserved (no lastTickAt writes); 7 tests green
- simulateOneTick: ctx.silent triggers auto-harvest sweep before tick
  increment; active-play path unchanged (silent defaults false)
- content/dialogue/season1/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 (D-11)
- ink-loader: loadInkStory union extended with letter-from-the-garden;
  separate letterStoryGlob for lazy code-split chunk; INK_VARIABLE_MAP
  gains plants_bloomed / fragment_titles / lura_was_here slots reading
  from session.pendingLetterEventBlock
- src/ui/letter/letter-renderer.ts: pure buildLetterSlots helper —
  prefers fragment first-sentence body for tonal weight, slugified-id
  fallback; 10 tests green
- npm run compile:ink emits 5 .ink.json files (was 4); Vite emits the
  letter as a separate lazy chunk (letter-from-the-garden.ink-*.js)
- 295/295 tests green (was 264; +31 new); npm run ci exits 0
2026-05-09 10:49:59 -04:00
josh de3f55b1c4 docs(02-04): complete lura-gate-beats plan
- 02-04-lura-gate-beats-SUMMARY.md created. Documents:
  - 3 Lura Ink beats authored in bible voice (warmth anchor, contrast not co-griever)
  - Build-time inklecate compile pipeline with bundled binary (BLOCKER 4 mitigated)
  - RESEARCH Assumption A6 verified first-try on Windows
  - 47 new Vitest cases (264/264 total green); npm run ci exits 0
  - sim/narrative gating pure-state; STRY-10 mechanically defended
  - sim/* contains zero inkjs imports; ESLint sim-purity rule still green
  - 4 lazy code-split chunks emitted for compiled Ink JSON
  - Compost-toast UI deferred to Plan 02-05 (folded into persistence-toast surface)
  - 5 auto-fix deviations documented (Rule 1 + Rule 3); 2 tightenings; 0 architectural changes
- STATE.md updated: progress 18% → 19%; Phase 2 plans 3/5 → 4/5; 217 → 264 tests;
  per-phase metrics updated (Phase 2 4/5 plans, ~66min, ~16min/plan).
- ROADMAP.md: Plan 02-04 marked complete with duration; progress table updated.
- REQUIREMENTS.md: STRY-01 / STRY-06 / STRY-07 / STRY-10 marked complete with
  full traceability annotations.

Plan 02-05 (offline catchup + letter + Settings + Playwright e2e) is the only
remaining Phase-2 work.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 10:41:24 -04:00
josh 661f990e9a feat(02-04): Lura dialogue overlay + Ink runtime + gate visual + Garden scene wiring
- src/ui/dialogue/ink-runtime.ts: thin wrapper around inkjs Story —
  nextLine() with text-message cadence (1500ms base + 20ms/char, capped
  at 4000ms), skipDelay() for tap-to-advance, choice surface forwarded
  to ChooseChoiceIndex. Constants exported for Plan 02-05's UX-05
  reduced-motion hook + playtest tuning.
- src/ui/dialogue/ink-runtime.test.ts: 7 cases pinning the cadence
  bounds, skipDelay one-shot semantics, choice forwarding (uses
  vi.useFakeTimers() to validate timing without wall-clock waits).
- src/ui/dialogue/ink-renderer.tsx: drips lines into the DOM as the
  runtime yields them; userSelect: 'text' for MEMR-05 copy-paste;
  click-anywhere skip; choice buttons stop event propagation.
- src/ui/dialogue/LuraDialogue.tsx: D-15 — full-screen DOM overlay
  driven by dialogueOverlayOpen + luraBeatProgress.pending. Loads the
  compiled Ink JSON via loadInkStory, binds variables from store
  snapshot, ChoosePathString into the named knot ('arrival'/'mid'/
  'farewell'), then runs InkRenderer. Close button calls
  resolvePendingLuraBeat to mark visited and clear pending.
- src/ui/dialogue/LuraDialogue.test.tsx: 6 cases — closed-state null,
  dialog renders on open+pending, Close fires resolvePendingLuraBeat
  for all three beats, loadInkStory called with correct beat name +
  knot. Mocks the loadInkStory + ink-runtime layer to keep happy-dom
  out of inkjs internals (Plan 02-05 e2e exercises the live path).
- src/render/garden/gate-renderer.ts: drawGate() + updateGateIndicator()
  — Phaser primitive gate (body / glow / hit) at canvas (880, 384).
  Glow alpha-pulses via Sine-yoyo tween when isPending=true; idempotent.
- src/game/scenes/Garden.ts: gate added in create(); pointerdown
  dispatches setDialogueOverlayOpen(true) only when a beat is pending.
  storeUnsubscribe also drives updateGateIndicator on luraBeatProgress
  changes. update() loop now calls simAdapter.applyLuraProgress when
  the sim's luraBeatProgress differs from the store's so harvests
  trigger the gate indicator. destroy() cleans up the gate's tween.
- src/App.tsx: <LuraDialogue /> mounted as DOM sibling of PhaserGame.
- src/ui/index.ts + src/render/garden/index.ts: re-exports.

13 new tests across dialogue layer; 264/264 total green; npm run ci
exits 0; Vite emits 4 lazy ink-*.js chunks (compiled JSON code-split
per file); ESLint sim-purity rule still green (sim/narrative imports
no inkjs).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 10:33:22 -04:00
josh 7b79d11584 feat(02-04): sim/narrative — Lura beat gating (1/4/8 harvest, STRY-10)
- src/sim/narrative/beat-queue.ts: LuraBeatId / LuraBeatProgress contracts
  matching V1Payload.luraBeatProgress + NarrativeSlice; INITIAL frozen.
- src/sim/narrative/lura-gate.ts: LURA_BEAT_THRESHOLDS = {1: arrival,
  4: mid, 8: farewell}; advanceLuraBeatProgress / resolvePendingLuraBeat /
  isLuraBeatPending — pure, no inkjs import, no Date.now (sim-purity rule
  green). The gate counts harvest events, never wall-clock time, so STRY-10
  holds.
- src/sim/narrative/lura-gate.test.ts: 17 cases including the load-bearing
  STRY-10 case (24 hours of FakeClock advance with 0 harvests leaves all
  flags + pending false). Pitfall 10 boundaries pinned at 3/4/5 and 7/8/9.
  pending-set-already + already-visited carry-throughs covered.
- src/sim/garden/commands.ts: harvest() now calls advanceLuraBeatProgress
  AFTER the harvest commit (Pitfall 10 — same-tick boundary). The new
  luraBeatProgress field flows through the returned SimState and into the
  store via the existing Garden.update() path.
- src/sim/garden/commands.test.ts: +5 cases pinning the harvest → beat
  gate edges (1st→arrival, 4th→mid, 8th→farewell, between-threshold
  no-fire, pending preservation when player hasn't visited).
- src/sim/index.ts: re-export ./narrative.

67/67 sim tests green; npm run lint + build exit 0.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 10:27:06 -04:00
josh c90f8f1e5c feat(02-04): ink compilation pipeline + 4 authored Season-1 Ink files + runtime loader
- scripts/compile-ink.mjs: build-time inklecate runner using bundled binary
  (BLOCKER 4 — uses node_modules/inklecate/bin, not stale -windows/-mac path strings).
  Assumption A6 verified first-try on Windows; the same binary path resolution
  works on macOS + Linux per the wrapper's own getInklecatePath convention.
- scripts/compile-ink.test.mjs: 3 Vitest cases proving the compiler runs +
  emits valid JSON with inkVersion. wipe=false for the test path so it can
  run in parallel with the ink-loader test without racing on the wipe step.
- 4 Season-1 .ink files authored in voice (Lura warmth-anchor, gardener-keeper
  for compost): lura-arrival.ink, lura-mid.ink, lura-farewell.ink,
  compost-acknowledgements.ink (rewrite of Plan 02-03 scaffolded version into
  VAR-driven branch shape consumable by the runtime).
- src/content/ink-loader.ts: loadInkStory + bindGardenStateToInk +
  INK_VARIABLE_MAP. Centralized snake_case slot mapping per Pitfall 4. UTF-8
  BOM stripped before Story instantiation.
- src/content/ink-loader.test.ts: 8 cases — Story instantiation for all 4
  beats, fragment_count binding, Pitfall 4 snake_case enforcement, silent
  skip for stories missing declared vars.
- package.json: build now runs compile:ink first; ci chain runs compile:ink
  before test so ink-loader.test.ts's precondition check passes.
- .gitignore: src/content/compiled-ink/ excluded (regenerated on every build).

npm run ci exits 0; 11 new tests green (228 total).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 10:24:40 -04:00
josh 348c76a537 docs(02-03): complete harvest-journal-fragments plan
Plan 02-03 (Wave 1 second plan) executed in sequential mode. 3 atomic
commits + this metadata commit:

  - f192e82 — Season-1 fragments + sim/memory selector + harvest/compost
  - 572c861 — journal + reveal modal + harvest pointer wiring
  - 39bfcd2 — scripts/check-bundle-split.mjs (PIPE-02 structural verifier)

Outcomes:
  - 217/217 tests green (was 163; +54 new this plan)
  - npm run ci exits 0 with check:bundle-split integrated AFTER build
  - GARD-03 / GARD-04 / MEMR-01..06 / PIPE-02 satisfied end-to-end
  - The full Season-1 active-play loop works on real authored content:
    plant → grow → ready → click → harvest (deterministic, gated, no-dup,
    sentinel fallback for Pitfall 8) → reveal modal pops with full text →
    close → fragment files into journal under Season 1 → journal icon
    appears (D-23 first-harvest gate, invisible before) → click opens
    full-screen Memory Journal grouped by Season (D-24, MEMR-05 selectable
    DOM)
  - 17 Season-1 fragments authored in bible voice (9 warm + 3
    contemplative + 2 heavy + 1 _meta sentinel + 2 long-form Markdown)
  - Plant-type unlock thresholds finalized (Plan author's discretion
    within D-05): rosemary @ 0 / yarrow @ 3 / winter-rose @ 6. Pitfall 10
    boundaries pinned (locked at 2/5, unlocked at 3/6).
  - Pool-exhaustion sentinel chosen over repeat-most-recent — preserves
    no-dup invariant; warm pool depth ≥9 makes the sentinel structurally
    unreachable in normal Phase-2 play
  - compost-acknowledgements.ink content shipped ahead of Plan 02-04's
    Ink runtime; Garden.ts has TODO at the wiring point
  - PIPE-02 structurally verified by scripts/check-bundle-split.mjs (Vitest-
    importable Node ESM with runCheck() export)

Phase 2 progress: 3/5 plans complete (Wave 0 + both Wave 1 plans). Wave 2
(02-04 lura-gate-beats + 02-05 letter-settings-e2e) is the only remaining
Phase-2 work.

SUMMARY at .planning/phases/02-season-1-vertical-slice-soil/02-03-harvest-journal-fragments-SUMMARY.md.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 10:16:02 -04:00
josh 39bfcd2032 chore(02-03): scripts/check-bundle-split.mjs (PIPE-02 structural verification)
Task 3 of Plan 02-03: ship the PIPE-02 structural assertion that Season-1
content reaches the build output. Three structural checks (any one
sufficient): chunk filename slug match (fragments / season1 / 01-soil),
chunk-contents reference to /content/seasons/01-soil/ source path or to
known fragment ids, and a future-extension hook for index.html manifest
inspection.

Phase 2 ships eager-corpus loading alongside the lazy
loadSeasonFragments surface, so currently chunkContentMatch=true via
inlined ?raw content. When Plan 02-04+ switches consumers to lazy-only
and Vite emits a separate Season-1 chunk, chunkNameMatch will also
start passing — at which point either path satisfies the assertion. The
plumbing is structurally proven now; the chunk-naming side is documented
as the path of least resistance for the Phase-4 Season-2 onboarding.

scripts/check-bundle-split.mjs:
- Refactored body into export function runCheck() returning a structured
  result; the CLI invocation guard wraps process.exit so Vitest can
  import the module without termination (verified by the test file).

scripts/check-bundle-split.test.mjs:
- 3 cases: file exists, parses + imports without process.exit firing,
  runCheck() returns the documented {ok, message, chunkNameMatch,
  chunkContentMatch, files} shape. The on-disk dist/-required happy path
  fires via the package.json scripts.ci chain (`npm run build &&
  npm run check:bundle-split`).

package.json:
- New `check:bundle-split` script.
- `ci` chain extended: lint → test → validate:assets → build →
  check:bundle-split. dist/ is populated by build before the bundle-split
  assertion runs.

`npm run ci` exits 0 end-to-end. 217/217 tests green (was 214; +3 new
this task). The PIPE-02 verification step now refuses any future change
that breaks the lazy-content plumbing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 10:07:36 -04:00
josh 572c86192f feat(02-03): journal + reveal modal + harvest pointer wiring
Task 2 of Plan 02-03: ship the Memory Journal UI surfaces (D-23/D-24/D-25)
and wire harvest + compost pointer events through the Garden scene to the
sim → store → React reveal flow.

src/ui/journal/ (new module):
- Journal.tsx — full-screen modal (D-24); fragments grouped by Season;
  DOM-rendered text with userSelect: text per MEMR-05; reads
  harvestedFragmentIds from the store; resolves ids against the eager
  `fragments` corpus (defensive — unresolvable ids skip silently).
- FragmentRevealModal.tsx — D-25 active-play reveal modal; backdrop click
  + inner Close button dismiss; event.stopPropagation on the article
  body so clicking inside the text doesn't dismiss; defensive silent
  dismiss on unresolvable id.
- journal-icon.tsx — D-23 + D-29 corner affordance; gated by
  selectJournalRevealed (`harvestedFragmentIds.length > 0`); local open
  state (no store pollution); 'j' hotkey deferred to Plan 02-05.
- index.ts — barrel.
- 16 new Vitest cases across 3 test files (Journal: 7 / FragmentRevealModal:
  6 / journal-icon: 3); all green.

src/App.tsx — mount FragmentRevealModal + JournalIcon as DOM siblings of
PhaserGame.

src/ui/index.ts — re-export ./journal.

src/game/scenes/Garden.ts — harvest/compost pointer flow:
- create() builds a SimContext from the eager `fragments` corpus filtered
  to Season 1; passed to every simulateOneTick call (Phase 4+ should swap
  to await loadSeasonFragments(currentSeason) when Season transitions land).
- handleTilePointerDown branches on tile state: empty → seed picker
  event; ready plant → enqueue 'harvest' command; immature plant → enqueue
  'compost' command (TODO Plan 02-04: render the Ink-authored compost
  acknowledgement beat from compost-acknowledgements.ink).
- update() detects newly-appended harvestedFragmentIds and sets
  fragmentRevealId so the reveal modal pops with the new fragment text.
- BLOCKER 3 invariant preserved — sim writes tickCount, never lastTickAt.

content/dialogue/season1/compost-acknowledgements.ink — authored content
for the GARD-04 + D-07 compost beat. 6 short lines in the gardener-keeper
voice (NOT Lura — she's the warmth anchor; the garden's voice is the
contrast). Plan 02-04 wires the inkjs runtime; Plan 02-03 ships the
content so the writer can iterate independently.

214/214 tests green (was 163; +51 new this plan); npm run lint exits 0;
npm run ci exits 0; npm run build exits 0 with the expected
INEFFECTIVE_DYNAMIC_IMPORT warnings (eager `fragments` export still
imports the Season-1 yaml/md statically alongside the lazy
loadSeasonFragments path — documented in 02-02-SUMMARY.md).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 10:05:45 -04:00
josh f192e8298c feat(02-03): Season-1 fragments + sim/memory selector + harvest/compost commands
Task 1 of Plan 02-03: ship Season-1 authored content + the deterministic
fragment selector + extend sim/garden/commands.ts with harvest + compost.

Content (≥17 Season-1 fragments under /content/seasons/01-soil/):
- 14 in fragments.yaml (9 warm / 3 contemplative / 2 heavy + 1 _meta sentinel)
- 2 long-form Markdown fragments (lura-first-letter.md, winter-rose-night.md)
- Pool depth (W6): warm pool ≥9 satisfies the worst-case all-rosemary
  playthrough at the 8th-harvest Lura threshold (CONTEXT D-14)
- All ids match /^season1\.[a-z0-9._-]+$/ (FragmentSchema regex; CLAUDE.md
  stable-string-ID rule); bible voice maintained throughout

FragmentSchema extension (back-compat — tags is optional):
- Optional `tags: z.array(z.string()).optional()` for tonal-register gating
- Reserved tag `_meta` excludes the exhaustion sentinel from the normal pool

src/sim/memory/ (new module):
- pool.ts — filterPool() pure helper (Season + tonal-register + no-dup gates)
- selector.ts — selectFragment() deterministic + mulberry32 PRNG +
  EXHAUSTION_FALLBACK_ID for Pitfall 8 fallback
- selector.test.ts — 16 tests covering gating / no-dup / determinism /
  sentinel-fallback / sentinel-exclusion-from-normal-pool
- index.ts — barrel; src/sim/index.ts re-exports

src/sim/garden/commands.ts (extended):
- harvest() pure command — empties tile, appends one fragment id,
  re-computes unlockedPlantTypes (Pitfall 10: thresholds checked AFTER
  the harvest commit). Refuses immature plants and OOR indices.
- compost() pure command — empties tile regardless of stage; no fragment
  yield (D-07); no resource refund (D-04 = infinite seeds).
- SimContext interface — application-layer-injected (fragments, currentSeason)
- simulateOneTick() takes optional ctx (default empty pool); harvest/compost
  branches added to the kind switch.
- BLOCKER 3 invariant preserved — sim writes tickCount, never lastTickAt.

Plant-type unlock thresholds (CONTEXT D-05, plan author's discretion):
- rosemary @ count 0 (start)
- yarrow @ count 3 (third harvest)
- winter-rose @ count 6 (sixth harvest)

commands.test.ts: +18 new cases (harvest / compost / Pitfall 10 boundary
on yarrow + winter-rose / sentinel fallback / immutability). 65/65 tests
green across src/sim/memory + src/sim/garden + src/content; lint exits 0;
build green (Vite parses all 17 fragments without schema violation).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 10:00:38 -04:00
josh d052a35478 docs(02-02): complete begin-plant-grow plan
- Add 02-02-begin-plant-grow-SUMMARY.md (3 atomic commits, 35 new tests, BLOCKER 3 invariant honored, RESEARCH Assumption A5 verified, deviations documented)
- STATE.md — advance to Plan 02-02 complete (2/5 plans in Phase 2; 9 total plans complete)
- ROADMAP.md — mark 02-02 plan complete; phase progress 2/5
- REQUIREMENTS.md — mark GARD-01, GARD-02, AEST-07, UX-01 complete (with traceability comments)
2026-05-09 09:50:05 -04:00
josh 414a554549 feat(02-02): begin screen + seed picker + ui-strings + lazy content split
- content/seasons/01-soil/ui-strings.yaml: player-visible Phase-2 copy externalized per CLAUDE.md (Begin / seed picker / post-harvest beat / journal / settings / plant display names); voice reviewed against bible + anti-fomo-doctrine.md
- content/seasons/01-soil/fragments.yaml: placeholder Season-1 fragment file (Plan 02-03 expands to ≥10 authored)
- content/seasons/00-demo/: deleted (Phase-1 demo replaced)
- src/content/schemas/ui-strings.ts: UiStringsSchema (Zod) — validates structure of every season's ui-strings.yaml at load time
- src/content/schemas/index.ts + src/content/index.ts: re-export UiStringsSchema/UiStrings
- src/content/loader.ts: eager `uiStrings` glob + PIPE-02 lazy `loadSeasonFragments(seasonId)` (Plan 02-03+ exploit)
- src/ui/begin/use-audio-bootstrap.ts: bootstrapAudioContext() lazy-creates + resumes (RESEARCH Pattern 9; Pitfall 5 mitigation — context construction inside the gesture for iOS Safari) + installFirstInteractionGestureHandler() one-shot for D-22 returning players + __resetAudioBootstrapForTest()
- src/ui/begin/BeginScreen.tsx: D-21 typographic Begin screen — title + subtitle + CTA from uiStrings[1].begin; onClick calls bootstrapAudioContext synchronously inside the click event then dismisses the session gate (D-22)
- src/ui/begin/BeginScreen.test.tsx: 4 tests — render / D-22 skip / click bootstraps + dismisses / subtitle string
- src/ui/garden/SeedPicker.tsx: D-02 inline DOM popover; subscribes to 'tile-clicked-coords'; renders one button per unlocked plant type from uiStrings[1].plants; click enqueues plantSeed command via store.enqueueCommand
- src/ui/garden/SeedPicker.test.tsx: 6 tests — initial-null / coords-positioned / unlocked-only / enqueue / dismiss / multi-plant; mocks game/event-bus to avoid Phaser canvas init under happy-dom (deviation Rule 3)
- src/ui/{begin,garden,index}.ts: barrels
- src/App.tsx: mount BeginScreen + SeedPicker as overlay siblings to PhaserGame
- src/PhaserGame.tsx: bootstrap unlockedPlantTypes=['rosemary'] for first-run; install gesture handler + scene-ready listener
- npm run ci exits 0; 163/163 tests pass (10 new this commit + 25 from Task 1 + 128 baseline)
2026-05-09 09:43:47 -04:00
josh 537016b48f feat(02-02): render layer + Garden scene + scheduler integration
- src/render/garden/tile-coords.ts: GRID_LAYOUT (96px tiles + 16px gap, centered in 1024×768) + tileTopLeftCanvas / tileCenterCanvas / tileCenterToDom helpers (RESEARCH Pattern 4 + Assumption A5: handles Phaser.Scale.FIT scaling via canvas getBoundingClientRect)
- src/render/garden/tile-renderer.ts: drawTiles(scene) — 16 outlined rounded rectangles with hover state (D-06)
- src/render/garden/plant-renderer.ts: drawPlant(scene, idx, tile, stage) — primitive shapes per stage (sprout dot / mature stem / ready bloom) tinted by plant type (D-26); destroyPlant cleanup
- src/render/garden/ready-pulse.ts: applyReadyPulse alpha-cycle tween for ready stage (D-27)
- src/render/garden/index.ts + src/render/index.ts: barrels
- src/game/scenes/Garden.ts: Phaser Garden scene wires scheduler ↔ store ↔ render. update() loop drains commands → simulateOneTick → simAdapter.applyTilesAndUnlocks. appStore.subscribe drives reactive plant repaint (Pitfall 6 mitigation: subscribe, not read-once-in-create). pointerdown on empty tile emits 'tile-clicked-coords' for the React seed picker. BLOCKER 3 invariant: lastTickAt is read-through from store (NOT seeded with this.currentTick).
- src/game/main.ts: scene registry now [Boot, Garden]
- src/game/scenes/Boot.ts: create() transitions to Garden
- Lint clean; build clean; 153/153 tests pass (Task-1 sim/garden tests + all baseline)
2026-05-09 09:36:09 -04:00
josh e82a11b988 feat(02-02): sim/garden — types, plants table, growth state machine, plantSeed
- src/sim/garden/types.ts: Tile/PlantInstance/PlantType/PlantTypeId/GrowthStage interfaces; tileIdx(row,col) + tileCoords(idx) + emptyTiles() helpers (RESEARCH Pitfall 2 canonical row*4+col encoding)
- src/sim/garden/plants.ts: 3 Season-1 plants per D-03 (rosemary 600t / yarrow 900t / winter-rose 1500t — D-08/D-09 2–5min band) with placeholder tints (D-26)
- src/sim/garden/growth.ts: advanceGrowth() pure function — Sprout (0%) → Mature (33%) → Ready (100%); Math.max clamp on negative deltas defends Pitfall 1 system-clock rewind
- src/sim/garden/commands.ts: plantSeed (D-05 unlock-gate + occupied silent no-op + immutability) + simulateOneTick (BLOCKER 3 — writes tickCount, NEVER lastTickAt) + tileGrowthStage helper
- src/sim/garden/index.ts: barrel
- src/sim/index.ts: re-export garden barrel
- 25 new tests (11 growth + 14 commands) — all green; lint clean; build green
- ESLint sim-purity rule from Plan 02-01 confirms zero Date.now/setInterval call sites under src/sim/garden/
2026-05-09 09:32:59 -04:00
josh 38535bac73 docs(02-01): complete foundations plan
- 02-01-foundations-SUMMARY.md authored with frontmatter dependency
  graph, key-files manifest, decisions log, patterns established,
  test-count breakdown (72 new tests), TICK_MS=200 (no drift), and
  ESLint sim-purity rule landed (defended-option clause did not trigger)
- STATE.md: Phase 2 progress 1/5 plans (Wave 0 complete);
  velocity table updated with Plan 02-01 ~12min entry; decisions log
  cites BLOCKER 3 split, V1Payload extension, ESLint rule
- ROADMAP.md: Phase 2 row updated to 1/5; 02-01 plan marked [x] with
  duration + summary backlink
- REQUIREMENTS.md: CORE-02, CORE-03, CORE-11, UX-10, UX-11 marked
  complete with annotations; traceability table updated

Plan execution metrics:
- 3 atomic commits (58db532, fe99058, 2a8d354)
- 72 new tests across 9 test files (cushion above plan estimate of 54)
- Total test count: 128/128 green
- npm run ci exits 0
- Duration: ~12 min (sequential mode)
2026-05-09 09:26:37 -04:00
josh 2a8d354b58 chore(02-01): eslint sim-purity rule + Date.now violator fixture
- eslint.config.js block 3: no-restricted-syntax bans Date.now() and
  setInterval() inside src/sim/**, with src/sim/scheduler/clock.ts as
  the single allowed wall-clock owner (CONTEXT D-33, RESEARCH Pitfall 1)
- date-now-violator.ts deliberate-violation fixture (excluded from
  default lint by Block 1's top-level ignores; the programmatic ESLint
  test passes ignore: false to override)
- lint-firewall.test.ts gains 2 new cases: positive (rule fires on
  violator) + negative (rule does NOT fire on clock.ts the one exception)
- Existing CORE-10 firewall test left untouched and remains green
2026-05-09 09:20:44 -04:00
josh fe99058040 feat(02-01): Zustand store + V1Payload extension + save lifecycle hooks
- Zustand 5 vanilla createStore composes 4 slices (garden / memory /
  narrative / session); useAppStore React hook re-renders on selector
  change; getState() works without React (Phaser ↔ React bridge per D-32)
- simAdapter exposes drainCommands / applyTilesAndUnlocks /
  applyHarvestedFragments / applyLuraProgress / applyTickCount; sim
  never imports the store (CORE-10)
- V1Payload extended in place per D-34: tickCount (BLOCKER 3 monotonic
  counter), unlockedPlantTypes, luraBeatProgress, offlineEvents,
  settings.persistenceToastShown — CURRENT_SCHEMA_VERSION stays 1, no
  migrations[2] sneaked in (regression-defense test pins this)
- migrations[1] populates all new field defaults; tickCount: 0 means
  fresh sims always start at sim-tick 0
- registerSaveLifecycleHooks (UX-10): visibilitychange→hidden,
  beforeunload, plus saveOnSeasonTransition() — Vitest covers all three
- Phaser EventBus singleton seeded per the Phaser 4 React-template pattern
- Install @testing-library/react as devDep so the React-hook test can
  exercise the real renderHook surface
- 27 new tests across store / migrations / lifecycle all green; full
  npm run ci is 126/126
2026-05-09 09:18:43 -04:00
josh 58db53227c feat(02-01): BigQty + scheduler + sim foundations
- Install zustand@^5.0.0 + break_eternity.js@^2.1.3 as dependencies
- BigQty immutable wrapper around Decimal (D-31): factories,
  arithmetic, comparison, JSON round-trip, saturating coercion
- formatHumanReadable for K/M/B/T/scientific HUD readouts (UX-11)
- Clock interface + wallClock + FakeClock — only file in src/sim/
  allowed to read Date.now() (D-33)
- drainTicks fixed-timestep accumulator: refuses negative deltas
  (CORE-11), clamps at MAX_OFFLINE_MS=24h (CORE-03), TICK_MS=200
- computeOfflineCatchup pure descriptor for offline boundaries
- SimState root shape with BLOCKER 3 split: lastTickAt
  (wall-clock, app-layer-only) + tickCount (sim-internal monotonic)
- 52 tests across big-qty / format / clock / tick / catchup all green
2026-05-09 09:14:10 -04:00
josh 5ddaabcdc1 docs(02): cite D-12, D-16, D-32 in plan must_haves + record planning complete
ci / lint + test + validate-assets + build (push) Successful in 9m39s
Decision-coverage gate found three CONTEXT.md decisions structurally
implemented but not literally cited by their D-NN tags. Added one-line
must_haves entries citing each:

- D-12 (Lura as discrete gate visits, 3 beats this Season) → 02-04
- D-16 (all Lura dialogue authored in Ink, runtime via inkjs) → 02-04
- D-32 (Zustand 5 store as the Phaser↔React bridge; sim never imports
  store, CORE-10 enforced) → 02-01

STATE.md flipped from in_progress (context gathered) to ready_to_execute
with the planning summary in stopped_at.

All 24 REQ-IDs + 34 D-XX decisions now covered.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 03:19:44 -04:00
josh a641056364 fix(02): plan revision iter 3 — BLOCKER 3 cross-plan regression + W1/W2
BLOCKER 3 — cross-plan regression: Plans 02-03 and 02-05 BOTH re-author
src/sim/garden/commands.ts but had reverted simulateOneTick to the old
defective return shape (`return { ...next, lastTickAt: currentTick };`).
Wave 1's execution of 02-03 would overwrite 02-02's correct version,
breaking the invariant for the entire phase.

  - 02-03: simulateOneTick return now matches 02-02 line 457 exactly:
    `return { ...next, tickCount: next.tickCount + 1 };`
  - 02-05: same fix for the silent-mode update (Step 6).
  - 02-03 acceptance_criteria: add negative grep
    (`! grep -E "lastTickAt:\s*(this|currentTick)" src/sim/garden/commands.ts`)
    and positive grep (`grep -q "tickCount: next.tickCount" ...`).
  - 02-05 acceptance_criteria: add the same two greps for commands.ts so
    02-05's silent-mode edits cannot silently re-introduce the regression.

W1 — App.tsx import: 02-05 Step 11 used `useEffect` without importing it.
Combined `import { useState }` and `import { useRef }` into a single
`import { useState, useEffect, useRef } from 'react';` line.

W2 — helper arity divergence: Settings.tsx (one-arg, Date.now() inline)
and PhaserGame.tsx (two-arg, clock.now() injected) had two parallel
definitions of buildPayloadFromStore / hydrateStoreFromPayload. Fix:

  - New Step 3.5 introduces `src/save/payload.ts` with the unified
    two-arg signature: `buildPayloadFromStore(state, nowMs)` and
    `hydrateStoreFromPayload(state, payload)`.
  - `src/save/index.ts` re-exports both.
  - Settings.tsx imports from save barrel; passes Date.now() at the
    call site (no clock injection on hand).
  - PhaserGame.tsx imports from save barrel; passes clock.now() (the
    injected wallClock or FakeClock).
  - Inline duplicate definitions in both files removed; replaced with
    a comment pointing to the shared module.
  - files_modified updated to include src/save/payload.ts.
  - acceptance_criteria asserts: shared file exists, both helpers
    exported, both consumers import from save barrel, no inline
    duplicate definitions remain.

VALIDATION.md not updated — no `<automated>` verify command changed;
the new greps live inside `<acceptance_criteria>` (executor-checked
per task), and VALIDATION.md is not present in the phase dir.

All iteration-1 + iteration-2 fixes preserved; no regressions.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 03:15:39 -04:00
josh d065922cad revise(02): mark Open Questions RESOLVED + tidy GrowthStage import order
- W1: 02-RESEARCH.md Open Questions section now flagged (RESOLVED) and each
  Recommendation prefixed with RESOLVED + a pointer to the artifact that
  codified the resolution.
- W8: 02-02 Plan example moves `import type { GrowthStage }` to the top of
  commands.ts (alongside the other type-only imports) and drops the trailing
  parenthetical apology — the executor doesn't need to fix anything.
2026-05-09 03:05:51 -04:00
josh e5c55b0aae revise(02): BLOCKER 3 — split lastTickAt (wall-clock) from tickCount (sim counter)
Two distinct fields with strict separation:
  - lastTickAt: wall-clock milliseconds. Written ONLY at saveSync time by
    the application layer. The sim NEVER writes this field.
    computeOfflineCatchup uses it as the wall-clock anchor.
  - tickCount: monotonic sim-internal counter (one per simulate() call).
    Used for STRY-10 narrative gating that must be immune to wall-clock
    manipulation. The sim writes this field; the application layer reads
    it via simAdapter.applyTickCount.

Changes:
  02-01: SimState + V1Payload gain `tickCount: number`; migrations[1]
  defaults to 0; GardenSlice exposes tickCount + lastTickAt + setters;
  simAdapter exposes applyTickCount; tests assert the round-trip.
  02-02: simulateOneTick increments next.tickCount + 1 (not lastTickAt:
  currentTick); Garden scene's SimState snapshot reads lastTickAt
  through from store and writes tickCount: this.currentTick locally;
  acceptance_criteria forbids `lastTickAt: this.*` in the sim and scene.
  02-05: buildPayloadFromStore now persists tickCount (from store);
  hydrateStoreFromPayload restores it via state.setTickCount.

This unblocks the offline-catchup math: computeOfflineCatchup(payload.lastTickAt,
nowMs) now reliably reads wall-clock ms because the sim never overwrites it
with a tick counter.
2026-05-09 03:04:45 -04:00
josh a9f190ed27 revise(02-05): fix migrate() bypass in boot+import paths + lifecycle leak + hotkey
- BLOCKER 1: PhaserGame.tsx boot path now runs unwrap(env) → migrate(raw, env.schemaVersion).
  Casting unwrap(record.envelope) directly to V1Payload silently accepted any
  future-shape payload as the current shape; only migrate() walks the schema
  version chain.
- BLOCKER 2: Settings.tsx onImport now correctly orders importFromBase64 →
  unwrap (CRC verify) → migrate. Previous code discarded migrate's result
  and then read v1.payload as if unwrap returned an envelope rather than
  the payload itself — runtime crash on every import.
- BLOCKER 3: documented the lastTickAt invariant as wall-clock milliseconds,
  written ONLY at saveSync time (never by the sim). Added acceptance_criteria
  greps proving (a) saveSync writes clock.now(), (b) Garden scene does not
  overwrite lastTickAt with a tick counter, (c) sim/garden/Garden.ts (if it
  exists; the Garden scene actually lives at src/game/scenes/Garden.ts)
  contains no lastTickAt: this.* writes.
- W2: D-29 keyboard shortcut wired in App.tsx — comma toggles Settings,
  'j' dispatches a window CustomEvent the JournalIcon picks up.
- W5: lifecycle handle now stored in useRef and detached in the OUTER
  useLayoutEffect cleanup (the previous IIFE-internal return was a closure
  return, never reaching React's effect cleanup contract).
2026-05-09 03:01:27 -04:00
josh 953784ae93 revise(02-03): bump warm-pool fragment count + journal hotkey listener
- W6: warm-tagged pool depth raised to ≥9 (8th-harvest threshold + 1 buffer)
  so a worst-case all-rosemary playthrough never exhausts. Total per-pool
  targets: ≥9 warm, ≥3 contemplative, ≥3 heavy, plus the sentinel.
- W2: JournalIcon now listens for the 'tlg:toggle-journal' window event so
  App.tsx can wire a 'j' hotkey without lifting open/close state into the
  store. Hotkey is gated on the same revealed selector as the icon itself.
2026-05-09 03:01:12 -04:00
josh f6bef061c3 revise(02-04): replace in-test compileAllInk() call with precondition check
Per W9: invoking the compiler from inside ink-loader.test.ts's beforeAll
creates a filesystem race against other concurrent tests because the
script wipes src/content/compiled-ink/ at start. Compile is already part
of the npm run ci chain (via npm run build); the test should only verify
the artefact exists and fail loudly with a fix-it message otherwise.
2026-05-09 02:57:15 -04:00
josh f7428da299 revise(02-04): fix inklecate binary path + drop unused .ink + add last_fragment_title slot
- BLOCKER 4: inklecateBinary() now resolves node_modules/inklecate/bin/inklecate{,.exe};
  the previous path (inklecate-windows/, inklecate-mac/, inklecate-linux/) does not
  exist in the package — fallback verification of Assumption A6 would have masked
  the wrapper-API failure.
- W3: removed lura-greeting-template.ink from files_modified (never authored).
- W4: added last_fragment_title to INK_VARIABLE_MAP (extracts first sentence of the
  most-recently-harvested fragment) so the must_haves slot promise is fulfilled.
2026-05-09 02:56:50 -04:00
josh 63d2d8d5f7 docs(02): create phase 2 plan — 5 plans across 3 waves
Phase 2 (Season 1 Vertical Slice — Soil) plan set:
- 02-01 (Wave 0): foundations (BigQty + Zustand 5 store + tick scheduler + V1Payload extension + save lifecycle hooks + Phaser EventBus + ESLint sim-purity rule)
- 02-02 (Wave 1, parallel): Begin → Plant → Grow vertical slice
- 02-03 (Wave 1, parallel): Harvest → Journal → Compost + Season 1 fragments + PIPE-02 verification
- 02-04 (Wave 2, parallel): Lura's 3 Ink-authored gate beats (1st/4th/8th harvest, STRY-10)
- 02-05 (Wave 2, parallel): Letter + Settings + boot-path save lifecycle + Playwright PIPE-07 e2e

All 24 Phase-2 REQ-IDs covered across the plan set. VALIDATION.md per-task verification map filled (15 tasks); nyquist_compliant: true.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 02:45:56 -04:00
josh 5bc98ba4ac docs(02): map phase 2 file targets to existing analogs
54 file targets classified (49 new, 5 modified) with 87% analog coverage.
Key patterns: V1Payload extension (not v1→v2 migration), per-layer
public barrel pattern, test colocation, Zustand vanilla store + Phaser
EventBus singleton as the dual sim↔React bridge, ESLint sim-purity rule
proposed as a defended option (not auto-locked, per minimum-viable bias).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 02:11:01 -04:00
josh 01e02dcdb8 docs(phase-02): add validation strategy
Nyquist VALIDATION.md scaffold for Phase 2. Defines test infrastructure
(Vitest + Playwright already wired by Phase 1), sampling rates (npm test
after each commit, npm run ci after each wave), Wave-0 dependency surface
(BigQty + scheduler + Zustand store + V1Payload extension), and three
manual-only verifications (AudioContext cross-browser, letter voice review,
cozy-pace playtest). The per-task verification map is intentionally empty —
the planner fills it during plan generation; nyquist_compliant flips to
true once it's complete.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 02:03:17 -04:00
josh c4589a56b4 docs(02): research phase 2 vertical slice — 24 REQ-IDs mapped
5-plan MVP slice proposal across 3 waves: Wave 0 lands the three
deferred foundations (BigQty wrapper around break_eternity.js, Zustand
5 store wiring, tick scheduler / monotonic clock). Wave 1 ships two
parallel vertical slices (Begin+Plant+Grow, Harvest+Journal+Fragments).
Wave 2 ships the Lura gate-visit slice and the offline-letter slice
including Playwright PIPE-07 e2e. All 24 REQ-IDs addressed in the
coverage map; 10 architectural patterns enumerated; tick rate locked at
5Hz with 24h offline cap; AudioContext.resume() bootstrap pattern
documented for first-run + returning-player paths; V1Payload extension
shape locked per CONTEXT D-34 (no migrate_v1_to_v2 added). 10
assumptions logged, 8 are LOW risk; 2 are MEDIUM (canvas-DOM coord
mapping under FIT scale, inklecate Windows binary invocation) and
flagged for early verification in Plans 02-02 and 02-04.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 02:01:22 -04:00
josh 350e976fed docs(state): record phase 2 context session
Phase 2 discuss-phase complete; STATE.md now reflects context-gathered
status, updated stopped_at narrative, and next-action pointer to
/gsd-plan-phase 2.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 01:39:52 -04:00
josh 69964ba17f docs(02): capture phase context
Phase 2 (Season 1 Vertical Slice — Soil) discuss-phase output. 02-CONTEXT.md
captures 34 implementation decisions across 8 gray areas (garden geometry &
input · time density · Lura's arc · letter composition · Begin screen ·
Memory Journal · plant placeholders · Phase-2 Settings UI scope). Locks the
4×4 grid + click+inline seed picker, 2–5min growth band per plant with
auto-harvest-while-offline, 3 Lura gate visits gated by 1st/4th/8th harvest,
authored Ink letter skeleton with templated slots, full-screen letter on
≥5min absence, tasteful placeholder Begin screen (Phase 3 paints), full-
screen Journal modal revealing after first harvest, simple Phaser-primitive
plants with subtle ready-state pulse, save-management-only Settings.
Phase 2's first commits: BigQty wrapper, Zustand 5 store, tick scheduler.
Save schema is a v1 *extension*, not v1→v2.

02-DISCUSSION-LOG.md preserves the alternatives considered for human review.

Next: /gsd-plan-phase 2

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 01:39:48 -04:00
josh 49ba411a00 verify(01): phase 1 verification — all 16 REQ-IDs PASS, CI green
ci / lint + test + validate-assets + build (push) Successful in 9m38s
All 16 Phase-1 requirements verified against the live codebase:
- CORE-01, CORE-04..CORE-10: scaffold builds; save layer (IDB + fallback,
  envelope, migrations, snapshots, persist, codec) all green (53 tests / 12 files)
- PIPE-01: Vite-native content pipeline; schema violation fails build
- PIPE-03: asset provenance gate; refused-sample fixture proves gate structure
- PIPE-05: both doctrine docs authored + 8 doc-lint assertions green
- PIPE-06: ci.yml runs npm run ci on every push + PR
- AEST-08: ProvenanceSchema 6 fields; CI gate on every commit
- AEST-09: human curation gate in place; IOU records Path C deferral to Phase 5
- STRY-09: /content/ convention established (vacuously satisfied in Phase 1)
- UX-13: anti-fomo-doctrine.md authored + review-enforced

Deferred (non-blocking): 10-20 real north-star images (AEST-09 Task 2) — Phase 5.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-09 00:17:20 -04:00
josh d3410e207d feat(01-05): commit 2 placeholder north-star assets + IOU (Path C deferral)
User invoked planning-doctrine pushback principle on the 10-20 north-star
curation step. Two 1x1 transparent-PNG placeholders ship with provenance
sidecars marked model_id: 'placeholder' so the validator exercises at >0
assets. Full deferral rationale and resolution path in
.planning/phases/01-foundations-and-doctrine/01-05-IOU.md — to be revisited
at Phase 5 entry (curate then, or amend CONTEXT D-01 if still ceremonial).
2026-05-09 00:06:27 -04:00
josh 8ace3db7b4 docs(01-07): complete ci-workflow plan + Phase 1 closure metadata
- 01-07-ci-workflow-SUMMARY.md: structural enforcement map, Phase 2/8 handoff notes, threat T-01-08 mitigation confirmed
- 01-VALIDATION.md: per-task table populated (12/13 green, 01-05-T2 partial — checkpoint:human-verify awaiting north-star image curation); status flipped to executed
- ROADMAP.md: progress table marks Phase 1 as 7/7 with 01-05 partial annotation
- STATE.md: position advanced to Plan 7 of 7 complete; performance trend; Plan 01-05 Task 2 explicitly tracked as the only outstanding deliverable; next action = human curation pass then /gsd-verify-work
- REQUIREMENTS.md: PIPE-06 marked complete (CI workflow runs Vitest on every push/PR)
2026-05-09 00:00:23 -04:00
josh 609d58231d ci(01-07): minimum-viable GitHub Actions workflow running npm run ci on push + PR (PIPE-06)
- Single-job workflow at .github/workflows/ci.yml (~49 lines including load-bearing comments)
- runs-on: ubuntu-latest, timeout-minutes: 10
- Uses actions/setup-node@v4 with cache: 'npm' (per RESEARCH CI Pitfall A — never cache node_modules/)
- Node 22 (per RESEARCH § Environment Availability)
- Triggers on push to main and pull_request to main
- Steps: checkout → setup-node → npm ci (lockfile-strict) → npm run ci (lint + test + validate-assets + build)
- Per CONTEXT user pushback: NO matrix, NO test reporters, NO Codecov, NO release automation
- Local npm run ci exits 0 (53 tests passing across 12 files); workflow will be green on push
- Structurally enforces every Phase 1 success criterion on every commit going forward
2026-05-08 23:54:30 -04:00
josh bbaa2c6905 fix(01): remove unused eslint-disable in save/db.ts
01-02's flat config does not enable no-console, so 01-03's directive
landed as a max-warnings=0 violation after parallel-wave merge.
2026-05-08 23:50:16 -04:00