Generation guards: cards with repeated wording never ship

A generated Czech literature deck reached a phone with three identical
matching definitions — the card could not be solved. The pipeline only
deduped matching terms (and ordering items), so repeated definitions and
options sailed through.

- parseDeck now rejects any card whose options, items, terms or definitions
  repeat (case-insensitive), through the same drop-or-repair path as the
  ordering giveaway guard: the card is dropped, or a deck left with too few
  cards gets one repair round with a clear reason.
- The matching dedupe covers definitions too, and both it and the ordering
  one only trim when the card still meets the schema minimum afterwards —
  a shorter card would fail validation on the next read and take the whole
  deck down with it (latent data-loss bug).
- The generation prompt states the uniqueness rule.
- phase-1 suite: a stub Ollama returns a deck with a repeated-definition
  matching card (the reported shape), a repeated-option card and a
  salvageable one; asserts the first two are dropped, the third is trimmed
  to three pairs and the good cards ship (43 checks).
- README: decision 51.

Server-side only (lib/ai.ts, lib/prompts.ts) — no client bundle change, no
SW bump.
This commit is contained in:
Martin Dvořák 2026-10-12 05:37:03 +00:00
commit f202653017
4 changed files with 177 additions and 22 deletions

View file

@ -1346,8 +1346,21 @@ Things I chose while building, with the reasoning:
(`classifyOllamaFailure`) and mapped to `usage_limit` / `throttled` kinds — the client renders
the same localized message in every model flow, with a jump to the key field and a link to
Ollama, and keeps the user's input so Retry works right after swapping the key. Verified by the
phase-1 suite (38 checks, including the real route mapping against a stub Ollama returning a
monthly-limit body, and a two-device sync round-trip).
phase-1 suite (43 checks, including the real route mapping against a stub Ollama returning a
monthly-limit body, a two-device sync round-trip, and the generation guards below).
51. **Cards with repeated wording never ship.** The model sometimes pads a matching card with one
definition copied across several terms (a Czech literature deck arrived on a phone with three
identical definitions, so the card could not be solved by knowing the material), or repeats an
option. The generation pipeline now rejects any card whose options, items, terms or definitions
repeat (case-insensitive) — the same drop-or-repair path the ordering giveaway guard already
used, so a deck with too few cards left over gets one repair round instead. The matching and
ordering dedupe only trims a card when it still meets the schema's minimum afterwards: a
shorter card would fail validation on the next read and take the whole deck down with it, so
below the minimum the original is kept and the card is rejected instead. The generation prompt
states the uniqueness rule too. Verified by the phase-1 suite's stub-Ollama generation: the
repeated-definition card is dropped, a salvageable card is trimmed to three pairs, the
repeated-option card is dropped and the good cards still ship.
41. **The learning log is an event log, so it lives in IndexedDB.** Decks and homework stay in
`localStorage` (small, always loaded), but answers + SRS schedules go to a versioned