kanji-buddy
Code map
Where everything is, and the one rule that decides which directory a piece of code belongs in.
The rule
If it can be tested without a browser, it goes in lib/. Route handlers authenticate, parse, delegate and serialise. Components render. Everything with a decision in it sits between them as a plain function whose first argument is a database handle.
That is not tidiness for its own sake. The quiz queue lived inside the component for months and had no tests at all — which is exactly how a bug that re-asked the same radical three times got shipped. Pulling it into lib/quiz.ts as a pure function is what made it testable, and the test that reproduces that bug now exists.
lib/ in detail
| File | What it holds |
|---|---|
srs.ts | Stage arithmetic: what a correct or incorrect answer does to a stage, and when the item is next due. |
progression.ts | Unlocking. Passing a radical opens its kanji; passing 90% of a level's kanji opens the next level. Also bootstraps a new account. |
answer.ts | Grading. Damerau-Levenshtein typo tolerance for meanings, exact matching for readings, and the three-way correct/incorrect/retry verdict. |
quiz.ts | The review queue as a pure transition: what a graded answer does to the running order. |
dashboard.ts | The whole dashboard in two batched round trips. Also the shared SRS buckets and the trimmed subject columns the list endpoints select. |
vacation.ts | Stopping and restarting the SRS clock. Ending a vacation shifts every timed assignment forward by the time away. |
dailyLessons.ts | The daily lesson ceiling: how many the day still permits, counted as a calendar day in the account's timezone. |
itemSpread.ts | Every started item grouped by SRS tier, then by exact stage, then by type — one query for the Item Spread screen. |
audio.ts | Which recording of a word to play, given the five-way voice preference. Lifted out of the quiz so the only-versus-prefer split can be tested without a render. |
mnemonic.ts | Parser for the inline markup in mnemonics — 2,589 subjects carry <radical>, <kanji>, <reading> and friends. |
subject.ts | The shape of a subject and small readers over it. Shared by server and browser, so both halves agree. |
http.ts envelope.ts collection.ts pagination.ts conditional.ts filters.ts respond.ts query.ts serializers.ts | The WaniKani API surface: envelopes, id-cursor paging, ETag/304, shared filters. |
Why lib/web/ is separate
lib/web/ is the only part of lib/ that assumes a browser — fetch, localStorage. The split keeps server code from importing it by accident, and it is why the types in lib/subject.ts live outside both.
app/api/v1/ — the 34 route files
Eleven WaniKani resources, most with a collection route and a [id] route, plus the ones that are ours: dashboard, summary, review_stats, days_studied, heat_map, item_spread, lesson_allowance, vacation, study, advance, push_tokens, unsubscribe and three under internal/. That prefix is doing real work — it marks the endpoints guarded by a shared secret rather than a user token.
The app-specific ones exist for the same reason throughout: a screen needs an aggregate the resource endpoints cannot serve cheaply. Building the Item Spread from /assignments and /subjects would be a request per bucket plus a paged subject lookup, and the paging is where a mature account would quietly lose rows.
scripts/
Each exports a function taking a database handle and has a thin main() behind a run-as-main guard, so the tooling is tested like anything else. fetch-wanikani and import-subjects loaded the content; create-user and create-token are how accounts exist at all; nudge-preview and nudge-test exercise the email job without sending to anyone; import-mnemonic-images attaches the scraped illustrations.
What is not in the repository
data/ is git-ignored. It holds WaniKani’s copyrighted content — the 9,418 subjects with their mnemonics — and must not be committed.
public/radicals/ is the exception, and is committed: 19 SVGs, 9.7 KB, for the radicals that have no Unicode character. It was git-ignored and refilled by fetch:radical-svgs during npm run build, which made every deploy depend on the database and WaniKani’s CDN both answering — and the script only warned when one did not, so a failed fetch produced a green build with radicals silently missing from it. Nineteen small files are cheaper to keep than to re-earn on every deploy. Same copyright footing as data/, which is why the repository is private.
Next, if the framework is the unfamiliar part: Next.js by example.