test: pin that retiring a game cannot delete a reader's own events

Nothing in the client prunes reader data today — no store filters against the
feed, knownGames only appends, metaFor renders a lane whose game is gone — so a
retired source, page or game leaves completion marks, streaks and hand-entered
events untouched. That was true by construction and pinned nowhere.

The load path is what makes it worth pinning. useCustom reads through
validRecords, which drops a record that fails its schema, and the survivors are
what the next write persists. A record that stops parsing is therefore not
hidden until someone notices — it is deleted from the device, permanently, by
the act of opening the app, with no server-side copy to recover it from.

Which makes CustomEvent.game being z.string() rather than GameId the whole
safety property, and it currently reads like validation someone forgot. Anyone
narrowing it to the enum would be tightening a schema and arming every future
game removal to erase reader data on next launch. The tests state the premise,
the survival, and that one unreadable neighbour still does not take the rest
down; the field says why it is loose.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
This commit is contained in:
Lucas Winther
2026-08-19 04:40:04 +02:00
co-authored by Claude Opus 5
parent acadbfa17a
commit 239e970471
3 changed files with 86 additions and 2 deletions
+12 -1
View File
@@ -119,7 +119,7 @@ src/client/ React app, service worker, manifest
theme.ts — dark or light, and what a game hue reads as on each
scripts/ build-feed.ts, build-static.ts, parse-fixture.ts (offline), refresh-sources.ts (fetches)
serve.ts static server + /api/health
test/ 711 tests
test/ 716 tests
fixtures/<game>/ raw HTML + .expected.json per source — pinned, kept forever
snapshots/ current page per source, rewritten by refresh — see its README
```
@@ -631,6 +631,17 @@ A lane may now be a game the reader invented, so `gameMeta` is a context resolve
and total) rather than a direct lookup — a lane can outlive its game when an import carries an event
whose game did not come with it.
**Retiring a game, a source or a page must never cost the reader a row they typed.** We retire
things routinely — a source moves, a page goes stale, a game shuts down — and their events are the
only copy in existence. Nothing in the client deletes: no store prunes against the feed, `knownGames`
only ever appends, and `metaFor` renders a lane whose game is gone rather than dropping it. The one
place this could break is the load path. `useCustom` reads through `validRecords`, which **drops a
record that fails its schema**, and the survivors are what the next write persists — so a record that
stops parsing is not hidden pending a fix, it is deleted from the device by the act of opening the
app. That is why `CustomEvent.game` is `z.string()` and not `GameId`: narrowing it to the enum reads
like a tightening and would arm every future game removal to erase reader data on next launch.
`test/custom.test.ts` § retiring a game, a source or a page pins it.
## Shipping a new version
The shell is cached cache-first, so a reader with the tab open keeps the bundle they first loaded.