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