Commit Graph

20 Commits

Author SHA1 Message Date
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 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 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 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
josh d7aa38f37e docs: synthesize research findings 2026-05-08 21:24:01 -04:00
josh a5dc54e95f docs: project research (stack, features, architecture, pitfalls) 2026-05-08 21:23:58 -04:00
josh a9f4efabac chore: add project config 2026-05-08 20:58:54 -04:00
josh de4e094d00 docs: initialize project 2026-05-08 20:57:15 -04:00