import { describe, expect, it } from 'vitest'; import { einstreifigBefunde } from '@/domain/validation/rules'; import { RECHTSGRUNDLAGEN, RECHTSGRUNDLAGEN_EINSTREIFIG, type Rechtsgrundlage, } from '@/domain/rilsa/quellen'; import { createConflict, createEmptyProject, createPhase, createSignalGroup, } from '@/domain/model/factory'; import type { Project, SignalGroup } from '@/domain/model/project'; /* * BEFUND VOM 12.09.2026: EINE UNGEPRUEFTE ANGABE IM TON EINER FESTSTELLUNG. * * Die Regel `einstreifig.ztv-typklasse` schrieb in den Pruefbericht - und * damit in die Anordnungsunterlage -: * * "ZTV transportable LSA 2023 (ARS 07/2024): Typ D erforderlich" * * Herausgeberwerk, Einfuehrungserlass, Anforderung. Wer das las, hielt die * Typklasse fuer belegt. Belegt ist sie nicht: Die ZTV traegt in `quellen.ts` * den Pruefstand `abzugleichen` und keine Fundstelle, weil das Heft nicht * vorliegt. Das ist der Fall von Befund D4 (drei geratene Nummern, alle * falsch) - nur an einer Stelle, die gedruckt wird. * * DIE HAUSREGEL DAZU lautet: "Eine Fundstelle nur nach Abgleich * am Original." Sie ist bisher fuer Fundstellen bewacht, nicht fuer die * AUSSAGE, die aus einer nicht abgeglichenen Quelle stammt. * * WAS DIESE WACHE PRUEFT, IST DER TON UND NICHT DIE BEDINGUNG: Welche * Signalgruppen die Regel zaehlt, bleibt ihre Sache. Sie darf nur nicht * verschweigen, woher die Angabe kommt. * * Das Muster dafuer gibt es im Baum schon: Die Meldungen zum HBS tragen * "(Fundstelle hbs-abminderung, abzugleichen)", `einstreifig.laenge- * behoerdenpraxis` traegt "Behoerdenpraxis ..., kein Regelwerkswert". Die * Typklasse trug nichts dergleichen. */ /** Wendungen, mit denen der Baum einen fehlenden Abgleich benennt. */ const ABGLEICHSVORBEHALT = [ 'eigenen Ausgabe', 'geltenden Fassung', 'Original', 'abzugleichen', 'nicht am Original geprüft', ]; function traegtVorbehalt(text: string): boolean { return ABGLEICHSVORBEHALT.some((w) => text.includes(w)); } /** Eine einstreifige Verkehrsfuehrung, wie sie die Regel bewertet. */ function engstelle(optionen: { readonly mitFurt?: boolean; readonly kfzGruppen?: number } = {}) { const basis = createEmptyProject( 'Arbeitsstelle', new Date('2026-09-12T08:00:00Z'), 'einstreifig', ); const anzahl = optionen.kfzGruppen ?? 2; const kfz = Array.from({ length: anzahl }, (_, i) => createSignalGroup({ name: `A${i + 1}`, mode: 'kfz', index: i, vZul: 50 }), ); const gruppen: SignalGroup[] = [...kfz]; if (optionen.mitFurt === true) { gruppen.push(createSignalGroup({ name: 'F1', mode: 'fuss', index: anzahl })); } const konflikte = []; for (const a of kfz) { for (const b of kfz) { if (a === b) continue; konflikte.push({ ...createConflict(a.id, b.id), clearingDistance: 200, enteringDistance: 0 }); } } const projekt: Project = { ...basis, signalGroups: gruppen, conflicts: konflikte, phases: kfz.map((g, i) => createPhase(`Richtung ${i + 1}`, [g.id])), }; return projekt; } function typklassenbefund(projekt: Project) { const befund = einstreifigBefunde(projekt).find((f) => f.rule === 'einstreifig.ztv-typklasse'); expect(befund, 'Regel einstreifig.ztv-typklasse liefert keinen Befund').toBeDefined(); return befund!; } describe('Aussagen aus nicht abgeglichenen Regelwerken nennen ihren Prüfstand', () => { const alle: readonly Rechtsgrundlage[] = [...RECHTSGRUNDLAGEN, ...RECHTSGRUNDLAGEN_EINSTREIFIG]; it('sagt jede Rechtsgrundlage mit Prüfstand "abzugleichen" in ihrem Hinweis, dass sie es ist', () => { const offen = alle.filter((g) => g.pruefstand === 'abzugleichen'); expect(offen.length, 'keine offene Rechtsgrundlage gefunden').toBeGreaterThan(0); for (const grundlage of offen) { const hinweis = grundlage.hinweis ?? ''; expect( traegtVorbehalt(hinweis), `"${grundlage.bezeichnung}" trägt den Prüfstand "abzugleichen", sagt es aber in ` + `ihrem Hinweis nicht: "${hinweis}"`, ).toBe(true); } }); it('nennt der Typklassenhinweis den fehlenden Abgleich, auch wenn Typ D herauskommt', () => { // Beide Ausloeser einzeln, damit die Wache nicht an einem Zweig haengt. for (const projekt of [engstelle({ mitFurt: true }), engstelle({ kfzGruppen: 3 })]) { const befund = typklassenbefund(projekt); expect(traegtVorbehalt(befund.message), `Meldung ohne Vorbehalt: "${befund.message}"`).toBe( true, ); expect( traegtVorbehalt(befund.reference ?? ''), `Fundstelle ohne Vorbehalt: "${befund.reference ?? ''}"`, ).toBe(true); } }); it('nennt auch der Hinweis ohne Typ D den fehlenden Abgleich', () => { const befund = typklassenbefund(engstelle()); expect(traegtVorbehalt(befund.message)).toBe(true); expect(traegtVorbehalt(befund.reference ?? '')).toBe(true); }); it('behauptet der Titel die Typklasse nicht als erwiesene Anforderung', () => { /* * Der Titel ist die Zeile, die in Pruefbericht und Ausdruck allein steht; * wer nur ihn liest, liest die ganze Aussage. "Typ D erforderlich" ist * dort eine Feststellung ueber ein Regelwerk, das niemand aufgeschlagen * hat. */ for (const projekt of [engstelle({ mitFurt: true }), engstelle({ kfzGruppen: 3 })]) { expect(typklassenbefund(projekt).title).not.toContain('Typ D erforderlich'); } }); it('sagt die Meldung weiterhin, WELCHE Gruppen den Typ D auslösen', () => { // Gegenprobe zur Behebung: Der Vorbehalt darf die Auskunft nicht ersetzen. // Sonst waere aus einer zu starken Aussage eine nutzlose geworden. expect(typklassenbefund(engstelle({ mitFurt: true })).message).toContain('F1'); expect(typklassenbefund(engstelle({ kfzGruppen: 3 })).message).toContain('kreuzende Ströme'); expect(typklassenbefund(engstelle()).message).toContain('A bis D'); }); it('hängt der Vorbehalt am Prüfstand und nicht an jeder Meldung', () => { /* * Gegenprobe: Die Zwischenzeit der einstreifigen Verkehrsfuehrung rechnet * seit 5.27.0 nach der GEKAUFTEN RiLSA 2015, Abschnitt 5.2.2. Ihre * Meldung darf die Fundstelle ohne Vorbehalt nennen - sonst lehrt diese * Wache, jede Aussage zu relativieren, und der Unterschied zwischen * belegt und ungeprueft geht genau dort verloren, wo er zaehlt. */ const verfahren = einstreifigBefunde(engstelle()).find( (f) => f.rule === 'einstreifig.verfahren-abschnitt-5-2', ); expect(verfahren, 'Verfahrenshinweis fehlt').toBeDefined(); expect(verfahren!.reference).toContain('RiLSA 2015'); expect(traegtVorbehalt(verfahren!.reference ?? '')).toBe(false); }); });