Case study · japonais-2027 · Zetetic method in production

Steered by symptoms,
locked by invariants.

An offline-first Japanese learning PWA, one HTML file, aimed at JLPT N3. Its owner never wrote a spec. He reported what felt wrong in real use, the agent measured it against the data, shipped a minimal fix, and locked every fix with a test that replays the exact report. When owner and app disagreed, an instrument was built and the numbers settled it.

Owner on an iPad, agent in a Claude Code session, zetetic rules loaded. No invented content anywhere in the product.

The loop, as actually practiced

Every change followed the same five steps. No step is skippable, and the last two are what make the first three trustworthy.

1

A symptom, not a spec

The owner reports lived experience: “my kana never get past guru”, “the lesson quiz only offers the right answer”, “mouth + I gives naka, this is incomprehensible”. French, terse, symptom-level.

2

Measure before believing

The symptom is reproduced against real data: card counts, reference dictionaries, an audit of the session journal. When the measurement contradicts the report, that is said plainly, and the hunt moves to what produced the perception. There is almost always a real defect behind it, just not where it was announced.

3

Minimal fix, named metric

Standing rule from the owner: a change that cannot name the metric it improves has no right to exist. Commits are one concern each, staged file by file, and the message records the measured defect, not the timeline.

4

An invariant replays the report

Each fix lands with an executable check that replays the original complaint, values included: “the card for 中 must say au milieu, never naka”. Ten invariants guard the repo: nine static checks plus a real boot in headless Chromium with about thirty named scenario blocks.

5

Observable release

The app prints its version on screen and an invariant enforces that the displayed version equals the service worker build. “It is deployed” is not a sentence anyone has to trust. It is a line the owner reads on his iPad.

The gates that actually bit

Checks that never fail are decoration. These two failed for real, mid-build, and blocked a ship until the ambiguity was resolved.

The ratchet vs. the import

import : niveaux = 0, attendu 1 Ne pas livrer.

Adding an auto-open ratchet to level gating silently swallowed a scenario the import test owned. The red run forced a real distinction between a level opened by progress and a level opened by an imported save, instead of letting the ambiguity ship.

The kana that left the audio world

bascules : couper le son sur un niveau ne recalcule pas les cartes en pause

When syllable dictation was removed as pedagogically empty, the toggle test still expected paused audio cards on a kana level. The test was consciously retargeted to word dictation, in the same commit as the behavior change. Retargeted, never deleted.

The zetetic contract names three refusals: “pre-existing”, a skipped check, and a red run. All three held for the whole build.

Nothing written from memory

Standing rule number two. Every fact in the product traces to a source, and a gap in the source stays a visible gap instead of a plausible invention.

In the productComes fromLicense
Kanji meaningsKANJIDIC2, the EDRDG reference dictionaryCC BY-SA 4.0
Character decompositionCJKVI IDS database (CHISE project)GPLv2
Etymology storiesmakemeahanzi hints, derived from Wiktionary, translated faithfullyGPL / Arphic
Furigana readingsJanome morphological analyzer with the IPAdic dictionaryApache 2.0
Spacing & recall scienceCepeda 2008, Karpicke & Roediger 2008, Dumay & Gaskell 2007, Pashler 2008Cited in README

A word the dictionary cannot read gets no furigana rather than a guessed one. A sign with no French gloss shows bare rather than glossed by imagination. Course material under copyright never enters the repository; the app links to the official free textbook instead of copying it.

In the owner’s words

Four sentences that summarize how it felt to lead this build.

I built the app by reporting symptoms. I described what felt wrong, the agent measured it, fixed it, then locked the fix with a test that replays my exact report.
on the loop
Nothing was invented. Every kanji meaning, every etymology, every reading comes from a cited source, and a gap in the source stays a visible gap.
on sources
When I disagreed with the app, we did not argue. We built the screen that shows both measurements, and the numbers settled it.
on disagreement
Ten executable invariants guard the door. If one fails, the output says do not ship, and it does not ship.
on the gates

The product it produced

Repository content only, no third-party course material. Progress seeded so the mastery colors show.

Home screen: today's count, three toggles, mastery stages and the full kana grids
Home: the day’s number, per-stage counts, and every lesson of every section with one tile per item.
Card back with furigana reading over the kanji word
Card back: furigana interline over the word, and every kanji is a tap-target.
Kanji sheet for 男 with meaning, reading, etymology story and glossed components
Tap a kanji, get its sheet: meaning, reading, the etymology story, components glossed line by line.
Printable writing sheet with model signs and practice squares
Writing practice goes to paper: a generated print sheet, because a self-graded tracing card measures nothing.

Single-file engine, offline-first, three-mode sync, generic shell: an imported program can declare its own language, sections and cards, so the same engine can serve Korean or Chinese without a fork.

Want your build led like this?

Symptom in, measured fix out, invariant locked, release observable. That is the engagement.