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