From e448045e527d981079161de3559c02a48cab6233 Mon Sep 17 00:00:00 2001 From: George Turkington Date: Thu, 1 Oct 2026 06:39:45 +0000 Subject: [PATCH] =?UTF-8?q?ein-W=C3=B6rter=20was=20quietly=20dropping=20th?= =?UTF-8?q?ree=20of=20the=20nine=20dative=20prepositions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 Claude-Session: https://claude.ai/code/session_017Uza6mhTpAvSwMQuFc4Cz8 --- src/corpus.txt | 39 +++++++++++++++++++++++++++++++++ src/engine.js | 30 +++++++++++++++++++------- src/engine.test.js | 54 ++++++++++++++++++++++++++++++++++++++++------ 3 files changed, 109 insertions(+), 14 deletions(-) diff --git a/src/corpus.txt b/src/corpus.txt index 37e7c77..d55ca61 100644 --- a/src/corpus.txt +++ b/src/corpus.txt @@ -3214,6 +3214,24 @@ INDEF|außer|dative|kommen|Mädchen Alle kommen außer einem Mädch INDEF|außer|dative|kommen|Mann Alle kommen außer einem Mann. Everyone comes except a man. INDEF|außerhalb|genitive|arbeiten|Stadt Er arbeitet außerhalb einer Stadt. He works outside a city. INDEF|außerhalb|genitive|wohnen|Stadt Er wohnt außerhalb einer Stadt. He lives outside a city. +INDEF|bei|dative|sein|Arzt Er ist bei einem Arzt. He is at a doctor's. +INDEF|bei|dative|sein|Ärztin Er ist bei einer Ärztin. He is at a doctor's. +INDEF|bei|dative|sein|Freund Er ist bei einem Freund. He is at a friend's. +INDEF|bei|dative|sein|Freundin Er ist bei einer Freundin. He is at a friend's. +INDEF|bei|dative|sein|Lehrer Er ist bei einem Lehrer. He is at a teacher's. +INDEF|bei|dative|sein|Lehrerin Er ist bei einer Lehrerin. He is at a teacher's. +INDEF|bei|dative|warten|Arzt Er wartet bei einem Arzt. He waits at a doctor's. +INDEF|bei|dative|warten|Ärztin Er wartet bei einer Ärztin. He waits at a doctor's. +INDEF|bei|dative|warten|Freund Er wartet bei einem Freund. He waits at a friend's. +INDEF|bei|dative|warten|Freundin Er wartet bei einer Freundin. He waits at a friend's. +INDEF|bei|dative|warten|Lehrer Er wartet bei einem Lehrer. He waits at a teacher's. +INDEF|bei|dative|warten|Lehrerin Er wartet bei einer Lehrerin. He waits at a teacher's. +INDEF|bei|dative|wohnen|Arzt Er wohnt bei einem Arzt. He lives at a doctor's. +INDEF|bei|dative|wohnen|Ärztin Er wohnt bei einer Ärztin. He lives at a doctor's. +INDEF|bei|dative|wohnen|Freund Er wohnt bei einem Freund. He lives at a friend's. +INDEF|bei|dative|wohnen|Freundin Er wohnt bei einer Freundin. He lives at a friend's. +INDEF|bei|dative|wohnen|Lehrer Er wohnt bei einem Lehrer. He lives at a teacher's. +INDEF|bei|dative|wohnen|Lehrerin Er wohnt bei einer Lehrerin. He lives at a teacher's. INDEF|durch|accusative|fahren|Garten Er fährt durch einen Garten. He drives through a garden. INDEF|durch|accusative|fahren|Park Er fährt durch einen Park. He drives through a park. INDEF|durch|accusative|fahren|Stadt Er fährt durch eine Stadt. He drives through a city. @@ -3517,6 +3535,8 @@ INDEF|mit|dative|sprechen|Lehrer Er spricht mit einem Lehrer. INDEF|mit|dative|sprechen|Lehrerin Er spricht mit einer Lehrerin. He talks with a teacher. INDEF|mit|dative|sprechen|Mädchen Er spricht mit einem Mädchen. He talks with a girl. INDEF|mit|dative|sprechen|Mann Er spricht mit einem Mann. He talks with a man. +INDEF|nach|dative|kommen|Nacht Er kommt nach einer Nacht. He comes after a night. +INDEF|nach|dative|kommen|Woche Er kommt nach einer Woche. He comes after a week. INDEF|neben|accusative|legen|Bett Er legt das Buch neben ein Bett. He puts the book next to a bed. INDEF|neben|accusative|legen|Sofa Er legt das Buch neben ein Sofa. He puts the book next to a sofa. INDEF|neben|accusative|legen|Stuhl Er legt das Buch neben einen Stuhl. He puts the book next to a chair. @@ -3610,6 +3630,7 @@ INDEF|ohne|accusative|reisen|Lehrer Er reist ohne einen Lehrer. INDEF|ohne|accusative|reisen|Lehrerin Er reist ohne eine Lehrerin. He travels without a teacher. INDEF|ohne|accusative|reisen|Mädchen Er reist ohne ein Mädchen. He travels without a girl. INDEF|ohne|accusative|reisen|Mann Er reist ohne einen Mann. He travels without a man. +INDEF|seit|dative|wohnen|Woche Er wohnt hier seit einer Woche. He has lived here for a week. INDEF|statt|genitive|nehmen|Auto Er nimmt den Bus statt eines Autos. He takes the bus instead of a car. INDEF|statt|genitive|nehmen|Zug Er nimmt den Bus statt eines Zuges. He takes the bus instead of a train. INDEF|über|accusative|fahren|Brücke Er fährt über eine Brücke. He drives over a bridge. @@ -3707,7 +3728,13 @@ INDEF|unter|dative|warten|Baum Er wartet unter einem Baum. INDEF|unter|dative|warten|Brücke Er wartet unter einer Brücke. He waits under a bridge. INDEF|unter|dative|warten|Dach Er wartet unter einem Dach. He waits under a roof. INDEF|unter|dative|wohnen|Dach Er wohnt unter einem Dach. He lives under a roof. +INDEF|von|dative|kommen|Arzt Er kommt von einem Arzt. He comes from a doctor's. +INDEF|von|dative|kommen|Ärztin Er kommt von einer Ärztin. He comes from a doctor's. INDEF|von|dative|kommen|Büro Er kommt von einem Büro. He comes from an office. +INDEF|von|dative|kommen|Freund Er kommt von einem Freund. He comes from a friend's. +INDEF|von|dative|kommen|Freundin Er kommt von einer Freundin. He comes from a friend's. +INDEF|von|dative|kommen|Lehrer Er kommt von einem Lehrer. He comes from a teacher's. +INDEF|von|dative|kommen|Lehrerin Er kommt von einer Lehrerin. He comes from a teacher's. INDEF|von|dative|kommen|Schule Er kommt von einer Schule. He comes from a school. INDEF|vor|accusative|fahren|Bahnhof Er fährt vor einen Bahnhof. He drives in front of a station. INDEF|vor|accusative|fahren|Haus Er fährt vor ein Haus. He drives in front of a house. @@ -3775,14 +3802,26 @@ INDEF|während|genitive|schlafen|Konzert Er schläft während eines Konze INDEF|wegen|genitive|bleiben|Hund Er bleibt wegen eines Hundes. He stays because of a dog. INDEF|wegen|genitive|bleiben|Katze Er bleibt wegen einer Katze. He stays because of a cat. INDEF|wegen|genitive|bleiben|Kind Er bleibt wegen eines Kindes. He stays because of a child. +INDEF|zu|dative|fahren|Arzt Er fährt zu einem Arzt. He drives to a doctor's. +INDEF|zu|dative|fahren|Ärztin Er fährt zu einer Ärztin. He drives to a doctor's. INDEF|zu|dative|fahren|Büro Er fährt zu einem Büro. He drives to an office. +INDEF|zu|dative|fahren|Freund Er fährt zu einem Freund. He drives to a friend's. +INDEF|zu|dative|fahren|Freundin Er fährt zu einer Freundin. He drives to a friend's. INDEF|zu|dative|fahren|Hotel Er fährt zu einem Hotel. He drives to a hotel. +INDEF|zu|dative|fahren|Lehrer Er fährt zu einem Lehrer. He drives to a teacher's. +INDEF|zu|dative|fahren|Lehrerin Er fährt zu einer Lehrerin. He drives to a teacher's. INDEF|zu|dative|fahren|Museum Er fährt zu einem Museum. He drives to a museum. INDEF|zu|dative|fahren|Park Er fährt zu einem Park. He drives to a park. INDEF|zu|dative|fahren|Restaurant Er fährt zu einem Restaurant. He drives to a restaurant. INDEF|zu|dative|fahren|Schule Er fährt zu einer Schule. He drives to a school. +INDEF|zu|dative|gehen|Arzt Er geht zu einem Arzt. He walks to a doctor's. +INDEF|zu|dative|gehen|Ärztin Er geht zu einer Ärztin. He walks to a doctor's. INDEF|zu|dative|gehen|Büro Er geht zu einem Büro. He walks to an office. +INDEF|zu|dative|gehen|Freund Er geht zu einem Freund. He walks to a friend's. +INDEF|zu|dative|gehen|Freundin Er geht zu einer Freundin. He walks to a friend's. INDEF|zu|dative|gehen|Hotel Er geht zu einem Hotel. He walks to a hotel. +INDEF|zu|dative|gehen|Lehrer Er geht zu einem Lehrer. He walks to a teacher's. +INDEF|zu|dative|gehen|Lehrerin Er geht zu einer Lehrerin. He walks to a teacher's. INDEF|zu|dative|gehen|Museum Er geht zu einem Museum. He walks to a museum. INDEF|zu|dative|gehen|Park Er geht zu einem Park. He walks to a park. INDEF|zu|dative|gehen|Restaurant Er geht zu einem Restaurant. He walks to a restaurant. diff --git a/src/engine.js b/src/engine.js index b4a672d..37cf5f6 100644 --- a/src/engine.js +++ b/src/engine.js @@ -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) { diff --git a/src/engine.test.js b/src/engine.test.js index 8d174aa..3baf43e 100644 --- a/src/engine.test.js +++ b/src/engine.test.js @@ -12,6 +12,8 @@ import { EIN_WORD_DETS, ADJ_ALL_ENDINGS, } from './engine.js'; +const lvl7Has = (qs, p) => qs.some(q => q.prepKey === p); + const runSession = (levelId, n) => { const s = createSession(levelId); return Array.from({ length: n }, () => s.next()); @@ -115,15 +117,55 @@ describe('article questions', () => { expect([...seen].sort()).toEqual([...lv.preps].sort()); }); - it('ein-Wörter never pairs an ein-word with a mass/definite-only noun', () => { - for (const q of runSession(7, 400)) { - expect(q.noun.noIndef, `„ein ${q.noun.w}" should never be generated`).toBeFalsy(); + // The guard that would have caught ein-Wörter silently losing bei/nach/seit: + // a level that lists a preposition must actually be able to ask about it. + // If a pairing is impossible (zu always fuses, trotz only takes uncountables) + // the honest fix is to drop it from the level's list, not to leave a prep + // advertised that never appears. + it.each(LEVELS.filter(l => l.preps).map(l => [l.id]))( + 'level %s generates every preposition it advertises', (id) => { + const lv = LEVELS.find(l => l.id === id); + const seen = new Set(runSession(id, lv.preps.length * 40).map(q => q.prepKey)); + const missing = lv.preps.filter(p => !seen.has(p)); + expect(missing, `level ${id} lists ${missing.join(', ')} but can never generate them`).toEqual([]); + }); + + it('ein-Wörter reaches every case, including the dative', () => { + const qs = runSession(7, 600); + for (const c of ['accusative', 'dative', 'genitive']) { + expect(qs.some(q => q.cas === c), `ein-Wörter never generated a ${c}`).toBe(true); + } + // and the dative must come from real dative prepositions, not only from + // Wechsel preps that happened to land on dative + const datPreps = PREPS_BY_CASE.dative.filter(p => lvl7Has(qs, p)); + expect(datPreps.length, `only ${datPreps.join(',')} covered`).toBeGreaterThanOrEqual(8); + }); + + it('ein-Wörter never pairs an ein-word with an uncountable noun', () => { + // noIndef means "not with ein unless a frame explicitly re-admits it": + // "eine Woche" is good German, "ein Wetter" is not. These can never be + // re-admitted by any frame. + const UNCOUNTABLE = ['Wasser','Wetter','Regen','Musik','Arbeit','Geld','Zeit', + 'Milch','Brot','Bier','Wein','Kaffee','Tee']; + for (const q of runSession(7, 600)) { + expect(UNCOUNTABLE, `„${q.ans} ${q.noun.w}" should never be generated`).not.toContain(q.noun.w); } }); - it('ein-Wörter never uses definite-idiom frames (nach, seit) or all-mass preps (trotz)', () => { - for (const q of runSession(7, 400)) { - expect(['nach', 'seit', 'trotz']).not.toContain(q.prepKey); + it('a frame may re-admit a countable noun that the global flag blocks', () => { + const qs = runSession(7, 1200); + // "nach einer Woche" and "seit einer Woche" are textbook German … + expect(qs.some(q => q.prepKey === 'nach' && q.noun.w === 'Woche')).toBe(true); + expect(qs.some(q => q.prepKey === 'seit' && q.noun.w === 'Woche')).toBe(true); + // … while "während einer Woche", the original bug, stays impossible. + expect(qs.some(q => q.prepKey === 'während' && ['Woche','Nacht'].includes(q.noun.w))).toBe(false); + }); + + it('ein-Wörter drops only the preps that genuinely cannot take an ein-word', () => { + const lv = LEVELS.find(l => l.id === 7); + expect(lv.preps, 'trotz only governs uncountables').not.toContain('trotz'); + for (const p of ['bei', 'nach', 'seit', 'von', 'zu', 'mit', 'aus']) { + expect(lv.preps, `${p} works fine with an ein-word`).toContain(p); } });