Kimariji決まり字 Join the waitlist

Case study · measured 2026-09-24

A reading app where a green test means something.

Kimariji teaches you to read Japanese: kana, then kanji and everyday words, scheduled by FSRS spaced repetition. One React + TypeScript codebase, shipped to Android through Capacitor. This page covers how it is built and three bugs that changed the rules.

Status: not yet on Google Play. The test suite has 3 known failures, explained below.

Back to Kimariji

Measured state

The numbers, with the command that produces them.

WhatValueCheck
Source files647npm run architecture:check
Layer violations · import cycles0 · 0same
Tests2,654 of 2,657 passnpx vitest run
Content2,136 kanji · 888 words · 778 sentencesrelease path §1
Audio102 kana clips · 3,448 text clipssame

Architecture

Two layers, and a script that holds the line.

The engine is pure TypeScript. The UI is React. The engine never imports the UI. The engine holds every rule: scheduling, mastery, curriculum. The UI renders engine state and forwards input. Changes flow outward through an event bus, and storage goes through one port.

A rule nobody checks drifts, so a script checks it. The build fails on an engine file importing React or UI code, on storage access outside the port, on a runtime import cycle, on a file over 300 lines, and on a file without a header stating its purpose and invariants. The checker has its own tests that feed it real violations.

Scheduling is FSRS (via ts-fsrs). Every study item has one FSRS card. Learn and Test both write to it, and progress is computed from it, so mastery is never a second number that drifts.

Content truth

Sourced, gated, never generated.

No model writes an example sentence or its translation. 679 of the 778 sentences are Tatoeba rows with their human translation; the other 99 are hand-authored. A bad sentence is replaced with another sourced one or dropped. Kanji and word data come from JMdict and KANJIDIC2.

A content gate runs over the whole catalog: 519 base kanji and 888 words, against dictionaries committed to the repo. Every rule states how many rows it examined and fails if that number collapses. Known, unfixed defects sit in a list the test asserts exactly.

What the gates do not prove: they catch mechanical errors, not style. The sentences have not had a native speaker's editorial review.

Audio QA

Measure the sound. Do not trust a transcript.

Speech recognition guesses on single syllables, so the guards measure the audio itself.

  • Integrity re-encodes each clip from its archived raw synthesis and fails on truncation or clipping.
  • Identity reduces each kana clip to a loudness-invariant acoustic fingerprint and requires it to match the take a person listened to. Own-print distance 0.000; the nearest other clip 1.345; threshold 0.8.

The 3 failing tests are this guard, on purpose. The kana clips were re-encoded after the fingerprints were approved. The baseline moves only after a person listens to all 102 again.

Three hard bugs

Each one became a written rule.

1. Words with a small っ were cut off mid-sentence

Commit e34b5be9 · 2026-06-12

A length-per-syllable check flagged 47 clips as too short. The encoder trimmed "trailing" silence with ffmpeg's silenceremove=stop_periods=1, which actually cuts at the first silence of 0.08 s or more. A small っ (a doubled consonant, as in がっこう) is 0.1–0.2 s of real silence, so every such sentence ended there. Fix: reverse, trim the start, reverse back. All 1,581 text clips were regenerated.

2. を said や, and the encoder ate consonants

Commits aab0b89b · cc685815 · 1d978627 · 2026-09-12

Listening on a phone, the owner heard を say や. A blind test then heard て as で and か as は. Two causes: the rare katakana ヲ made the voice improvise /ja/, confirmed by formant measurement; and the chain gated silence before normalising loudness, so all 3,569 clips started part-way into their attack, removing the burst that separates て from で. Fix: send を, normalise first, gate the head at −55 dB, and add real leading silence. The blind retest was 14 of 14. Two new guards (integrity, identity) came out of it.

3. A green suite over wrong content

Commits 5139afe4 · 2026-08-09 · 81d97156 · 2026-09-22

An audit found about a dozen teaching errors behind green tests; 月 taught 一月 as ひとつき, "one month", although written 一月 usually reads いちがつ, "January". A later data layer replaced example words at runtime, but three guards still read the data from before it, and two sweeps ran over zero rows and passed. Fix: every guard reads what the app renders and asserts its scope is non-zero; checkable rows went from 0 to 2,103.

A month later, new guards were broken on purpose to see if they fail. One did not: a counter stuck at zero passed its one-sided check. It now asserts both sides. Rule: break every new guard once before trusting it.