/** * Querverweise zwischen verwandten Fragen. * * **Der Anlass.** Der amtliche Katalog kennt keine Frage-zu-Frage-Beziehung, * obwohl die Bündel auf der Hand liegen: II-34 fragt, was ein Schalldämpfer * bewirkt, II-36 nach der Umkehrung – die Erklärung zu II-36 sagt das selbst * im Text. Wer eine davon verstanden hat, erfuhr bis 0.22.0 nie, dass es die * anderen gibt. * * Geprüft wird die Sammelregel an gebauten Fällen und der **ausgelieferte * Bestand** auf Beidseitigkeit: Ein einseitiger Verweis zeigt die Verbindung * nur dem, der zufällig von der richtigen Seite kommt. */ import { readFileSync } from 'node:fs'; import { join } from 'node:path'; import { describe, expect, it } from 'vitest'; import { verwandteSammeln, type Erklaerung, type Erklaerungen } from '../src/shared/erklaerungen'; import type { Katalog } from '../src/shared/katalog'; const WURZEL = join(process.cwd(), '..'); const ERKLAERUNGEN = JSON.parse( readFileSync(join(WURZEL, 'content', 'erklaerungen.json'), 'utf8'), ) as Erklaerungen; const KATALOG = JSON.parse( readFileSync(join(WURZEL, 'content', 'katalog', 'katalog.json'), 'utf8'), ) as Katalog; function nachschlagen(bestand: Record) { return (frageId: string): Erklaerung | null => { const verwandt = bestand[frageId]; return verwandt === undefined ? null : { kurz: 'k', text: 't', fundstellen: [], ohneFundstelleGrund: 'g', verwandt }; }; } describe('verwandteSammeln', () => { it('sammelt die Verwandten mehrerer Fragen', () => { const zu = nachschlagen({ 'II-34': ['II-33', 'II-36'], 'I.5-02': ['I.5-03'] }); expect(verwandteSammeln(zu, ['II-34', 'I.5-02'])).toEqual(['II-33', 'II-36', 'I.5-03']); }); it('lässt weg, was schon in der Sitzung stand', () => { // Wer II-33 gerade bearbeitet hat, braucht sie nicht als „verwandt“ // vorgeschlagen zu bekommen. const zu = nachschlagen({ 'II-34': ['II-33', 'II-36'], 'II-33': ['II-34', 'II-36'] }); expect(verwandteSammeln(zu, ['II-34', 'II-33'])).toEqual(['II-36']); }); it('nennt keine Frage zweimal', () => { const zu = nachschlagen({ 'II-34': ['II-36'], 'II-33': ['II-36'] }); expect(verwandteSammeln(zu, ['II-34', 'II-33'])).toEqual(['II-36']); }); it('behält die Reihenfolge der Begegnung bei', () => { /* Nach Kennung zu sortieren ginge schief: „I.2-99“ kommt im Katalog vor „I.2-135“, lexikografisch steht es dahinter. */ const zu = nachschlagen({ a: ['I.2-135'], b: ['I.2-99'] }); expect(verwandteSammeln(zu, ['a', 'b'])).toEqual(['I.2-135', 'I.2-99']); }); it('kommt mit Fragen ohne Erklärung zurecht', () => { expect(verwandteSammeln(() => null, ['II-34'])).toEqual([]); expect(verwandteSammeln(nachschlagen({}), [])).toEqual([]); }); }); describe('Der ausgelieferte Bestand', () => { const mitVerwandten = Object.entries(ERKLAERUNGEN.zuFrage).filter( ([, eintrag]) => eintrag.verwandt !== undefined, ); const bekannt = new Set(KATALOG.fragen.map((frage) => frage.id)); it('führt überhaupt Querverweise', () => { expect(mitVerwandten.length).toBeGreaterThan(50); }); it('verweist nur auf Fragen, die es gibt', () => { const unbekannt = mitVerwandten.flatMap(([frageId, eintrag]) => (eintrag.verwandt ?? []) .filter((ziel) => !bekannt.has(ziel)) .map((ziel) => `${frageId} -> ${ziel}`), ); expect(unbekannt).toEqual([]); }); it('verweist nirgends auf sich selbst', () => { const selbstbezug = mitVerwandten .filter(([frageId, eintrag]) => (eintrag.verwandt ?? []).includes(frageId)) .map(([frageId]) => frageId); expect(selbstbezug).toEqual([]); }); it('hält jeden Verweis beidseitig', () => { /* Die eigentliche Zusage. Ein einseitiger Verweis ist fast immer ein Versehen beim Schreiben – und er zeigt die Verbindung nur dem, der zufällig von der richtigen Seite kommt. */ const einseitig = mitVerwandten.flatMap(([frageId, eintrag]) => (eintrag.verwandt ?? []) .filter((ziel) => !(ERKLAERUNGEN.zuFrage[ziel]?.verwandt ?? []).includes(frageId)) .map((ziel) => `${frageId} nennt ${ziel}, aber nicht zurück`), ); expect(einseitig).toEqual([]); }); it('nennt keine Frage doppelt in derselben Liste', () => { const doppelt = mitVerwandten .filter(([, eintrag]) => { const liste = eintrag.verwandt ?? []; return new Set(liste).size !== liste.length; }) .map(([frageId]) => frageId); expect(doppelt).toEqual([]); }); it('kennt das Schalldämpfer-Bündel, mit dem der Befund begann', () => { // II-36 nennt sich in der eigenen Erklärung „die Umkehrung von Frage 34“. expect(ERKLAERUNGEN.zuFrage['II-36']?.verwandt).toContain('II-34'); expect(ERKLAERUNGEN.zuFrage['II-34']?.verwandt).toContain('II-36'); }); it('verbindet auch über Kapitelgrenzen hinweg', () => { /* I.1-07 fragt nach der waffenrechtlichen Einordnung des Double-Action-Revolvers, II-47 nach der Technik der Double-Action- Pistole. Genau solche Paare findet man im Katalog nicht von allein. */ expect(ERKLAERUNGEN.zuFrage['I.1-07']?.verwandt).toContain('II-47'); }); });