ein-Wörter was quietly dropping three of the nine dative prepositions

Reported as "not a single dative in the ein-Wörter section". The engine did
still produce dative questions, but only from 6 of the 9 dative
prepositions, so the dative was thinner than the lesson claimed and leant
on Wechsel preps landing on dative rather than on dative preps themselves.

The indefOnly frame filter was throwing three away:

- bei — a regression from the "bei der Schwester" fix. That commit added
  `!f.enPossS` to the filter, which killed bei's only non-possessive frame
  outright. "bei einem Arzt" is perfectly good German and reads as "at a
  doctor's" in English, so there was never a reason to exclude it. von and
  zu survived but lost their person frames the same way, halving them.

- nach, seit — excluded as "definite idioms" because their frames carry an
  enDet override ("nach der Schule" is "after school", not "after the
  school"). But "nach einer Woche" and "seit einer Woche" are textbook
  German; the engine's own PREPS entry for seit uses "seit einer Woche" as
  its example.

The enDet exclusion existed to stop "während einer Woche", the sentence
reported a while back, so it could not simply be deleted. Frames can now
name `indefNouns`: the nouns that DO work once an ein-word is in front,
overriding the blanket noIndef flag for that frame only. nach and seit
re-admit Woche; während re-admits nothing, so "während einer Woche" stays
impossible. seit also gets enPrepIndef, because "seit einer Woche" is "FOR
a week" in English, not "since a week".

trotz stays out, but honestly: it only governs uncountables here ("trotz
eines Wetters" is not German), so it is removed from level 7's preps list
rather than left advertised and unreachable — the same treatment zu got in
the Dativ lesson.

The guard that should have caught this: a new test asserts that every level
generates every preposition it lists. A prep that can never be asked about
is now a test failure, not a silent hole. Two older tests that asserted the
buggy behaviour (that nach/seit never appear, that no noIndef noun ever
appears) are rewritten to assert the actual rule instead of being loosened.

Dative coverage in ein-Wörter: 6/9 prepositions before, 9/9 after. Case mix
47% dative / 32% accusative / 21% genitive. A round of 10 with no dative at
all went from 0.2% to 0.06%.

142 tests pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017Uza6mhTpAvSwMQuFc4Cz8
This commit is contained in:
2026-10-01 06:39:45 +00:00
co-authored by Claude Opus 5
parent b9a4e16892
commit e448045e52
3 changed files with 109 additions and 14 deletions
+22 -8
View File
@@ -71,6 +71,10 @@ const SENTENCE_ACC_PREPS = ['durch','für','gegen','ohne','um'];
// sentence can never show it honestly — it lives in the Kontraktionen levels
// and the mixed rounds, which teach the fused form properly.
const SENTENCE_DAT_PREPS = PREPS_BY_CASE.dative.filter(p => p !== 'zu');
// "trotz eines Wetters" is not German — trotz only governs uncountables here,
// so the ein-Wörter lesson leaves it out rather than listing a prep it can
// never ask about.
const INDEF_GEN_PREPS = PREPS_BY_CASE.genitive.filter(p => p !== 'trotz');
export const WECHSEL_EX = {
an: {noun:'Tür', g:'feminine', en:'door', mot:'to', loc:'at'},
@@ -476,7 +480,7 @@ export const LEVELS = [
{id:4.5, tab:'article', name:'Kontraktionen', desc:'Fused forms in context: ins, am, zum…', cases:['accusative','dative'], type:'chunk', artType:'definite'},
{id:5, tab:'article', name:'Genitiv', desc:'trotz, während, wegen, statt...', cases:['genitive'], preps:PREPS_BY_CASE.genitive, type:'article', artType:'definite'},
{id:6, tab:'article', name:'Alles gemischt', desc:'All prepositions, definite articles', cases:['accusative','dative','genitive'], preps:[...SENTENCE_ACC_PREPS,...PREPS_BY_CASE.dative,...PREPS_BY_CASE.genitive,...PREPS_BY_CASE.wechsel], type:'article', artType:'definite', mixed:true},
{id:7, tab:'article', name:'ein-Wörter', desc:'Everything with ein/eine/einem...', cases:['accusative','dative','genitive'], preps:[...SENTENCE_ACC_PREPS,...PREPS_BY_CASE.dative,...PREPS_BY_CASE.genitive,...PREPS_BY_CASE.wechsel], type:'article', artType:'indefinite', mixed:true},
{id:7, tab:'article', name:'ein-Wörter', desc:'Everything with ein/eine/einem...', cases:['accusative','dative','genitive'], preps:[...SENTENCE_ACC_PREPS,...PREPS_BY_CASE.dative,...INDEF_GEN_PREPS,...PREPS_BY_CASE.wechsel], type:'article', artType:'indefinite', mixed:true},
// Daily Practice pseudo-levels (one per tab): a one-tap drill that mixes
// questions from every lesson in the tab. Rendered pinned at the top of each
// tab by App.jsx (not inline in the level list) and locked until every
@@ -583,9 +587,12 @@ export const PREP_FRAMES = {
{verbs:['spielen'], nouns:['Hund','Katze','Kind']},
{verbs:['fahren'], nouns:['Auto','Zug']},
],
nach: [{verbs:['kommen'], nouns:['Schule','Arbeit','Nacht','Woche'], enPrep:'after', enDet:'', poss:false}],
nach: [{verbs:['kommen'], nouns:['Schule','Arbeit','Nacht','Woche'], enPrep:'after', enDet:'', poss:false,
indefNouns:['Woche','Nacht']}],
seit: [{verbs:['wohnen'], mid:{de:'hier',en:'here'}, nouns:['Nacht','Woche'], enPrep:'since', enDet:'that',
enVerb:{Er:'has lived',Sie:'has lived',Wir:'have lived',Ich:'have lived'}, poss:false}],
enVerb:{Er:'has lived',Sie:'has lived',Wir:'have lived',Ich:'have lived'}, poss:false,
// "seit einer Woche" is "FOR a week" in English, not "since a week".
indefNouns:['Woche'], enPrepIndef:'for'}],
von: [
{verbs:['kommen'], nouns:VISITABLE, enPossS:true},
{verbs:['kommen'], nouns:['Arbeit','Schule','Büro']},
@@ -859,12 +866,19 @@ export function usableFrames(prepKey, tw, { genderFilter, possOnly, indefOnly }
.filter(f => f.nouns.length);
}
if (indefOnly) {
// ein-word questions: drop frames whose English determiner is a baked-in
// definite idiom (nach der Schule → "after school", seit → "since that…"),
// and drop noIndef nouns — the remaining pairings read naturally with a/an.
// ein-word questions. A frame whose English determiner is a baked-in
// definite idiom ("nach der Schule" → after school) can still work with an
// ein-word if it says which nouns to use instead: "nach einer Woche" is
// fine even though "während einer Woche" is not, so the allowance is
// per-frame rather than per-noun.
frames = frames
.filter(f => f.enDet === undefined && !f.enPossS)
.map(f => ({...f, nouns: f.nouns.filter(n => !NOUN_BY_NAME[n].noIndef)}))
.filter(f => f.indefNouns || f.enDet === undefined)
.map(f => {
const g = {...f, nouns: f.indefNouns || f.nouns.filter(n => !NOUN_BY_NAME[n].noIndef)};
if (f.enPrepIndef) g.enPrep = f.enPrepIndef;
delete g.enDet; // the caller supplies a/an
return g;
})
.filter(f => f.nouns.length);
}
if (genderFilter) {