lsa-planer

LSA-Planer Professional – Planungssoftware für Lichtsignalanlagen nach RiLSA 2015 und § 45 StVO. EUPL-1.2.

/ tests domain regelwerksaussageOhneAbgleich.test.ts

6,6 KB Rohdatei
tests/domain/regelwerksaussageOhneAbgleich.test.ts — 165 Zeilen
1 import { describe, expect, it } from 'vitest';
2 import { einstreifigBefunde } from '@/domain/validation/rules';
3 import {
4 RECHTSGRUNDLAGEN,
5 RECHTSGRUNDLAGEN_EINSTREIFIG,
6 type Rechtsgrundlage,
7 } from '@/domain/rilsa/quellen';
8 import {
9 createConflict,
10 createEmptyProject,
11 createPhase,
12 createSignalGroup,
13 } from '@/domain/model/factory';
14 import type { Project, SignalGroup } from '@/domain/model/project';
15
16 /*
17 * BEFUND VOM 12.09.2026: EINE UNGEPRUEFTE ANGABE IM TON EINER FESTSTELLUNG.
18 *
19 * Die Regel `einstreifig.ztv-typklasse` schrieb in den Pruefbericht - und
20 * damit in die Anordnungsunterlage -:
21 *
22 * "ZTV transportable LSA 2023 (ARS 07/2024): Typ D erforderlich"
23 *
24 * Herausgeberwerk, Einfuehrungserlass, Anforderung. Wer das las, hielt die
25 * Typklasse fuer belegt. Belegt ist sie nicht: Die ZTV traegt in `quellen.ts`
26 * den Pruefstand `abzugleichen` und keine Fundstelle, weil das Heft nicht
27 * vorliegt. Das ist der Fall von Befund D4 (drei geratene Nummern, alle
28 * falsch) - nur an einer Stelle, die gedruckt wird.
29 *
30 * DIE HAUSREGEL DAZU lautet: "Eine Fundstelle nur nach Abgleich
31 * am Original." Sie ist bisher fuer Fundstellen bewacht, nicht fuer die
32 * AUSSAGE, die aus einer nicht abgeglichenen Quelle stammt.
33 *
34 * WAS DIESE WACHE PRUEFT, IST DER TON UND NICHT DIE BEDINGUNG: Welche
35 * Signalgruppen die Regel zaehlt, bleibt ihre Sache. Sie darf nur nicht
36 * verschweigen, woher die Angabe kommt.
37 *
38 * Das Muster dafuer gibt es im Baum schon: Die Meldungen zum HBS tragen
39 * "(Fundstelle hbs-abminderung, abzugleichen)", `einstreifig.laenge-
40 * behoerdenpraxis` traegt "Behoerdenpraxis ..., kein Regelwerkswert". Die
41 * Typklasse trug nichts dergleichen.
42 */
43
44 /** Wendungen, mit denen der Baum einen fehlenden Abgleich benennt. */
45 const ABGLEICHSVORBEHALT = [
46 'eigenen Ausgabe',
47 'geltenden Fassung',
48 'Original',
49 'abzugleichen',
50 'nicht am Original geprüft',
51 ];
52
53 function traegtVorbehalt(text: string): boolean {
54 return ABGLEICHSVORBEHALT.some((w) => text.includes(w));
55 }
56
57 /** Eine einstreifige Verkehrsfuehrung, wie sie die Regel bewertet. */
58 function engstelle(optionen: { readonly mitFurt?: boolean; readonly kfzGruppen?: number } = {}) {
59 const basis = createEmptyProject(
60 'Arbeitsstelle',
61 new Date('2026-09-12T08:00:00Z'),
62 'einstreifig',
63 );
64 const anzahl = optionen.kfzGruppen ?? 2;
65 const kfz = Array.from({ length: anzahl }, (_, i) =>
66 createSignalGroup({ name: `A${i + 1}`, mode: 'kfz', index: i, vZul: 50 }),
67 );
68 const gruppen: SignalGroup[] = [...kfz];
69 if (optionen.mitFurt === true) {
70 gruppen.push(createSignalGroup({ name: 'F1', mode: 'fuss', index: anzahl }));
71 }
72 const konflikte = [];
73 for (const a of kfz) {
74 for (const b of kfz) {
75 if (a === b) continue;
76 konflikte.push({ ...createConflict(a.id, b.id), clearingDistance: 200, enteringDistance: 0 });
77 }
78 }
79 const projekt: Project = {
80 ...basis,
81 signalGroups: gruppen,
82 conflicts: konflikte,
83 phases: kfz.map((g, i) => createPhase(`Richtung ${i + 1}`, [g.id])),
84 };
85 return projekt;
86 }
87
88 function typklassenbefund(projekt: Project) {
89 const befund = einstreifigBefunde(projekt).find((f) => f.rule === 'einstreifig.ztv-typklasse');
90 expect(befund, 'Regel einstreifig.ztv-typklasse liefert keinen Befund').toBeDefined();
91 return befund!;
92 }
93
94 describe('Aussagen aus nicht abgeglichenen Regelwerken nennen ihren Prüfstand', () => {
95 const alle: readonly Rechtsgrundlage[] = [...RECHTSGRUNDLAGEN, ...RECHTSGRUNDLAGEN_EINSTREIFIG];
96
97 it('sagt jede Rechtsgrundlage mit Prüfstand "abzugleichen" in ihrem Hinweis, dass sie es ist', () => {
98 const offen = alle.filter((g) => g.pruefstand === 'abzugleichen');
99 expect(offen.length, 'keine offene Rechtsgrundlage gefunden').toBeGreaterThan(0);
100 for (const grundlage of offen) {
101 const hinweis = grundlage.hinweis ?? '';
102 expect(
103 traegtVorbehalt(hinweis),
104 `"${grundlage.bezeichnung}" trägt den Prüfstand "abzugleichen", sagt es aber in ` +
105 `ihrem Hinweis nicht: "${hinweis}"`,
106 ).toBe(true);
107 }
108 });
109
110 it('nennt der Typklassenhinweis den fehlenden Abgleich, auch wenn Typ D herauskommt', () => {
111 // Beide Ausloeser einzeln, damit die Wache nicht an einem Zweig haengt.
112 for (const projekt of [engstelle({ mitFurt: true }), engstelle({ kfzGruppen: 3 })]) {
113 const befund = typklassenbefund(projekt);
114 expect(traegtVorbehalt(befund.message), `Meldung ohne Vorbehalt: "${befund.message}"`).toBe(
115 true,
116 );
117 expect(
118 traegtVorbehalt(befund.reference ?? ''),
119 `Fundstelle ohne Vorbehalt: "${befund.reference ?? ''}"`,
120 ).toBe(true);
121 }
122 });
123
124 it('nennt auch der Hinweis ohne Typ D den fehlenden Abgleich', () => {
125 const befund = typklassenbefund(engstelle());
126 expect(traegtVorbehalt(befund.message)).toBe(true);
127 expect(traegtVorbehalt(befund.reference ?? '')).toBe(true);
128 });
129
130 it('behauptet der Titel die Typklasse nicht als erwiesene Anforderung', () => {
131 /*
132 * Der Titel ist die Zeile, die in Pruefbericht und Ausdruck allein steht;
133 * wer nur ihn liest, liest die ganze Aussage. "Typ D erforderlich" ist
134 * dort eine Feststellung ueber ein Regelwerk, das niemand aufgeschlagen
135 * hat.
136 */
137 for (const projekt of [engstelle({ mitFurt: true }), engstelle({ kfzGruppen: 3 })]) {
138 expect(typklassenbefund(projekt).title).not.toContain('Typ D erforderlich');
139 }
140 });
141
142 it('sagt die Meldung weiterhin, WELCHE Gruppen den Typ D auslösen', () => {
143 // Gegenprobe zur Behebung: Der Vorbehalt darf die Auskunft nicht ersetzen.
144 // Sonst waere aus einer zu starken Aussage eine nutzlose geworden.
145 expect(typklassenbefund(engstelle({ mitFurt: true })).message).toContain('F1');
146 expect(typklassenbefund(engstelle({ kfzGruppen: 3 })).message).toContain('kreuzende Ströme');
147 expect(typklassenbefund(engstelle()).message).toContain('A bis D');
148 });
149
150 it('hängt der Vorbehalt am Prüfstand und nicht an jeder Meldung', () => {
151 /*
152 * Gegenprobe: Die Zwischenzeit der einstreifigen Verkehrsfuehrung rechnet
153 * seit 5.27.0 nach der GEKAUFTEN RiLSA 2015, Abschnitt 5.2.2. Ihre
154 * Meldung darf die Fundstelle ohne Vorbehalt nennen - sonst lehrt diese
155 * Wache, jede Aussage zu relativieren, und der Unterschied zwischen
156 * belegt und ungeprueft geht genau dort verloren, wo er zaehlt.
157 */
158 const verfahren = einstreifigBefunde(engstelle()).find(
159 (f) => f.rule === 'einstreifig.verfahren-abschnitt-5-2',
160 );
161 expect(verfahren, 'Verfahrenshinweis fehlt').toBeDefined();
162 expect(verfahren!.reference).toContain('RiLSA 2015');
163 expect(traegtVorbehalt(verfahren!.reference ?? '')).toBe(false);
164 });
165 });