/** * Die mitgelieferten Normtexte. * * Zwei Sorten Prüfung: die Auflösungsregel an gebauten Fällen – dort lässt * sich die Mehrdeutigkeit gezielt herstellen – und der **ganze ausgelieferte * Bestand** gegen alle 2123 Fundstellen der Erklärungen und des Glossars. * Letzteres ist die eigentliche Zusage: Wer „Im Gesetz nachlesen“ liest, soll * etwas vorfinden. */ import { readFileSync } from 'node:fs'; import { join } from 'node:path'; import { describe, expect, it } from 'vitest'; import type { Erklaerungen, Fundstelle } from '../src/shared/erklaerungen'; import type { Glossar } from '../src/shared/glossar'; import { auszugBilden, blockFinden, NORMTEXTE_LEER, zeigtAufAbbildung, type Anlagenblock, type Normtexte, } from '../src/shared/normtexte'; const WURZEL = join(process.cwd(), '..'); function inhalt(datei: string): unknown { return JSON.parse(readFileSync(join(WURZEL, 'content', datei), 'utf8')); } /* Gelesen wird der ausgelieferte Bestand selbst, nicht eine Attrappe: Die Zusage lautet, dass jede der 2123 Fundstellen etwas vorfindet – und das lässt sich nur an der echten Datei prüfen. */ const NORMTEXTE = inhalt('normtexte.json') as Normtexte; const ERKLAERUNGEN = inhalt('erklaerungen.json') as Erklaerungen; const GLOSSAR = inhalt('glossar.json') as Glossar; const ALLE_FUNDSTELLEN: readonly Fundstelle[] = [ ...Object.values(ERKLAERUNGEN.zuFrage).flatMap((eintrag) => eintrag.fundstellen), ...GLOSSAR.eintraege.flatMap((eintrag) => eintrag.fundstellen), ]; function block(marke: string | null, pfad: string[], text = 'Text'): Anlagenblock { return { marke, pfad, text }; } describe('blockFinden', () => { const bloecke: Anlagenblock[] = [ block(null, []), block('Abschnitt 1', []), block('Unterabschnitt 1', ['Abschnitt 1']), block('1.1', ['Abschnitt 1', 'Unterabschnitt 1'], 'Schusswaffen sind …'), block('1.3.1.3', ['Abschnitt 1', 'Unterabschnitt 1'], 'Tiefe Nummer'), block('Unterabschnitt 3', ['Abschnitt 1']), block('1.1', ['Abschnitt 1', 'Unterabschnitt 3'], 'Etwas ganz anderes'), block('Abschnitt 2', []), block('1.1', ['Abschnitt 2'], 'Und noch etwas anderes'), ]; it('trifft die Nummer im richtigen Unterabschnitt', () => { /* Der Kern der Sache: „1.1“ kommt hier dreimal vor, in Anlage 1 des WaffG sogar viermal. Ohne den Pfad spränge die Fundstelle unbemerkt an die falsche Stelle des Gesetzes. */ expect(blockFinden(bloecke, 'Abschnitt 1 Unterabschnitt 1 Nr. 1.1')?.text).toBe( 'Schusswaffen sind …', ); expect(blockFinden(bloecke, 'Abschnitt 1 Unterabschnitt 3 Nr. 1.1')?.text).toBe( 'Etwas ganz anderes', ); expect(blockFinden(bloecke, 'Abschnitt 2 Nr. 1.1')?.text).toBe('Und noch etwas anderes'); }); it('nimmt die tiefste genannte Nummer', () => { expect(blockFinden(bloecke, 'Abschnitt 1 Unterabschnitt 1 Nr. 1.3.1.3')?.marke).toBe('1.3.1.3'); }); it('findet einen Abschnitt auch ohne Nummer', () => { expect(blockFinden(bloecke, 'Abschnitt 2')?.marke).toBe('Abschnitt 2'); expect(blockFinden(bloecke, 'Abschnitt 1 Unterabschnitt 3')?.marke).toBe('Unterabschnitt 3'); }); it('gibt bei Mehrdeutigkeit lieber nichts zurück', () => { // „Nr. 1.1“ allein passt auf drei Blöcke. Einen davon zu wählen hieße, // zwei Drittel der Leser an die falsche Stelle zu schicken. expect(blockFinden(bloecke, 'Nr. 1.1')).toBeNull(); }); it('gibt bei fehlendem Treffer nichts zurück', () => { expect(blockFinden(bloecke, 'Abschnitt 9 Nr. 4.2')).toBeNull(); expect(blockFinden(bloecke, 'Abbildung 5b')).toBeNull(); expect(blockFinden(bloecke, '')).toBeNull(); }); }); describe('zeigtAufAbbildung', () => { it('erkennt die Abbildungen der Anlage II', () => { expect(zeigtAufAbbildung('Abbildung 5b')).toBe(true); expect(zeigtAufAbbildung('Abbildung 3 – Ortszeichen der zuständigen Behörden')).toBe(true); }); it('hält gewöhnliche Stellen auseinander', () => { expect(zeigtAufAbbildung('Abschnitt 1 Unterabschnitt 1 Nr. 1.1')).toBe(false); }); }); describe('auszugBilden', () => { it('liefert den zitierten Absatz', () => { const auszug = auszugBilden(NORMTEXTE, { gesetz: 'WaffG', norm: '§ 1', absatz: '1' }); expect(auszug.art).toBe('absatz'); expect(auszug.art === 'absatz' ? auszug.text : '').toContain('Umgang mit Waffen oder Munition'); }); it('setzt eine Norm ohne Absatzangabe aus ihren Absätzen zusammen', () => { const auszug = auszugBilden(NORMTEXTE, { gesetz: 'WaffG', norm: '§ 1' }); expect(auszug.art).toBe('norm'); expect(auszug.art === 'norm' ? auszug.text : '').toContain('(1)'); }); it('meldet ehrlich, wenn nichts vorliegt', () => { expect(auszugBilden(NORMTEXTE_LEER, { gesetz: 'WaffG', norm: '§ 1' }).art).toBe('fehlt'); expect(auszugBilden(NORMTEXTE, { gesetz: 'StGB', norm: '§ 173' }).art).toBe('fehlt'); }); it('zeigt bei einer Anlage den getroffenen Block', () => { const auszug = auszugBilden(NORMTEXTE, { gesetz: 'WaffG', norm: 'Anlage 1', stelle: 'Abschnitt 1 Unterabschnitt 1 Nr. 1.1', }); expect(auszug.art).toBe('block'); expect(auszug.art === 'block' ? auszug.block.text : '').toContain('Schusswaffen sind'); }); it('zeigt die ganze Anlage, wenn die Stelle eine Abbildung ist', () => { const auszug = auszugBilden(NORMTEXTE, { gesetz: 'BeschussV', norm: 'Anlage II', stelle: 'Abbildung 5b', }); expect(auszug.art).toBe('anlage'); expect(auszug.art === 'anlage' ? auszug.abbildung : false).toBe(true); expect(auszug.art === 'anlage' ? auszug.bloecke.length : 0).toBeGreaterThan(0); }); }); describe('Der ausgelieferte Bestand', () => { it('enthält nur die zitierten Normen', () => { /* Der Index führt die sieben Gesetze vollständig – 914 Normen, darunter katalogfremde wie § 173 StGB. Ausgeliefert wird der Auszug. */ const zitiert = new Set(ALLE_FUNDSTELLEN.map((f) => `${f.gesetz}|${f.norm}`)); const geliefert = Object.entries(NORMTEXTE.gesetze).flatMap(([kuerzel, gesetz]) => Object.keys(gesetz.normen).map((norm) => `${kuerzel}|${norm}`), ); expect(geliefert.length).toBeGreaterThan(0); for (const norm of geliefert) { expect(zitiert.has(norm)).toBe(true); } }); it('kennt jede Norm, auf die eine Fundstelle zeigt', () => { const fehlend = ALLE_FUNDSTELLEN.filter( (f) => NORMTEXTE.gesetze[f.gesetz]?.normen[f.norm] === undefined, ).map((f) => `${f.gesetz} ${f.norm}`); expect([...new Set(fehlend)]).toEqual([]); }); it('kennt jeden zitierten Absatz', () => { /* Die eigentliche Zusage: 1501 Paragrafenzitate, und keines davon läuft ins Leere. Ohne diesen Test bliebe eine Lücke unbemerkt, bis sie jemandem beim Lernen begegnet. */ const fehlend = ALLE_FUNDSTELLEN.filter((f) => { const norm = NORMTEXTE.gesetze[f.gesetz]?.normen[f.norm]; return ( norm !== undefined && !norm.istAnlage && f.absatz !== undefined && norm.absaetze?.[f.absatz] === undefined ); }).map((f) => `${f.gesetz} ${f.norm} Abs. ${f.absatz ?? ''}`); expect([...new Set(fehlend)]).toEqual([]); }); it('löst mindestens neun von zehn Anlagenzitaten eindeutig auf', () => { /* Kein Anspruch auf hundert Prozent, und das mit Grund: Die Abbildungen der Anlage II BeschussV stehen im amtlichen XML nur als Bildunterschrift. Was nicht auflöst, zeigt die ganze Anlage und sagt, warum – falsch wird dabei nichts. */ const anlagenzitate = ALLE_FUNDSTELLEN.filter( (f) => NORMTEXTE.gesetze[f.gesetz]?.normen[f.norm]?.istAnlage === true, ); const getroffen = anlagenzitate.filter( (f) => auszugBilden(NORMTEXTE, f).art === 'block', ).length; expect(anlagenzitate.length).toBeGreaterThan(600); expect(getroffen / anlagenzitate.length).toBeGreaterThan(0.9); }); it('liefert zu jeder Fundstelle etwas Anzeigbares', () => { const leer = ALLE_FUNDSTELLEN.filter((f) => auszugBilden(NORMTEXTE, f).art === 'fehlt'); expect(leer).toEqual([]); }); it('steht auf demselben Gesetzesstand wie Erklärungen und Glossar', () => { /* Die eine Falle des Bauwegs: Nach einer Gesetzesänderung wird der Index neu erzeugt und die Fundstellen werden gegen ihn geprüft – wer dann vergisst, `normtexte_bauen.py` laufen zu lassen, liefert den Wortlaut des alten Standes zu Fundstellen des neuen aus. Von außen sähe das tadellos aus: Jede Norm da, jeder Absatz da, nur eben von gestern. */ expect(NORMTEXTE.meta.gesetzesstand).toEqual(ERKLAERUNGEN.meta.gesetzesstand); expect(NORMTEXTE.meta.gesetzesstand).toEqual(GLOSSAR.meta.gesetzesstand); }); it('nennt zu jedem Gesetz den Änderungsstand', () => { // Ein Gesetzestext ohne Fassungsangabe ist eine Behauptung über heute, // die morgen falsch sein kann. for (const [kuerzel, gesetz] of Object.entries(NORMTEXTE.gesetze)) { expect(gesetz.stand.length, kuerzel).toBeGreaterThan(0); expect(NORMTEXTE.meta.gesetzesstand[kuerzel]).toBe(gesetz.stand); } }); it('hält den Wortlaut der Anlagen verlustfrei', () => { /* Die Pipeline rechnet das beim Bauen nach; hier steht die Zusage noch einmal am ausgelieferten Bestand. Ein beschnittenes Gesetzeszitat wäre der schlimmste Fehler, den diese Anwendung machen kann – deshalb wird nirgends geschnitten, nur zerlegt. */ for (const gesetz of Object.values(NORMTEXTE.gesetze)) { for (const [name, norm] of Object.entries(gesetz.normen)) { if (!norm.istAnlage) { continue; } const bloecke = norm.bloecke ?? []; expect(bloecke.length, name).toBeGreaterThan(0); // Jeder Block beginnt mit seiner eigenen Marke – die Marke bleibt im // Text stehen, sie wird nicht in ein Feld herausgezogen. for (const block of bloecke) { if (block.marke !== null) { expect(block.text.trimStart().startsWith(block.marke), `${name} ${block.marke}`).toBe( true, ); } } } } }); });