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>
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>
- 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.
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.
- 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).
- 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.
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.
- 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.
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>
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>
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>
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>