Phase 2: persisted state and backup parsing

storage.ts owns the blob that survives a restart - boxes, ctx, deck,
dir, input - plus validation for text a person pastes into the backup
box. Pure: parsing and serialising only. Where it is kept, localStorage
mirrored into Capacitor Preferences, stays the shell's problem.

STORAGE_KEY is still sp404mk2-layers-v1, unchanged from Phase 1, so
progress already on the phone is read rather than orphaned. The test
parses a payload copied verbatim out of CapacitorStorage.xml on the
device, so a change that would break migration fails the suite.

Two levels of strictness, matching the reference: reading our own
storage is lenient and falls back per field, while a pasted backup must
be an object carrying a boxes object or it is refused outright.

input defaults to hold rather than sniffing for a coarse pointer as the
reference did - that is a DOM question, so the shell decides it.

The domain layer is now complete: entries, layers, drill, format,
search, storage. 103 tests.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-17 00:57:38 +03:00
parent 43fb51396c
commit ad847faa2e
4 changed files with 223 additions and 0 deletions

View File

@@ -176,8 +176,17 @@ src/domain/
entries.ts derives Entry[] from RAW
layers.ts which combos exist for a context + held modifiers
drill.ts decks, Leitner boxes, answer checking, card selection
format.ts ids to printed words, and the "SHIFT + PAD 1" lines
search.ts the Find tab's matcher
storage.ts the persisted blob, backup parsing, defaults
```
**Two strings are load-bearing for migration** and must not be changed casually:
`Entry.id` (below) and `STORAGE_KEY`, still `sp404mk2-layers-v1`. Together they
mean the boxes already on the phone are picked up by the React app rather than
orphaned. `storage.test.ts` parses a payload copied verbatim out of
`CapacitorStorage.xml` on the device, so a change that would break that fails.
**The combo table is never transcribed by hand.** `scripts/extract-reference-data.mjs`
lifts the literals out of `reference/index.html`, evaluating only pure-data
declarations in a `vm` with no DOM, and writes `raw.ts` and `labels.ts`. Re-run it