/** * Wie weit das Glossar den Katalog wirklich abdeckt. * * **Der Anlass.** Die 87 Einträge vor 0.22.0 waren fast durchweg * Rechtsbegriffe. In Kapitel II – Waffentechnik – erreichten nur zwei von fünf * Fragen überhaupt einen Treffer, und selbst diese Treffer waren meist * Rechtsbegriffe wie „Schusswaffe“. Das Glossar ist die WCAG-3.1.3-Zusage * dieser Software; eingelöst wurde sie ausgerechnet in dem Kapitel nicht, * dessen Vokabular Einsteigern am fremdesten ist. * * **Warum gemessen und nicht gezählt.** Eine Zahl wie „118 Einträge“ sagt * nichts darüber, ob sie dort greifen, wo gelernt wird. Gemessen wird deshalb * mit **derselben Begriffssuche**, die auch unter der Frage läuft: Wie viele * Fragen eines Kapitels finden mindestens einen Begriff? */ import { readFileSync } from 'node:fs'; import { join } from 'node:path'; import { describe, expect, it } from 'vitest'; import { begriffssucheBauen, type Glossar } from '../src/shared/glossar'; import type { Katalog } from '../src/shared/katalog'; const WURZEL = join(process.cwd(), '..'); const GLOSSAR = JSON.parse( readFileSync(join(WURZEL, 'content', 'glossar.json'), 'utf8'), ) as Glossar; const KATALOG = JSON.parse( readFileSync(join(WURZEL, 'content', 'katalog', 'katalog.json'), 'utf8'), ) as Katalog; const SUCHE = begriffssucheBauen(GLOSSAR); /** Der Text, den die Oberfläche nach Begriffen durchsucht. */ function volltext(frage: Katalog['fragen'][number]): string { const optionen = (frage.optionen ?? []).map((option) => option.inhalt.text).join(' '); return `${frage.frage.text} ${optionen} ${frage.musterantwort?.text ?? ''}`; } /** Anteil der Fragen eines Kapitels mit mindestens einem Begriff, in Prozent. */ function abdeckung(kapitelId: string): number { const fragen = KATALOG.fragen.filter((frage) => frage.kapitel === kapitelId); const getroffen = fragen.filter((frage) => SUCHE.imText(volltext(frage)).length > 0); return Math.round((100 * getroffen.length) / fragen.length); } describe('Abdeckung des Glossars', () => { it('erreicht in der Waffentechnik mindestens vier von fünf Fragen', () => { /* Der eigentliche Befund: Kapitel II lag bei 40 Prozent. Die Schwelle steht bewusst unter dem gemessenen Wert – sie soll einen Rückfall melden, nicht jede Schwankung. */ expect(abdeckung('II')).toBeGreaterThanOrEqual(80); }); it('fällt in keinem Kapitel unter drei von vier Fragen', () => { for (const kapitel of KATALOG.kapitel) { expect(abdeckung(kapitel.id), kapitel.id).toBeGreaterThanOrEqual(75); } }); it('kennt die tragenden Begriffe der Waffentechnik', () => { /* Namentlich, damit ein stilles Verschwinden auffällt: Ein Schwellwert allein bliebe grün, wenn ein Begriff wegfiele und ein anderer dazukäme. */ const begriffe = new Set(GLOSSAR.eintraege.map((eintrag) => eintrag.begriff)); for (const begriff of [ 'Munition', 'Patrone', 'Kaliber', 'Lauf', 'Verschluss', 'Patronenlager', 'Geschoss', 'Magazin', 'Trommel', 'Revolver', 'Pistole', 'Büchse', 'Flinte', 'Züge und Felder', 'Drall', 'Mündungsenergie', 'Höchstreichweite', ]) { expect(begriffe.has(begriff), begriff).toBe(true); } }); it('kennt die beiden Maßstäbe der Aufbewahrung und hält sie auseinander', () => { /* Widerstandsgrad ist geltendes Recht, Sicherheitsstufe lebt nur über die Übergangsregelung fort. Wer beides verwechselt, verwechselt Bestandsschutz mit Anforderung. */ const widerstand = GLOSSAR.eintraege.find((e) => e.begriff === 'Widerstandsgrad'); const stufe = GLOSSAR.eintraege.find((e) => e.begriff === 'Sicherheitsstufe'); expect(widerstand?.fundstellen.some((f) => f.gesetz === 'AWaffV')).toBe(true); expect(stufe?.text).toContain('Übergangsregelung'); expect(stufe?.kurz).toContain('nicht mehr vor'); }); it('begründet jeden Eintrag ohne Fundstelle', () => { /* Bei der Waffentechnik ist das der Regelfall – sie ist fachliche Praxis, kein Gesetzestext. Das auszusprechen ist die Alternative dazu, eine Fundstelle zu dehnen. */ const stumm = GLOSSAR.eintraege .filter( (eintrag) => eintrag.fundstellen.length === 0 && eintrag.ohneFundstelleGrund === undefined, ) .map((eintrag) => eintrag.begriff); expect(stumm).toEqual([]); }); it('lässt keinen „Siehe auch“-Verweis ins Leere laufen', () => { const begriffe = new Set(GLOSSAR.eintraege.map((eintrag) => eintrag.begriff)); const kaputt = GLOSSAR.eintraege.flatMap((eintrag) => (eintrag.siehe ?? []) .filter((ziel) => !begriffe.has(ziel)) .map((ziel) => `${eintrag.begriff} -> ${ziel}`), ); expect(kaputt).toEqual([]); }); });