/** * 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 2138 Fundstellen der Erklärungen und des Glossars. * Letzteres ist die eigentliche Zusage: Wer „Im Gesetz nachlesen“ liest, soll * etwas vorfinden. */ import { readdirSync, 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 2138 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 Gesetzesabzug sagt, wann er gezogen wurde', () => { /* Die Wache, die gefehlt hat. `content/gesetze/index.json` führte zu jedem Gesetz die amtliche Standangabe („Zuletzt geändert durch …“) – aber nirgends stand, wann jemand zuletzt nachgesehen hat, ob es dabei geblieben ist. Alle 1854 Fundstellen werden bei jedem Lauf gegen genau diesen Abzug geprüft; ist er veraltet, meldet `pruefe_erklaerungen.py` weiter grün, gegen eine Vorschrift, die es so nicht mehr gibt. Am 01.09.2026 lagen zwischen dem ältesten Abzug (Beschussgesetz, 09.09.2020) und dem jüngsten (Sprengstoffgesetz, 23.07.2026) fast sechs Jahre. Geprüft wird hier ausdrücklich **nicht** das Alter. Ob eine Vorschrift sich geändert hat, kann nur ein Mensch nachsehen, und eine Prüfung, die allein durch Zeitablauf rot wird, meldet an ihrem Tag keinen Befund, sondern eine Störung – dieselbe Bauart, die in `selbstsicherung.test.ts` schon einmal zugeschlagen hat. Geprüft wird, dass die Angabe **da** ist; hinsehen muss `docs/veroeffentlichen.md`. */ const index = JSON.parse( readFileSync(join(WURZEL, 'content', 'gesetze', 'index.json'), 'utf8'), ) as { gesetze: Record }; it('trägt zu jedem Gesetz ein Abzugsdatum', () => { const ohne = Object.entries(index.gesetze) .filter(([, gesetz]) => gesetz.abzug === undefined) .map(([kuerzel]) => kuerzel); expect(ohne, 'Neu erzeugen mit: python data-pipeline/gesetze_index.py').toEqual([]); }); it('schreibt das Abzugsdatum als ISO-Datum', () => { for (const [kuerzel, gesetz] of Object.entries(index.gesetze)) { expect(gesetz.abzug, `Abzugsdatum von ${kuerzel}`).toMatch(/^\d{4}-\d{2}-\d{2}$/u); } }); it('trägt zu jedem Gesetz auch die amtliche Standangabe', () => { /* Beides zusammen ergibt erst die Auskunft: Der Abzug sagt, wann nachgesehen wurde, die Standangabe, was dabei zu sehen war. */ for (const [kuerzel, gesetz] of Object.entries(index.gesetze)) { expect(gesetz.stand.length, `Standangabe von ${kuerzel}`).toBeGreaterThan(10); } }); it('nennt den Änderungsstand und nicht die Neufassungs-Bekanntmachung', () => { /* Die Wache, die gefehlt hat – und der Grund, warum sie hier steht. Das amtliche XML führt die Standangabe je nach Gesetz ein- oder zweimal: Wer seit seiner Neufassung geändert wurde, trägt zuerst einen Eintrag vom Typ `Neuf` („Neugefasst durch Bek. v. …“) und danach den vom Typ `Stand` („zuletzt geändert durch …“). `gesetze_index.py` las mit `standangabe/standkommentar` blind den ersten. Bei StGB, SprengG und 1. SprengV – zusammen 40 der 148 ausgelieferten Normen – stand dadurch unter dem Wortlaut die Bekanntmachung statt des Änderungsstands; beim StGB die von 1998 statt der von 2026. Zweite Wirkung, und der Grund für die Schwere: Der Freigabeschritt „Gesetzesstand ansehen“ in `docs/veroeffentlichen.md` vergleicht die Angabe mit gesetze-im-internet.de. Eine Neufassungs-Bekanntmachung ändert sich durch keine Gesetzesänderung – die Aktualitätsprüfung war für diese drei Gesetze dauerhaft wirkungslos. Geprüft wird ohne die Zuordnung Kürzel → Datei, die das Werkzeug führt: Aus jedem XML kommt die Angabe, die dort gelten muss (der `Stand`-Eintrag, sonst der einzige vorhandene), und jede Angabe im Index muss eine davon sein. So schlägt der Fall auch an, wenn der Index gegenüber einem neu geladenen XML zurückbleibt. */ const zulaessig = new Set(); for (const name of readdirSync(join(WURZEL, 'content', 'gesetze'))) { if (!name.endsWith('.xml')) { continue; } const roh = readFileSync(join(WURZEL, 'content', 'gesetze', name), 'utf8'); /* Nur der erste Datensatz trägt die Angaben zum Gesetz als Ganzes – dieselbe Stelle, die `gesetze_index.py` liest. */ const kopf = roh.slice(0, roh.indexOf('')); let erste = ''; let stand = ''; for (const treffer of kopf.matchAll( /]*>\s*([^<]*)<\/standtyp>\s*([^<]*)<\/standkommentar>/gu, )) { const typ = treffer[1] ?? ''; const text = (treffer[2] ?? '').trim(); if (erste === '') { erste = text; } if (typ === 'Stand') { stand = text; } } zulaessig.add(stand === '' ? erste : stand); } expect(zulaessig.size, 'Standangaben aus content/gesetze/*.xml').toBe(7); for (const [kuerzel, gesetz] of Object.entries(index.gesetze)) { expect( [...zulaessig], `Standangabe von ${kuerzel} – neu erzeugen mit: python data-pipeline/gesetze_index.py`, ).toContain(gesetz.stand); } }); it('gibt die Standangaben unverändert an die Normtexte weiter', () => { /* `normtexte_bauen.py` schreibt `meta.gesetzesstand` aus dem Index ab. Läuft nur einer der beiden Erzeuger, zeigt die Anwendung eine andere Angabe als die, gegen die geprüft wurde. */ expect(Object.fromEntries(Object.entries(index.gesetze).map(([k, g]) => [k, g.stand]))).toEqual( NORMTEXTE.meta.gesetzesstand, ); }); it('wird von der Veröffentlichungsliste tatsächlich angesehen', () => { /* Ohne diesen Fall wäre die Angabe da und niemand sähe sie an. Der Schritt in der Liste ist der einzige Ort, an dem aus dem Datum eine Handlung wird. */ const liste = readFileSync(join(WURZEL, 'docs', 'veroeffentlichen.md'), 'utf8'); expect(liste).toContain('Gesetzesstand ansehen'); expect(liste).toContain('gesetze_index.py'); }); }); describe('Die Zahlen über den Bestand sind gezählt', () => { /* Die Wache, die gefehlt hat – dreimal in Folge. „588 von 632“, „43 von 44 Abbildungen“, „1853 Fundstellen“, „2123“, „2137“, „503 KiB“: Zahlen über diesen Bestand standen in `docs/stand.md`, im Kopf von `src/shared/normtexte.ts`, im Kopf dieser Datei und in `data-pipeline/normtexte_bauen.py` – jede von Hand abgeschrieben, jede beim nächsten Redaktionsschritt still veraltet. Zweimal wurde an einer Stelle berichtigt, während die anderen stehen blieben. Gezählt wird deshalb hier, aus dem Bestand selbst, mit derselben Funktion, die auch die Anzeige benutzt. Die Dokumente müssen die gezählte Zahl tragen; wer eine Fundstelle ergänzt, sieht sofort, wo sie nachzuführen ist. `docs/stand.md` ist nach CLAUDE.md die einzige Stelle für den Umsetzungsstand – dann muss sie auch stimmen. */ const stand = readFileSync(join(WURZEL, 'docs', 'stand.md'), 'utf8'); const modulkopf = readFileSync(join(process.cwd(), 'src', 'shared', 'normtexte.ts'), 'utf8'); const anlagenzitate = ALLE_FUNDSTELLEN.filter((f) => f.norm.startsWith('Anlage')); const absatzzitate = ALLE_FUNDSTELLEN.filter( (f) => !f.norm.startsWith('Anlage') && f.absatz !== undefined, ); const auszuege = anlagenzitate.map((f) => auszugBilden(NORMTEXTE, f)); const getroffen = auszuege.filter((a) => a.art === 'block').length; const offen = auszuege.filter((a) => a.art === 'anlage'); const abbildungen = offen.filter((a) => a.abbildung).length; it('nennt in docs/stand.md die gezählte Zahl der Fundstellen', () => { const erklaerungen = Object.values(ERKLAERUNGEN.zuFrage).flatMap((e) => e.fundstellen).length; /* Vier Stellen, alle in der Gegenwartsform – nicht die Rückblicke auf einzelne Behebungen, die eine damalige Zahl nennen dürfen. */ const zahl = String(erklaerungen); expect(stand).toContain(`Alle 575 Fragen, ${zahl} vollständig nachgewiesene`); expect(stand).toContain(`jede der ${zahl} Fundstellen **existiert**`); expect(stand).toContain(`sind es **${zahl} = ${zahl} + 0**`); expect(stand).toContain(`heute sind es **${zahl}**`); }); const absaetze = String(absatzzitate.length); const anlagen = String(anlagenzitate.length); const genau = String(getroffen); const rest = String(offen.length); const bilder = String(abbildungen); it('nennt in docs/stand.md die gezählte Auflösungsquote der Anlagenzitate', () => { expect(stand).toContain( `${absaetze} von ${absaetze} Absatzzitaten und ` + `${genau} von ${anlagen} Anlagenzitaten treffen genau`, ); expect(stand).toContain(`${bilder} der ${rest} nennen eine Abbildung`); expect(stand).toContain(`über alle ${String(ALLE_FUNDSTELLEN.length)} Fundstellen`); }); it('nennt im Kopf von normtexte.ts dieselben gezählten Zahlen', () => { expect(modulkopf).toContain(`Von ${anlagen} Anlagenzitaten lösen sich ${genau} eindeutig auf.`); expect(modulkopf).toContain(`verbleibenden ${rest} nennen ${bilder}`); expect(modulkopf).toContain(`den ${rest} nicht auflösbaren Zitaten`); }); }); 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, ); } } } } }); }); describe('Buchstabenzusätze: Absätze und Nummern', () => { /* Zwei Befunde der Prüfrunde zu 0.27.2, beide am amtlichen Wortlaut. Erstens die Reihenfolge. `Object.entries` gibt bei einem Objekt zuerst alle ganzzahligen Schlüssel aufsteigend zurück und danach erst die übrigen. Ein Paragraf mit den Absätzen 1, 2, 2a, 3 erschien deshalb als 1, 2, 3, 2a — in einer Ansicht, die zusagt, der Text werde „nicht gekürzt, nicht zusammengesetzt und nicht umsortiert“. Nachgemessen an `content/normtexte.json`: acht Normen sind betroffen, darunter WaffG § 42 und § 32. Zweitens die Fundstellen. Das Muster für „Nr. …“ las nur Ziffern und Punkte; aus „Nr. 2a“ wurde die Zahl 2, und gesucht wurde der Block mit der Marke „2“ — die Überschrift eine Ebene darüber. Acht Zitate in Erklärungen und Glossar nennen „Nr. 2a“ oder „Nr. 2b“, und **kein einziger** Block im Bestand trägt eine Marke mit Buchstaben. Der Auszug zeigte damit eine andere Stelle als die zitierte. */ const MIT_BUCHSTABEN: Normtexte = { meta: { version: 1, stand: '2026-09-03', hinweis: 'Prüfstand', gesetzesstand: {} }, gesetze: { WaffG: { bezeichnung: 'Waffengesetz', stand: 'Prüfstand', quelle: 'gesetze-im-internet.de', normen: { '§ 42': { titel: 'Verbot des Führens', istAnlage: false, absaetze: { '1': 'Erster Absatz.', '2': 'Zweiter Absatz.', '3': 'Dritter Absatz.', '4': 'Vierter Absatz.', '4a': 'Absatz vier a.', '5': 'Fünfter Absatz.', }, }, 'Anlage 2': { titel: '(zu § 2 Abs. 2 bis 4)', istAnlage: true, bloecke: [ { marke: '2', pfad: ['Abschnitt 2', 'Unterabschnitt 2'], text: '2.\nErlaubnisfreier Erwerb durch Inhaber einer Waffenbesitzkarte', }, ], }, }, }, }, }; it('lässt die Absätze in der Reihenfolge der Quelle', () => { const auszug = auszugBilden(MIT_BUCHSTABEN, { gesetz: 'WaffG', norm: '§ 42' }); expect(auszug.art).toBe('norm'); if (auszug.art !== 'norm') return; const stellen = ['(1)', '(2)', '(3)', '(4)', '(4a)', '(5)'].map((m) => auszug.text.indexOf(m)); expect(stellen.every((stelle) => stelle >= 0)).toBe(true); expect([...stellen].sort((a, b) => a - b)).toEqual(stellen); }); it('springt bei „Nr. 2a“ nicht auf den Block „2“', () => { const auszug = auszugBilden(MIT_BUCHSTABEN, { gesetz: 'WaffG', norm: 'Anlage 2', stelle: 'Abschnitt 2 Unterabschnitt 2 Nr. 2a', }); /* Kein Block trägt die Marke „2a“. Richtig ist dann die ganze Anlage mit dem Hinweis, dass die Stelle nicht genau zu treffen war – nicht der Block eine Ebene darüber. */ expect(auszug.art).toBe('anlage'); }); it('trifft „Nr. 2“ weiterhin genau', () => { /* Die Gegenprobe: Das schärfere Muster darf die gewöhnlichen Nummern nicht verlieren. */ const auszug = auszugBilden(MIT_BUCHSTABEN, { gesetz: 'WaffG', norm: 'Anlage 2', stelle: 'Abschnitt 2 Unterabschnitt 2 Nr. 2', }); expect(auszug.art).toBe('block'); }); });