Two gaps, both visible from one screenshot of the Genitiv level.
1. Adjective endings were half-present and entirely untested. STRONG and
WEAK existed in engine.js with correct data, but only as the S/W columns
of a reference chart — nothing generated them, nothing tested them, and
the MIXED table (the one "in seinem eigenen Kontext" actually needs) did
not exist at all. There was not a single adjective in the codebase.
2. Nothing explained why "Regen" becomes "Regens" after trotz.
New Adjectives tab, six lessons plus Daily Practice:
Schwach after der/diese/jede — only -e and -en
Gemischt after ein/kein/mein — weak plus three exceptions
Stark no determiner — the adjective carries the case
Welche Tabelle? der-word, ein-word or nothing? (the meta-skill)
Nach Präposition the full chain: preposition -> case -> table
Alles gemischt all three tables, including Genitiv
The three tables are built on one idea: the case has to be signalled once
in the noun phrase, and whoever can show it does. MIXED is therefore not
hand-typed — it is DERIVED from POSS_ENDINGS as "weak, except in the three
slots where the ein-word has no ending of its own, where it borrows the
strong one". A test asserts that relationship as well as all 48 cells, so
the rule cannot silently rot.
Two info panels:
- "Which ending table?" — the one-signal idea, the three tables, plus the
two traps: strong genitive masc/neut is -en not -es (the noun already
carries the -(e)s), and predicate adjectives take no ending at all.
- "Why Regens?" — appears wherever the case changes the noun itself:
the -es/-s split, that feminines and plurals never change, the dative
plural -n, and a note on the N-declension nouns Beug does not have yet.
Notable decisions:
- Nominative is now tested, for adjectives only. der gute Mann vs ein
guter Mann is the clearest illustration of the whole weak/mixed split and
it exists only in the nominative, so the app-wide "no nominative" rule is
explicitly scoped to non-adjective levels rather than quietly dropped.
- The tab bar measured 369px of content in a 350px bar with four tabs, so
a fifth could not fit. The row now scrolls instead of squeezing labels.
- Adjectives are gated like Possessives and Pronouns.
The corpus caught its keep again. Adjective sentences are enumerated into
corpus.txt keyed on the semantic pairing (frame x adjective x noun), which
keeps it at ~2000 lines instead of exploding over every determiner and
case. Reading it surfaced, and this commit fixes: "I live in the old lake"
(places split into buildings/settlements/landscape), "I play with the blue
sofa", "I like the bad kitchen" (frames that assert liking now exclude
negative adjectives), "I buy the good time" and "I cook with the good
money" (pools narrowed to what you can buy and cook with), grosser/kleiner
of a person rendering as "big/small" rather than "tall/short", and
"in dem alten Hotel" failing to fuse into "im alten Hotel".
130 tests pass; 12000 generated questions across all 30 levels.
Known gap, stated in the rule panel: Beug has no plural nouns, so the
plural column of all three tables is shown for reference but not drilled.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017Uza6mhTpAvSwMQuFc4Cz8
"Wir wohnen bei der Schwester. / We live at the sister." was two bugs at
once, and both were the same shape: a template applied to a pairing nobody
had ever read.
Root cause: sentences are built combinatorially (frames x verbs x nouns x
subjects). The space is ~2900 sentences and only a sample had ever been
looked at, so every fix so far has been whack-a-mole on whichever bad one
happened to surface.
The fix is structural, not another special case:
- composeFrame() is now pure, and enumerateCorpus() walks every frame,
verb, noun and case for every consumer (fill-the-article, ein-Woerter,
possessive, Wechsel im Kontext, fused). src/corpus.txt is that full
enumeration, checked in and reviewed.
- Two tests make it foolproof rather than decorative:
* the snapshot test fails if generation drifts from corpus.txt, so no
new sentence can reach a learner unreviewed. Regenerate with
`npm run corpus` and the diff IS the review.
* a coverage test runs the real generators over every sentence level
and asserts each question it produces has a key in the corpus — so
the file is provably the live output, not a parallel implementation.
Two more assert no un-fused contraction and no German leaking into an
English line.
Bugs the audit then found and fixed:
- zu/von/bei + a person means "at/to/from their PLACE". English needs the
-'s ("at the doctor's"), and with a bare definite article German only
allows roles you visit (beim Arzt), not family — "bei der Schwester" is
wrong, "bei meiner Schwester" is right, so family frames are now
possessive-only. Added Aerztin/Lehrerin/Freundin so the role case has a
non-fusing feminine form.
- "zu dem Arzt", "in dem Haus": a fill-the-article sentence may no longer
show a two-word form German obligatorily fuses. Those pairings belong to
the Kontraktionen questions, which already teach them. "zu" therefore
leaves the Dativ lesson entirely, the way "bis" already had.
- mein Mann / meine Frau mean my husband / my wife, not my man / my woman.
- "mein Maedchen" is not idiomatic and was reachable from the possessive
templates; the coverage test caught it.
- You cannot drive into a kitchen, a cellar or a library: the "in" motion
frame is split into drivable places and on-foot-only places.
- "stellt den Stuhl an die Wand" is "against the wall", not "to the wall";
haengen/schreiben/kleben take "on"; wohnen am See is "by the lake".
Also: questions now carry the verb and case they were built from, the
article redraw is depth-bounded so a data mistake degrades to one odd
question instead of hanging, and level 3's advertised prep list matches
what it can actually generate.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017Uza6mhTpAvSwMQuFc4Cz8
"hinter + das = ?" with options am / im / uebers / hinters was no test at
all — only one option could start with "hinter", so you never had to know
which article was fused in. The whole level was answerable on the prefix.
Flipped it. The fused form is given and you take it apart:
- The form is shown large ("im"), and you pick the preposition from a word
cloud, then the article from a second one. Graded on both parts.
- The preposition step is a real question for the truncated forms — im vs
am, vom, zum/zur — which are exactly the ones that get mixed up.
- The article step is the point: -s means das, -m means dem, -r means der.
Distractors are always real articles (das/dem/der/die/den), never whole
contraction forms, so elimination on shape no longer works.
- Feedback names which half was wrong and states the ending rule, then
highlights the article in the definite-article table.
- The chosen preposition chip is tappable to change before committing;
backspace does the same on desktop. On reveal the line resolves to the
correct pair.
Tests: the old flaw is pinned shut (nothing on offer is ever a contraction
form), both choice sets are checked, and a data test asserts the -s/-m/-r
ending predicts the article for all 16 contractions.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017Uza6mhTpAvSwMQuFc4Cz8
The level showed the article already in the sentence ("unter dem Stuhl"),
so it was answerable by recognising "dem" — it never tested wohin/wo at
all. And the frame pool was tiny: one cat, one lamp, eight nouns.
- The article is now blanked. The only evidence is the verb and whether it
has a direct object, which is the actual skill. The solved sentence is
revealed on answer, contracted where German really contracts (ins Regal,
am Bahnhof), with a note naming the fusion.
- Frames rebuilt around what decides the case: 27 new nouns, 18 new verbs,
and every frame one-way so no verb+noun pairing licenses both readings.
- Motion verbs now appear on the DATIVE side on purpose — schwimmen im See,
tanzen in der Kueche, spielen auf der Strasse — because "is it moving?" is
the wrong test and that is the main thing learners get wrong.
- The put/be pairs are drilled properly: legen/liegen, stellen/stehen,
setzen/sitzen, plus haengen and stecken, which keep the same present tense
either way so only the direct object tells you.
- ankommen added as the lexical exception: Dativ despite obvious arrival.
- New "Wohin oder wo?" info panel: the rule, the before-and-after test, the
movement trap, the verb-pair table and the exception. Reachable from the
practice screen, from the answer feedback, and from Charts.
- Feedback now explains the specific sentence rather than restating theory.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017Uza6mhTpAvSwMQuFc4Cz8
- Possessives, pronouns, and Kontraktionen switch from typed input to the
same 4-choice tap grid as articles. Possessive choices all share the
question's stem; pronoun choices lead with the pronoun's own case forms.
- The translation line is hideable everywhere (shows by default, hidden
state persists like the other hints, revealed again on a wrong answer).
The possessive and pronoun hint lines get hide toggles too.
- Header: Charts moves out; One-tap becomes a pill — plain grey when off,
green outlined when on. More margin below the header across the board.
- Bottom bar: progress dots centered with 'n/10 · level name' underneath,
outlined Charts button (capital C) pinned bottom-right.
- Touch devices hide the 1-4/enter/esc keyboard chips; answer buttons keep
their full size via min-heights so tap targets stay big.
The ein-Wörter level reused sentence frames written for definite articles
and blindly swapped in the ein/eine table, producing non-German like
'trotz eines Wetters' and 'während einer Woche' while the English still
said 'the'.
- Nouns gain a noIndef flag (mass nouns: Wetter, Regen, Musik, Arbeit;
definite-idiom temporal/kinship nouns: Nacht, Woche, Vater, Mutter,
Bruder, Schwester, Tochter). They are never paired with an ein-word.
- Frames with a custom English determiner (nach → 'after school',
seit → 'since that week') are definite idioms and are skipped for
ein-word questions.
- genArticleQ redraws prepositions whose frames are entirely ruled out
(trotz has only mass nouns), instead of generating a bad sentence.
- Indefinite translations now use a/an to match the German article;
definite levels keep 'the'.
- Tests pin all of this down (75 passing).
- One-tap mode no longer shows the answer when you're right: a green tick
flashes and it skips straight to the next question. Wrong answers still
show the full correction and now wait for an explicit Next tap.
- Every tab (Prepositions, Articles, Possessives, Pronouns) gets its own
pinned Daily Practice card. Sessions mix questions from every lesson in
the tab via a shuffle-bag over lesson ids.
- Daily Practice is locked until every lesson in its section has been
passed (one full round at 70%); the locked card shows x/y progress.
- Finishing a daily session offers 'Continue: <section> Daily Practice'
cycling to the next tab with an unlocked daily, with play again and
back to menu as secondary options.
Every answer card now renders the complete 4×4 declension grid instead
of a single case row. The correct cell is highlighted green; on a wrong
answer, the cell matching what you typed is highlighted red, so you see
both your pick and the right one in context.
- FillFeedback (articles + possessives): full table, correct + wrong
highlighting within the noun's gender column.
- ChunkFeedback + ContractionFeedback: contraction questions now show
the definite-article table too, not just the fused answer.
- PronounFeedback: correct cell green, your wrong pick red.
- Daily Practice (all 31 prepositions) is pinned to the top of the
Prepositions tab once the "Alle Präpositionen" lesson is passed
(>=70% over a full round).
- One-tap mode: after answering, the app flashes right/wrong and
auto-advances. Always on in Daily Practice; a persisted "One-tap"
toggle in the practice header enables it for any other level.
New pseudo-level (id 10.5) that drills all 31 prepositions. App.jsx
pins it to the top of the Prepositions tab once the "Alle
Präpositionen" lesson is completed, and defaults it to one-tap mode.
- Remove the Nominativ level and drop nominative-case testing from mixed/
possessive/pronoun levels (nominative kept only in reference charts)
- Add persistent hide/show toggles on the two giveaway hint lines in
practice (noun gender, case/preposition info); state persists in localStorage
- De-spoil "Wechsel im Kontext": remove motion/location hint from answer
buttons; put sentence translation + preposition meaning behind a toggle
- Add a 7-day practice-history dot strip beside the streak counter
- Extend engine + tests (61 vitest tests)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Multi-stage build (node -> nginx) serving the Vite static output on
port 80, matching the dockerfile build pack used for other apps.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Vite + React app for daily German declension practice with
localStorage-backed progress. Includes Coolify deploy instructions.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>