Wechsel im Kontext: blank the article, make the verb decide, add the rule

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
This commit is contained in:
2026-09-17 06:55:26 +00:00
co-authored by Claude Opus 5
parent f39db3cb86
commit 047454f23f
3 changed files with 418 additions and 67 deletions
+70 -1
View File
@@ -3,7 +3,7 @@ import {
DEF, INDEF, PREPS, PREPS_BY_CASE, COMMON_PREPS, RARE_PREPS, ALL_PREPS,
NOUNS, nounForm, LEVELS, CONTRACTIONS, CONTRACTABLE, CHUNK_PREPS,
POSS_ENDINGS, combinePoss, PRONOUNS, VERBS, PREP_FRAMES,
createSession, gen, genContractionContextQ, newState, buildChunkChoices,
createSession, gen, genContractionContextQ, newState, buildChunkChoices, PREPS_BY_CASE as _PBC,
bumpStreak, currentStreak, weekDots,
} from './engine.js';
@@ -189,6 +189,75 @@ describe('wechsel in context (level 18)', () => {
expect(q.sentence.endsWith('.')).toBe(true);
}
});
it('blanks the article so the verb is the only evidence', () => {
for (const q of runSession(18, 300)) {
expect(q.sentence).toContain('_____');
expect(q.article).toBe(DEF[q.noun.g][q.ans]);
// the blanked sentence must never leak the answer article next to the prep
expect(q.sentence).not.toContain(` ${q.prepKey} ${q.article} `);
}
});
it('the solved sentence contracts exactly where German contracts', () => {
for (const q of runSession(18, 400)) {
const fused = CONTRACTABLE[q.prepKey]?.[q.article] || null;
expect(q.contraction).toBe(fused);
expect(q.answerSentence).not.toContain('_____');
if (fused) expect(q.answerSentence).toContain(` ${fused} `);
else expect(q.answerSentence).toContain(` ${q.prepKey} ${q.article} `);
}
});
it('drills far more than the handful of cats and lamps it used to', () => {
const qs = runSession(18, 400);
expect(new Set(qs.map(q => q.verb)).size).toBeGreaterThanOrEqual(20);
expect(new Set(qs.map(q => q.noun.w)).size).toBeGreaterThanOrEqual(25);
// no single sentence should dominate the round
const counts = {};
for (const q of qs) counts[q.answerSentence] = (counts[q.answerSentence] || 0) + 1;
expect(Math.max(...Object.values(counts))).toBeLessThanOrEqual(12);
});
it('teaches that movement alone is not Akkusativ', () => {
const qs = runSession(18, 600);
// motion verbs used with a dative "arena" reading
const arena = qs.filter(q => ['schwimmen','tanzen','spielen','fliegen'].includes(q.verb) && q.ans === 'dative');
expect(arena.length).toBeGreaterThan(0);
// ankommen is the lexical exception: always dative
const ank = qs.filter(q => q.verb === 'ankommen');
expect(ank.length).toBeGreaterThan(0);
for (const q of ank) expect(q.ans).toBe('dative');
});
it('the put/be verb pairs land on the case their transitivity demands', () => {
// Unambiguously transitive "put it there" verbs vs unambiguously stative ones.
// hängen and stecken are deliberately excluded — see the next test.
const PUT = ['legen','stellen','setzen','werfen','schieben','packen','tragen','kleben','schreiben'];
const BE = ['liegen','stehen','sitzen','wohnen','leben','schlafen','warten','arbeiten','sein'];
for (const q of runSession(18, 600)) {
if (PUT.includes(q.verb)) expect(q.ans, `${q.answerSentence}`).toBe('accusative');
if (BE.includes(q.verb)) expect(q.ans, `${q.answerSentence}`).toBe('dative');
}
});
it('hängen and stecken appear in BOTH cases — only the direct object decides', () => {
const qs = runSession(18, 800);
for (const v of ['hängen', 'stecken']) {
const mine = qs.filter(q => q.verb === v);
expect(mine.some(q => q.ans === 'accusative'), `${v} never showed transitive`).toBe(true);
expect(mine.some(q => q.ans === 'dative'), `${v} never showed intransitive`).toBe(true);
// transitive = something is being moved, so the sentence carries an object
// before the preposition; intransitive sentences start with that thing.
for (const q of mine) {
const beforePrep = q.answerSentence.split(` ${q.prepKey} `)[0];
const fusedHead = q.contraction ? q.answerSentence.split(` ${q.contraction} `)[0] : beforePrep;
const head = q.contraction ? fusedHead : beforePrep;
const hasObject = /\b(das|den|die|dem)\b/.test(head);
expect(hasObject, `${q.answerSentence} (${q.ans})`).toBe(q.ans === 'accusative');
}
}
});
});
// ── Chunk / contraction questions ───────────────────────────