Commit Graph

54 Commits

Author SHA1 Message Date
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
josh 0f192ca3c0 chore: merge executor worktree (01-06 doctrine-docs) 2026-05-08 23:48:35 -04:00
josh 2aa61d030e chore: merge executor worktree (01-05 asset-provenance, partial — Task 2 awaits human curation) 2026-05-08 23:48:30 -04:00
josh 4cc3d8dbd2 chore: merge executor worktree (01-03 save-layer) 2026-05-08 23:48:25 -04:00
josh c3289440d6 chore: merge executor worktree (01-04 content-pipeline) 2026-05-08 23:48:15 -04:00
josh 161be69d7b chore: merge executor worktree (01-02 eslint-firewall) 2026-05-08 23:48:11 -04:00
josh 13139547f7 docs(01-03): complete save layer plan
7 commits across 3 TDD tasks (RED + GREEN per task) + .gitkeep cleanup;
36 Vitest tests across 7 test files green; npm run build clean under
TypeScript strict; all 6 CORE requirements (CORE-04 through CORE-09)
covered by at least one assertion.

Key structural decision documented in SUMMARY: SaveDB is a single
common-contract interface, not a union of IDBPDatabase | LocalStorageDBAdapter.
The union shape failed TypeScript-strict at the build gate; the interface
refactor isolates the type-system cast to one location at openSaveDB().
2026-05-08 23:47:01 -04:00
josh d4c519c38d chore(01-03): remove src/save/.gitkeep (firewall marker no longer needed)
src/save/ now contains 7 production files + 7 test files. The .gitkeep
firewall marker exists only to make empty directories trackable in git;
it can be retired once the directory has real content (per Plan 01-01
SUMMARY's pattern — 'firewall-as-directory pattern').
2026-05-08 23:42:13 -04:00
josh 2761bcc1e0 feat(01-03): Base64 codec + DoS-capped import + index re-exports + SaveDB interface refactor [GREEN]
- codec.ts: exportToBase64 / importFromBase64 via lz-string with
  MAX_IMPORT_BYTES=50MB DoS cap (T-01-02 in plan threat model); import
  validates against SaveEnvelopeSchema before returning. lz-string sync
  caveat documented per RESEARCH Pitfall 5 (Web Worker mitigation deferred
  to Phase 8 per CONTEXT D-09)
- index.ts: 14 public re-exports — the only entry point Phase 2 should
  import from. Includes the LocalStorageDBAdapter class so consumers can
  type-check the fallback path explicitly if needed

[Rule 1 - Bug] Build was failing because the original SaveDB type was a
union (IDBPDatabase<SaveDBSchema> | LocalStorageDBAdapter) — TypeScript
cannot resolve method calls through a union when each branch has
differently-shaped overloads ('no compatible signature' on every db.put).
Fixed by:
  - Defining SaveDB as a single common-contract interface that both
    backends MUST satisfy (get/put/delete/getAll/transaction with
    conditional-type RecordOf<S> return values)
  - Hoisting the canonical SavedRecord/SnapshotRecord/StoreName types
    into db-localstorage-adapter.ts (lower-level module) and re-exporting
    them from db.ts to avoid a circular import
  - Casting the idb-returned IDBPDatabase to SaveDB at the open-call
    boundary (the casts are isolated to openSaveDB; Phase 2 only sees
    the SaveDB interface)
  - Promoting SnapshotEntry to a type-alias of SnapshotRecord so
    snapshots.ts no longer redeclares the shape and can rely on
    canonical types

Tests: 36/36 pass under 'npx vitest run src/save/' (full suite incl
sentinel: 37/37). 'npm run build' exits 0 under TypeScript strict.
'npm run lint' is not invoked here because Plan 02 (eslint-firewall) has
not landed yet — the lint script will fail until it does, by design per
the Plan 01-01 SUMMARY ('Plan 02 owns it').
2026-05-08 23:42:00 -04:00
josh de39c1b7c3 docs(01-02): complete eslint-firewall plan
SUMMARY documents the ESLint flat config, the 9 element types, the
single CORE-10 rule, the deliberate-violation fixture, the Vitest test
that runs ESLint programmatically against the violator, and the four
auto-fixed deviations (typescript-eslint parser-only integration,
real render target file for the violator import, eslint-import-resolver-
typescript wiring, tsconfig.app.json test-file exclusion).

Verifies: npm run lint -> 0 errors / 0 warnings; npm test -> 2/2 pass;
npm run build -> green; eslint --no-ignore on violator -> exits 1.

Self-Check: PASSED.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 23:38:19 -04:00
josh bec0df1dc2 test(01-03): add failing tests for Base64 codec + full round-trip [RED]
- round-trip.test.ts (3 tests): full pipeline EXPORT -> IMPORT -> MIGRATE
  -> WRAP -> UNWRAP -> IDB PUT -> IDB GET exercising every save layer
  file end-to-end (CORE-09 + CORE-04 + CORE-06 + CORE-07); plus DoS-cap
  rejection at MAX_IMPORT_BYTES + 1; plus malformed-Base64 rejection

RED phase per TDD plan-level gate. Tests fail because codec.ts does not
exist yet.
2026-05-08 23:37:13 -04:00
josh 0b1425d4f6 feat(01-03): idb DB + localStorage fallback adapter (CORE-04) + last-3 snapshot retention + persist API [GREEN]
- db.ts: openSaveDB() opens IndexedDB ('tlg-save', v1) with two object
  stores (saves singleton + save_snapshots keyed); on openDB rejection
  (private mode, blocked, quota exceeded) falls back to LocalStorageDBAdapter
  per CORE-04 contract
- db-localstorage-adapter.ts: ~110-LoC adapter exposing the same minimal
  get/put/delete/getAll/transaction surface as idb's IDBPDatabase, namespaced
  under tlg.saves.<id> and tlg.save_snapshots.<id>; transaction() shim
  proxies straight through (localStorage has no real transactions)
- snapshots.ts: snapshot(envelope) writes to save_snapshots and prunes to
  RETAIN=3 newest by savedAt descending (CORE-08); listSnapshots() returns
  newest-first; entropy suffix on snapshot IDs avoids same-ms collisions
- persist.ts: requestPersistence() returns {granted, apiAvailable} for all
  4 navigator.storage scenarios per CORE-05 + RESEARCH Pitfall 2

Test infra fixes: snapshots.test.ts and db.test.ts cannot deleteDatabase
between tests because openSaveDB leaves an open connection that idb caches
(deleteDatabase blocks indefinitely). beforeEach instead clears store
contents directly. The fallback test calls vi.resetModules() BEFORE
vi.doMock('idb') so the freshly-imported db.ts picks up the rejecting
openDB stub, and re-imports LocalStorageDBAdapter from the same module
graph so instanceof checks against the same class identity.

Tests: 12/12 pass (npx vitest run src/save/db.test.ts
src/save/snapshots.test.ts src/save/persist.test.ts).
Full save suite: 33/33 pass (Task 1 + Task 2 combined).
TypeScript-strict; no 'any' in production code (CLAUDE.md).
2026-05-08 23:36:20 -04:00
josh f44c108b7c docs(01-06): complete doctrine docs plan
- Both Phase-1 doctrine docs landed under .planning/ (anti-fomo + season-7-end-state)
- Vitest doc-lint test scripts/doctrine.test.ts asserts both docs' structural integrity
  (file existence + required H2 sections + required source citations + boundary disclaimers)
- 'npm test' green: 2 test files, 9 tests passing (sentinel + doctrine)
- Per CONTEXT D-07: anti-FOMO enforced by review, no UX-string lint rule
- Per CONTEXT D-08: Season 7 end-state at principle level, treatment text deferred to Phase 7
- Per CONTEXT D-09: docs live in .planning/, not docs/
- Requirements complete: PIPE-05, UX-13, STRY-09
2026-05-08 23:34:11 -04:00
josh 8c1d839adf test(01-02): add CORE-10 firewall test + violator fixture
- src/sim/__test_violation__/violator.ts deliberately imports from
  src/render/__firewall_target__.ts to trigger the firewall rule.
- src/sim/__test_violation__/lint-firewall.test.ts runs ESLint
  programmatically (with ignore: false) against the violator and
  asserts boundaries/element-types fires with severity=error and the
  message mentions both 'sim' and 'render'.
- src/render/__firewall_target__.ts is a minimal export so the
  boundaries plugin can resolve the import to a real path on disk.
  Without a real target, the plugin marks the import as isUnknown
  and silently skips the rule (verified empirically; see SUMMARY).
- eslint.config.js gains an import/resolver: typescript block so the
  TS-aware resolver follows extension-less imports
  ('../../render/foo' -> src/render/foo.ts). Required by the
  boundaries plugin's element classification of import targets.
- tsconfig.app.json excludes *.test.ts and src/sim/__test_violation__/
  so 'tsc -b' does not try to typecheck Node-API-using test code with
  the DOM-only project's lib settings; vitest still discovers them
  via its own include glob.
- Added eslint-import-resolver-typescript as devDep.

Verifies green:
  npm run lint        -> 0 errors, 0 warnings (violator excluded)
  npm test            -> 2/2 pass (sentinel + firewall)
  npm run build       -> tsc -b clean, vite build clean
  npx eslint --no-ignore src/sim/__test_violation__/violator.ts
                      -> exits 1 with the expected
                         boundaries/element-types error

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 23:34:01 -04:00
josh 3625ef85e6 docs(01-04): complete content pipeline plan
PIPE-01 + STRY-09 satisfied. Vite-native loader with literal
import.meta.glob patterns; FragmentSchema regex enforced; demo
fragment proves round-trip; 5 Vitest assertions cover schema
violations; content/README.md is the writer-facing contract.
2026-05-08 23:32:17 -04:00
josh 8521e04ddf docs(01-05): partial summary — Task 1 complete (validator + schema + refused-sample + Vitest), halted at Task 2 human-curate checkpoint
Documents the asset-provenance gate that landed (validator script, Zod sidecar
schema with the 6 CLAUDE.md fields + optional provenance_schema_version, refused-
sample PNG, tmpdir-isolated Vitest enforcement test) and the resume protocol for
Task 2 (Path A AI-generate / Path B hand-painted-or-licensed-photograph fallback /
Path C defer with explicit IOU). Per plan's autonomous: false flag and orchestrator
spawn instructions, the human curates the 10–20 north-star reference images.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 23:32:11 -04:00
josh cde93883bd docs(01-06): author Season 7 end-state principle doctrine + Vitest doc-lint test (PIPE-05)
- .planning/season-7-end-state.md answers principle-level the three questions
  per CONTEXT D-08: (a) what rest state means, (b) what the finite Roothold
  ceiling is tied to (count of authored fragments + Seasons), (c) the coda's
  tonal register (warm/quiet/specific/final). Cites SEAS-04, SEAS-09, SEAS-10,
  STRY-08; ROADMAP Phase 7; PITFALLS #1.
- Includes the explicit 'What this document is NOT' boundary section so
  treatment-level scope creep is structurally rejected (binary-choice scene
  text, ending paragraph text, Lura's final line, credits screen — all
  authored Phase 7, not Phase 1).
- scripts/doctrine.test.ts is the only automated enforcement of both Phase-1
  doctrine docs (per CONTEXT D-07: no UX-string lint rule). Asserts file
  existence + required H2 sections + required source citations + boundary
  disclaimer. 8 assertions / 2 doc files.
- vitest.config.ts include glob extended to scripts/**/*.test.ts so
  doctrine.test.ts is discovered by 'npm test'.
- tsconfig.node.json include extended to scripts/**/*.ts so the strict-TS
  gate covers the new doc-lint test alongside the existing build configs.
- 'npm test' green: 2 test files, 9 tests passing (sentinel + doctrine).
- Per CONTEXT D-09: lives in .planning/, not docs/.

Rule 3 [Blocking]: vitest.config.ts and tsconfig.node.json include globs
extended to discover the new TypeScript test file (existing globs only
covered .mjs scripts and src/ tests).
2026-05-08 23:31:25 -04:00
josh e2d82ffa90 test(01-03): add failing tests for IDB DB + snapshots + persist API [RED]
- db.test.ts (4 tests): IDB-primary path opens both stores + round-trips
  saves and save_snapshots; localStorage-fallback path via vi.doMock('idb')
  asserts LocalStorageDBAdapter is returned and tlg.saves.main is written
- snapshots.test.ts (4 tests): basic put + listSnapshots, empty store
  returns [], CORE-08 5-then-3 retention with newest-first ordering, and
  pruned entries are oldest by savedAt
- persist.test.ts (4 tests): all 4 navigator.storage scenarios per
  CORE-05 + RESEARCH Pitfall 2 (granted true / false / throws / missing)

RED phase per TDD plan-level gate. Tests fail because db.ts / snapshots.ts /
persist.ts / db-localstorage-adapter.ts do not exist yet.
2026-05-08 23:30:02 -04:00
josh c49710e3ad test(01-04): PIPE-01 enforcement — schema violations throw at content load
- 2 happy-path tests: empty globs, valid YAML round-trip
- 3 throw assertions covering the schema-violation matrix:
  * numeric id (violates stable-string-ID rule)
  * season out of [0,7] range
  * Markdown frontmatter missing required id
- All 5 tests pass; full Phase-1 suite remains green
- Proves the throws that fail npm run build at module-eval time
2026-05-08 23:29:40 -04:00
josh da3f55cb69 feat(01-05): asset provenance validator + Zod sidecar schema + refused-sample fixture + PIPE-03 enforcement test (tmpdir-isolated)
- scripts/validate-assets.mjs: walks ASSETS_DIR (default 'assets'), requires every
  non-sidecar non-.gitkeep non-README file to carry a sibling <name>.provenance.json
  validating against Zod ProvenanceSchema (6 required fields per CLAUDE.md / AEST-08
  + optional provenance_schema_version per RESEARCH Open Question #2). Excludes
  assets/__samples__/refused/ so the proof-of-gate fixture passes the gate.
- assets/__samples__/refused/no-provenance.png: 1x1 transparent PNG with no sidecar;
  the gate-proof artifact per CONTEXT D-03.
- scripts/validate-assets.test.ts: Vitest integration test covering both cases.
  Positive: real /assets/ tree must exit 0. Negative: per-test-run mkdtemp under
  os.tmpdir() with one orphan PNG; runs validator with ASSETS_DIR pointing at the
  tmpdir; asserts exit 1 + clear error message + cleanup in afterAll. No risk of
  polluting the real /assets/ tree (BLOCKER 2 fix).
- vitest.config.ts: extend include glob to also pick up scripts/**/*.test.ts (Rule 3
  blocking fix — without this the new test file is invisible to vitest).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 23:29:34 -04:00
josh e9b742da79 chore(01-02): add ESLint flat config + boundaries plugin + CORE-10 firewall rule
- New eslint.config.js (flat, ESLint 9) declaring 9 element types:
  the seven Phase-1 firewall directories (sim, render, ui, save,
  content, audio, store) plus the template's app + game.
- One rule, severity error: sim cannot import from render or ui (CORE-10).
- Default posture allow — Phase 1 enforces ONE rule, not closed-by-default.
- src/sim/__test_violation__/ excluded from default lint glob; the rule's
  end-to-end correctness is proven by Task 2's Vitest test, not by 'lint
  exits 0 on clean code'.
- Added typescript-eslint as devDep (parser only — no rule sets) so
  ESLint can parse .ts/.tsx (Espree default cannot). Documented as a
  Plan 02 deviation in 01-02-SUMMARY.md (Rule 3 — Blocking).

Verifies green on the clean codebase: 0 errors, 0 warnings via
'npm run lint'. Stderr notices from boundaries plugin about deprecated
rule name (element-types vs dependencies in v6) and legacy selector
syntax are informational only — they don't count as ESLint warnings.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 23:29:09 -04:00
josh dddadbc1ac docs(01-06): author anti-FOMO doctrine consolidating PROJECT/REQUIREMENTS/CLAUDE/PITFALLS constraints (PIPE-05, UX-13)
- Banned Mechanics table enumerates 17 banned patterns (gacha, lootboxes,
  narrative gating, daily login, streaks, limited-time, energy/stamina,
  rewarded ads, push notifications, loss-aversion copy, countdown timers,
  Season skipping, time-skip purchases, hint systems, mobile nag UX, etc.)
- Allowed Engagement section names the 4 affordances that respect presence
  rather than demand it (Memory Storm opt-in, while-you-were-away letter,
  tab-title bloom indicator, Season-transition save-export reminder)
- Review Checklist provides 3 questions for every UX/copy/monetization change
- Source Documents section cites PROJECT.md, REQUIREMENTS.md, CLAUDE.md,
  .planning/research/PITFALLS.md
- Per CONTEXT D-07: doctrine is enforced by review, NOT by lint rule on UX
  strings (the doc explicitly notes this and proposes no lint rule)
- Per CONTEXT D-09: lives in .planning/ alongside other internal design docs
2026-05-08 23:29:01 -04:00
josh d52e35f3ad feat(01-04): Vite-native content pipeline + Zod schemas + demo fragment + /content/ README
- FragmentSchema with stable-string-ID regex /^season\d+\.[a-z0-9._-]+$/
- SeasonContentSchema wraps fragments[]
- loader.ts uses import.meta.glob with literal patterns (Pitfall 1)
- Throws on schema violation at module-eval time, failing npm run build (PIPE-01)
- Test-only loadFragmentsFromGlob helper for unit-test injection
- Demo fragment season0.demo.first-light proves end-to-end round-trip
- content/README.md documents the convention for Phase 2 writers (STRY-09)
- Removes now-redundant src/content/.gitkeep firewall marker
2026-05-08 23:28:59 -04:00
josh b6cc9000c3 feat(01-03): save envelope + canonical-JSON CRC32 + synthetic v0->v1 migration [GREEN]
- checksum.ts: crc32hex (8-char lowercase hex of CRC-32, signed->unsigned via >>>0)
  + canonicalJSON (recursive object-key sort, arrays preserved) per Pitfall 3
- envelope.ts: wrap/unwrap with SaveCorruptError on checksum mismatch + Zod
  SaveEnvelopeSchema accepting nonnegative schemaVersion (allows synthetic v0)
- migrations.ts: forward-only registry with migrations[1] producing the v1
  shape from CONTEXT D-04 (garden.tiles, plants, harvestedFragmentIds,
  lastTickAt, settings); throws on negative or future-version inputs

Removes src/save/.gitkeep firewall marker (real source files now live here).

Tests: 21/21 pass (npx vitest run src/save/checksum.test.ts
src/save/envelope.test.ts src/save/migrations.test.ts).
TypeScript-strict; no 'any' in production code (CLAUDE.md).
2026-05-08 23:28:56 -04:00
josh 445a46139f test(01-03): add failing tests for save core (checksum, envelope, migrations) [RED]
- checksum.test.ts: 6 tests covering crc32hex determinism + 8-char-hex format
  + canonicalJSON recursive key sort + array-order preservation (Pitfall 3)
- envelope.test.ts: 9 tests covering wrap/unwrap round-trip + tamper detection
  + Zod schema validation (incl synthetic v0 schemaVersion 0)
- migrations.test.ts: 6 tests covering CURRENT_SCHEMA_VERSION = 1 + the
  load-bearing synthetic v0 -> v1 shape per CONTEXT D-04 + future/negative
  version throws + spy-confirmed registry invocation (RESEARCH Pitfall 7)

RED phase per TDD plan-level gate. Tests fail because impl files do not
exist yet.
2026-05-08 23:27:34 -04:00
josh 1e99356b27 docs(01-01): complete scaffold and test infra plan
Wave 1 of Phase 1 complete. Phaser 4 + React 19 + Vite 8 + TypeScript 6
scaffold builds (npm run build green); 15 Phase-1 deps installed at
locked versions; 7 architectural-firewall directories ready under src/;
repo-root /content/ + /assets/ trees ready; Vitest (happy-dom) +
Playwright wired with passing sentinel; package.json scripts
pre-declared for the entire Phase 1 plan-set so Wave 2 can run in
parallel without colliding on package.json.

3 deviations auto-fixed (1 blocking, 2 missing-critical):
1. Built scaffold by hand because @phaserjs/create-game v1.3.2 is
   interactive-only — plan's documented fallback path was used.
2. build script wraps tsc -b before vite build so strict-TS gates
   every build (CLAUDE.md Code Style invariant).
3. Added *.tsbuildinfo to .gitignore (TS 6 incremental cache files).

Wave 2 readiness: Plan 02 must use ESLint 9 flat-config format
(eslint.config.js); legacy .eslintrc.* not supported. fake-indexeddb
pre-installed for Plan 03 IDB tests. inkjs + inklecate installed but
no .ink files compiled (Phase 2 PIPE-02 owns that).
2026-05-08 23:23:15 -04:00
josh 7b2982b839 chore(01-01): wire Vitest (happy-dom) and Playwright config + sentinel test
- vitest.config.ts: happy-dom environment (so Plan 03's IndexedDB tests
  can layer fake-indexeddb on top of happy-dom's window per RESEARCH);
  passWithNoTests:false enforces RESEARCH CI Pitfall B (a green CI run
  must mean tests *ran*, not 'no tests existed'); include glob covers
  src/**/*.test.ts(x) and scripts/**/*.test.mjs.
- playwright.config.ts: testDir 'tests/e2e' (not yet created — first spec
  lands in Phase 2 PIPE-07); webServer config wires npm run dev so smoke
  tests can self-start the dev server; baseURL pinned to vite default.
- src/__sentinel__.test.ts: a single test asserting 1+1===2 AND that
  globalThis.window exists, proving the runner is wired and happy-dom is
  active. To be deleted once real tests exist (Plan 03 onward).
- npm test → 1 file, 1 test passed in 593ms.
- npx playwright --version → Version 1.59.1 (matches RESEARCH lock).
2026-05-08 23:18:22 -04:00
josh df7d687da4 chore(01-01): scaffold Phaser 4 + React 19 + Vite + TS template + Phase-1 deps + firewall directories
- Built equivalent React + Vite + TypeScript scaffold by hand because the official
  npm create @phaserjs/game@latest scaffolder is interactive-only and the documented
  --template/--yes flags are ignored (verified 2026-05-08 with create-game v1.3.2).
  Plan Step 1 explicitly authorizes this fallback. Resulting tree mirrors the
  official template shape: index.html, src/main.tsx, src/App.tsx, src/PhaserGame.tsx,
  src/game/main.ts, src/game/scenes/Boot.ts.
- Installed Phase-1 production deps at versions verified in RESEARCH.md:
  phaser@4.1.0, react@19.2.6, react-dom@19.2.6, idb@8.0.3, lz-string@1.5.0,
  zod@4.4.3, crc-32@1.2.2, gray-matter@4.0.3, yaml@2.8.4, inkjs@2.4.0.
- Installed Phase-1 dev deps: vite@8.0.11, @vitejs/plugin-react@6.0.1,
  typescript@6.0.3, @types/react@19, @types/react-dom@19, @types/node@22,
  vitest@4.1.5, @vitest/ui, happy-dom, fake-indexeddb@6 (for Plan 03 IDB tests),
  @playwright/test@1.59.1, eslint@9, eslint-plugin-boundaries@6.0.2, inklecate@1.8.1.
- Created the seven architectural-firewall directories under src/ with .gitkeep
  markers (sim, render, ui, save, content, audio, store) — siblings to the
  template-provided src/game/ — so Plan 02's ESLint boundaries rule has clean
  targets per CLAUDE.md 'Architectural Firewall'.
- Created repo-root /content/ (with /dialogue/ and /seasons/ subdirs) and /assets/
  trees per CONTEXT D-11, D-12.
- Pre-declared all downstream-required scripts in package.json so Plans 02–06 only
  edit code, not script keys: dev, build, preview, lint (--max-warnings 0 per
  RESEARCH CI Pitfall C), test (--passWithNoTests=false per CI Pitfall B),
  test:watch, validate:assets, compile:ink (no-op stub for Phase 1; Phase 2
  replaces with real inklecate invocation), ci.
- TypeScript strict mode enforced via tsconfig.json + tsconfig.app.json + tsconfig.node.json.
- npm run build succeeds (tsc -b && vite build) producing dist/index.html and
  dist/assets/index-*.js (~1.5MB Phaser bundle; code-splitting deferred to Phase 2+
  when actual scenes exist).
2026-05-08 23:17:17 -04:00
josh 39563f6934 docs(01): plan phase 1 — 7 plans across 3 waves, verified after 1 revision
Wave 1: Plan 01 (scaffold + test infra)
Wave 2: Plans 02 (eslint firewall), 03 (save layer), 04 (content pipeline),
        05 (asset provenance — autonomous:false human-curate checkpoint),
        06 (doctrine docs)
Wave 3: Plan 07 (CI workflow)

All 16 Phase-1 REQ-IDs covered. Plan-checker found 4 blockers + 6 warnings
on first pass; revision iteration 1 landed all 10 fixes; iteration 2
returned VERIFICATION PASSED. Two orchestrator judgment calls during
revision: (1) implement CORE-04 localStorage fallback in Phase 1 (the
literal requirement and ROADMAP success criterion #2 both call for it),
(2) reclassify STRY-09 as vacuously satisfied in Phase 1.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 23:09:08 -04:00
josh 9c9c6eddbc docs(01): research phase 1 foundations and doctrine 2026-05-08 22:19:21 -04:00
josh 8f58e811b5 docs(01): capture phase 1 context
User locked four implementation decisions:
- AI asset pipeline: minimum-viable schema + sidecar provenance + CI gate;
  vendor/model deferred to Phase 5; 10–20 hand-curated AI generations as
  Phase 1 north-stars
- Save v1: minimal payload (Phase 2 fields only); synthetic v0→v1 migration
  proves the chain works; first real migration ships in Phase 4
- Doctrine docs: anti-FOMO consolidation + Season 7 principle-level rest-state
  contract; both in .planning/; no CI/lint enforcement
- Phase 1 scaffold caps at the 5 success criteria — BigQty, Zustand store, and
  tick scheduler defer to Phase 2

Pushback recorded: user prefers minimum-viable infrastructure for support
systems; no ceremonial workflows.
2026-05-08 22:03:41 -04:00
josh eecb79ae80 docs: create roadmap (8 phases) 2026-05-08 21:37:11 -04:00
josh 3378068884 docs: define v1 requirements 2026-05-08 21:27:24 -04:00