lsa-planer

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

/ tests domain pruefberichtQualitaetsstufen.test.ts

20,1 KB Rohdatei
tests/domain/pruefberichtQualitaetsstufen.test.ts — 464 Zeilen
1 import { readFileSync } from 'node:fs';
2 import { describe, expect, it } from 'vitest';
3 import { validateProject } from '@/domain/validation/engine';
4 import { buildSignalPlan } from '@/domain/plan/signalPlan';
5 import {
6 createConflict,
7 createEmptyProject,
8 createPhase,
9 createSignalGroup,
10 createStandardIntersectionProject,
11 } from '@/domain/model/factory';
12 import type { Anlagenart, Project } from '@/domain/model/project';
13
14 /*
15 * Pruefbericht zur Qualitaetsstufe seit Fassung 5.4.0 (Befunde
16 * B1, B2, B4).
17 *
18 * Bis dahin lief jede Furt ueber die Kfz-Rechnung mit Kapazitaet 0, erhielt
19 * formal die Stufe F, und der Bericht erklaerte die eigene Stufe fuer
20 * aussagelos ("Wartezeit nicht bestimmbar - bei Fussgaengerfurten ist das der
21 * Regelfall"). Die Kfz-Meldung schrieb "nach HBS", waehrend immer die
22 * HCM-Formel gerechnet wurde, und nannte nicht, woran die Stufe haengt.
23 *
24 * Jetzt: Fussgaenger und Radverkehr werden nach der maximalen Wartezeit
25 * (laengste Sperrzeit) bewertet - ohne Verkehrsstaerke, auch an der
26 * Fussgaengerschutzanlage; Kfz und OePNV nennen Verfahren und Kriterium; nach
27 * HCM sagt der Bericht, dass Fussgaenger und Radverkehr nicht bewertet werden.
28 */
29
30 const STICHTAG = new Date('2026-01-01T00:00:00Z');
31
32 function vollstaendig(): Project {
33 const base = createStandardIntersectionProject('Prueffall', STICHTAG);
34 return {
35 ...base,
36 meta: {
37 ...base.meta,
38 projectNumber: 'VP-2026-001',
39 client: 'Stadt Musterhausen',
40 planner: 'O. Willerding',
41 location: 'Hauptstrasse / Bahnhofstrasse',
42 },
43 };
44 }
45
46 /**
47 * Eine Fahrbahn, eine Furt, feste Umlaufzeit und feste Freigabezeit der Furt.
48 * Die Sperrzeit der Furt ist Umlaufzeit abzueglich Freigabezeit und
49 * Uebergaengen - bei 120 s Umlauf und 10 s Freigabe rund 110 s.
50 */
51 function furtprojekt(
52 umlaufzeit: number,
53 gruenFurt: number,
54 anlagenart: Anlagenart,
55 furtart: 'fuss' | 'rad' = 'fuss',
56 ): Project {
57 const basis = createEmptyProject('Furt', STICHTAG, anlagenart);
58 const kfz = createSignalGroup({ name: 'K1', mode: 'kfz', index: 0 });
59 const furt = createSignalGroup({ name: 'F1', mode: furtart, index: 1 });
60 const phase1 = createPhase('Fahrverkehr', [kfz.id]);
61 const phase2 = createPhase('Furt', [furt.id]);
62 const konflikte = [
63 { ...createConflict(kfz.id, furt.id), clearingDistance: 25, enteringDistance: 2 },
64 { ...createConflict(furt.id, kfz.id), clearingDistance: 12, enteringDistance: 5 },
65 ];
66 return {
67 ...basis,
68 meta: {
69 ...basis.meta,
70 projectNumber: 'VP-2026-002',
71 client: 'Stadt Musterhausen',
72 planner: 'O. Willerding',
73 location: 'Furt',
74 },
75 signalGroups: [kfz, furt],
76 conflicts: konflikte,
77 phases: [phase1, { ...phase2, manualGreen: gruenFurt }],
78 program: {
79 ...basis.program,
80 phaseOrder: [phase1.id, phase2.id],
81 method: 'manuell',
82 manualCycleTime: umlaufzeit,
83 },
84 };
85 }
86
87 function qualitaetsstufen(project: Project) {
88 const plan = buildSignalPlan(project);
89 const bericht = validateProject(project, plan, STICHTAG);
90 return {
91 plan,
92 bericht,
93 stufen: bericht.findings.filter((f) => f.rule === 'leistungsfaehigkeit.qualitaetsstufe'),
94 };
95 }
96
97 describe('Fussgaenger und Radverkehr: maximale Wartezeit statt "nicht bestimmbar"', () => {
98 it('meldet fuer eine Furt mit langer Sperrzeit die Stufe F nach der maximalen Wartezeit', () => {
99 // Umlauf 120 s, Furt 10 s Freigabe: Sperrzeit rund 110 s, Tafel F (> 85 s).
100 const { plan, stufen } = qualitaetsstufen(furtprojekt(120, 10, 'knotenpunkt'));
101 const furt = plan.groups.find((g) => g.name === 'F1')!;
102 expect(furt.delay?.serviceLevel?.grade).toBe('F');
103
104 const befund = stufen.find((f) => f.target?.label === 'F1');
105 expect(befund, 'Die Furt muss eine Stufenmeldung erhalten').toBeDefined();
106 expect(befund!.severity).toBe('warnung');
107 expect(befund!.title).toBe('Qualitätsstufe F für "F1"');
108 expect(befund!.message).toContain('maximale Wartezeit');
109 expect(befund!.message).toContain('längste Sperrzeit im Umlauf');
110 expect(befund!.message).toContain('nach HBS 2015');
111 expect(befund!.message).toContain('Fußgänger und Radverkehr: maximale Wartezeit');
112 expect(befund!.message).not.toContain('mittlere Wartezeit');
113 expect(befund!.message).not.toContain('nicht bestimmbar');
114 expect(befund!.reference).toBe('HBS 2015 - Qualitätsstufen des Verkehrsablaufs');
115 expect(befund!.suggestion).toContain('Sperrzeit');
116
117 // Der genannte Wert ist der Wert des Plans - nachrechenbar aus Umlauf und
118 // Freigabezeit, nicht eine zweite Zahl.
119 const maximal = furt.delay!.maximumDelay;
120 expect(maximal).toBeGreaterThan(85);
121 expect(befund!.message).toContain(`${String(maximal).replace('.', ',')} s`);
122 });
123
124 it('bewertet auch ohne erfasste Verkehrsstaerken - die Furt braucht keine', () => {
125 const projekt = furtprojekt(120, 10, 'knotenpunkt');
126 expect(projekt.demands).toHaveLength(0);
127 const { bericht, stufen } = qualitaetsstufen(projekt);
128 // Fuer den Kfz-Strom fehlt die Verkehrsstaerke - das bleibt eine Warnung ...
129 expect(
130 bericht.findings.some((f) => f.rule === 'leistungsfaehigkeit.keine-verkehrsstaerken'),
131 ).toBe(true);
132 // ... die Furt wird trotzdem bewertet.
133 expect(stufen.some((f) => f.target?.label === 'F1')).toBe(true);
134 });
135
136 it('bewertet den Radverkehr genauso wie die Fussgaengerfurt', () => {
137 const { stufen } = qualitaetsstufen(furtprojekt(120, 10, 'knotenpunkt', 'rad'));
138 const befund = stufen.find((f) => f.target?.label === 'F1');
139 expect(befund).toBeDefined();
140 expect(befund!.message).toContain('maximale Wartezeit');
141 expect(befund!.message).toContain('Fußgänger und Radverkehr: maximale Wartezeit');
142 });
143
144 it('prueft die Furt auch an der Fussgaengerschutzanlage - dort ist die Wartezeit das Mass', () => {
145 const { bericht, stufen } = qualitaetsstufen(furtprojekt(120, 10, 'fussgaengerschutzanlage'));
146 const befund = stufen.find((f) => f.target?.label === 'F1');
147 expect(befund).toBeDefined();
148 expect(befund!.message).toContain('maximale Wartezeit');
149 // Die Kfz-Leistungsfaehigkeit bleibt dort ausser Betracht: kein
150 // Auslastungsgrad, keine Forderung nach Verkehrsstaerken.
151 expect(
152 bericht.findings.some(
153 (f) =>
154 f.rule === 'leistungsfaehigkeit.keine-verkehrsstaerken' ||
155 f.rule === 'leistungsfaehigkeit.ueberlastet' ||
156 (f.rule === 'leistungsfaehigkeit.qualitaetsstufe' && f.target?.label === 'K1'),
157 ),
158 ).toBe(false);
159 });
160
161 it('stuft E als Hinweis ein und schweigt bei A bis D', () => {
162 // 100 s Umlauf, 20 s Freigabe: Sperrzeit 80 s -> E (70 < tS <= 85).
163 const e = qualitaetsstufen(furtprojekt(100, 20, 'knotenpunkt'));
164 const furtE = e.plan.groups.find((g) => g.name === 'F1')!;
165 expect(furtE.delay?.maximumDelay).toBe(80);
166 expect(furtE.delay?.serviceLevel?.grade).toBe('E');
167 const befundE = e.stufen.find((f) => f.target?.label === 'F1');
168 expect(befundE?.severity).toBe('hinweis');
169
170 // 60 s Umlauf, 15 s Freigabe: Sperrzeit 45 s -> C, keine Meldung.
171 const b = qualitaetsstufen(furtprojekt(60, 15, 'knotenpunkt'));
172 const furtB = b.plan.groups.find((g) => g.name === 'F1')!;
173 expect(['A', 'B', 'C', 'D']).toContain(furtB.delay?.serviceLevel?.grade);
174 expect(b.stufen.some((f) => f.target?.label === 'F1')).toBe(false);
175 });
176
177 it('kennt die alte Meldung "nicht bestimmbar" nicht mehr', () => {
178 const quelltext = readFileSync(
179 new URL('../../src/domain/validation/engine.ts', import.meta.url),
180 'utf8',
181 );
182 expect(quelltext).not.toContain('nicht bestimmbar');
183 expect(quelltext).not.toContain('Bei Fußgängerfurten ist das der Regelfall');
184 });
185 });
186
187 describe('Kfz und OePNV: Verfahren und Kriterium stehen an der Stufe', () => {
188 function ueberlastet(scale: 'HBS' | 'HCM'): Project {
189 const base = vollstaendig();
190 return {
191 ...base,
192 settings: { ...base.settings, serviceLevelScale: scale },
193 demands: base.demands.map((d) => ({ ...d, volume: 2500 })),
194 };
195 }
196
197 it('nennt bei Ueberlastung das Kriterium q > C und die Stufe F nach HBS 2015', () => {
198 const { plan, bericht, stufen } = qualitaetsstufen(ueberlastet('HBS'));
199 const k1 = plan.groups.find((g) => g.name === 'K1')!;
200 expect(k1.delay?.serviceLevel?.kriterium).toBe('ueberlastung');
201
202 const befund = stufen.find((f) => f.target?.label === 'K1');
203 expect(befund).toBeDefined();
204 expect(befund!.severity).toBe('warnung');
205 expect(befund!.message).toContain('ist überlastet (q > C)');
206 expect(befund!.message).toContain(
207 'Nach HBS 2015 ist das die Stufe F unabhängig von der Wartezeit',
208 );
209 // q und C stehen in der Meldung - sonst liesse sich q > C nicht nachrechnen.
210 expect(befund!.message).toContain('2500 Fz/h');
211 expect(befund!.message).toContain(`${k1.capacity!.capacity} Fz/h`);
212 expect(befund!.reference).toBe('HBS 2015 - Qualitätsstufen des Verkehrsablaufs');
213
214 // Die Auslastungsgrad-Regel selbst bleibt und traegt die HBS-Fundstelle.
215 const ueberlastung = bericht.findings.find(
216 (f) => f.rule === 'leistungsfaehigkeit.ueberlastet' && f.target?.label === 'K1',
217 );
218 expect(ueberlastung?.reference).toBe('HBS 2015 - Kapazität und Auslastungsgrad');
219 });
220
221 it('nennt bei einer Wartezeitstufe das Kriterium mittlere Wartezeit', () => {
222 // K1 auf 1200 Fz/h: x knapp unter 1, mittlere Wartezeit weit ueber 70 s ->
223 // Stufe E (HBS 2015 kennt fuer Kfz keine Wartezeitstufe F).
224 const base = vollstaendig();
225 const projekt: Project = {
226 ...base,
227 demands: base.demands.map((d, i) => (i === 0 ? { ...d, volume: 1200 } : d)),
228 };
229 const { plan, stufen } = qualitaetsstufen(projekt);
230 const k1 = plan.groups.find((g) => g.name === 'K1')!;
231 expect(k1.delay?.serviceLevel?.grade).toBe('E');
232 expect(k1.delay?.serviceLevel?.kriterium).toBe('mittlere-wartezeit');
233
234 const befund = stufen.find((f) => f.target?.label === 'K1');
235 expect(befund).toBeDefined();
236 expect(befund!.severity).toBe('hinweis');
237 expect(befund!.message).toContain('mittlere Wartezeit');
238 expect(befund!.message).toContain('Kriterium: mittlere Wartezeit');
239 expect(befund!.message).toContain('nach HBS 2015');
240 expect(befund!.message).toContain(`${String(k1.delay!.averageDelay).replace('.', ',')} s`);
241 expect(befund!.message).not.toContain('überlastet');
242 });
243
244 it('sagt "HCM" statt "HBS 2015", wenn nach HCM gerechnet wurde', () => {
245 const { plan, stufen } = qualitaetsstufen(ueberlastet('HCM'));
246 const k1 = plan.groups.find((g) => g.name === 'K1')!;
247 expect(k1.delay?.serviceLevel?.scale).toBe('HCM');
248
249 const befund = stufen.find((f) => f.target?.label === 'K1');
250 expect(befund).toBeDefined();
251 expect(befund!.message).toContain('Nach HCM ist das die Stufe F unabhängig von der Wartezeit');
252 expect(befund!.message).not.toContain('HBS');
253 expect(befund!.reference).toBe('HCM - Level of Service');
254 });
255
256 it('weist die Schwellen 0,85 und 0,95 als Praxiswerte aus, nicht als HBS-Zahlen', () => {
257 // K1 auf 1300 Fz/h: K1 ueberlastet, K4 ueber dem Praxiswert, aber nicht
258 // ueberlastet (die Umlaufzeit steigt bis an ihre Obergrenze).
259 //
260 // NACHGEZOGEN MIT FASSUNG 5.43.0 (Gruppe Furt): Hier standen 1500 Fz/h und
261 // K2 als zweite Gruppe. Die Vorlage erfasst seither jede Furt auch gegen
262 // den Strom, der sie beim Ausfahren quert (K2/F2, K4/F1), und K4 beginnt
263 // erst 10 s nach dem Ende von F1. Mit 1500 Fz/h sind dadurch K1, K3 und K4
264 // ueberlastet, und keine Meldung nach Praxiswert bleibt uebrig; mit
265 // 1300 Fz/h liegt K4 bei 0,961 - der Fall, den diese Wache braucht.
266 const base = vollstaendig();
267 const projekt: Project = {
268 ...base,
269 demands: base.demands.map((d, i) => (i === 0 ? { ...d, volume: 1300 } : d)),
270 };
271 const { bericht } = qualitaetsstufen(projekt);
272 const praxis = bericht.findings.filter(
273 (f) => f.rule === 'leistungsfaehigkeit.hoch' || f.rule === 'leistungsfaehigkeit.kritisch',
274 );
275 expect(praxis.length).toBeGreaterThan(0);
276 for (const f of praxis) {
277 expect(f.reference).toContain('Praxiswerte');
278 expect(f.reference).not.toContain('HBS');
279 }
280 });
281 });
282
283 describe('Nach HCM: Fussgaenger und Radverkehr werden nicht bewertet - und der Bericht sagt es', () => {
284 it('gibt einen Hinweis je Projekt, nicht je Gruppe', () => {
285 const base = furtprojekt(120, 10, 'knotenpunkt');
286 const rad = createSignalGroup({ name: 'R1', mode: 'rad', index: 2 });
287 const projekt: Project = {
288 ...base,
289 settings: { ...base.settings, serviceLevelScale: 'HCM' },
290 signalGroups: [...base.signalGroups, rad],
291 phases: base.phases.map((p, i) =>
292 i === 1 ? { ...p, signalGroupIds: [...p.signalGroupIds, rad.id] } : p,
293 ),
294 };
295 const { plan, bericht, stufen } = qualitaetsstufen(projekt);
296 // Voraussetzung: keine Stufe fuer Furt und Radfurt.
297 expect(plan.groups.find((g) => g.name === 'F1')!.delay?.serviceLevel).toBeNull();
298 expect(plan.groups.find((g) => g.name === 'R1')!.delay?.serviceLevel).toBeNull();
299
300 const hinweise = bericht.findings.filter(
301 (f) => f.rule === 'leistungsfaehigkeit.fuss-rad-nicht-bewertet',
302 );
303 expect(hinweise, 'genau ein Hinweis fuer beide Gruppen').toHaveLength(1);
304 expect(hinweise[0]!.severity).toBe('hinweis');
305 expect(hinweise[0]!.title).toContain('nicht bewertet');
306 expect(hinweise[0]!.message).toContain('"F1"');
307 expect(hinweise[0]!.message).toContain('"R1"');
308 expect(hinweise[0]!.message).toContain('maximale Wartezeit');
309 expect(hinweise[0]!.reference).toBe('HCM - Level of Service');
310 // Keine Stufenmeldung fuer die Furten - es gibt keine Stufe.
311 expect(stufen.some((f) => f.target?.label === 'F1' || f.target?.label === 'R1')).toBe(false);
312 });
313
314 it('schweigt nach HBS und ohne Fuss- oder Radgruppen', () => {
315 const hbs = qualitaetsstufen(furtprojekt(120, 10, 'knotenpunkt'));
316 expect(
317 hbs.bericht.findings.some((f) => f.rule === 'leistungsfaehigkeit.fuss-rad-nicht-bewertet'),
318 ).toBe(false);
319
320 const base = vollstaendig();
321 const nurKfz: Project = {
322 ...base,
323 settings: { ...base.settings, serviceLevelScale: 'HCM' },
324 signalGroups: base.signalGroups.filter((g) => g.mode === 'kfz'),
325 conflicts: base.conflicts.filter((c) =>
326 base.signalGroups
327 .filter((g) => g.mode === 'kfz')
328 .some((g) => g.id === c.fromId || g.id === c.toId),
329 ),
330 phases: base.phases.map((p) => ({
331 ...p,
332 signalGroupIds: p.signalGroupIds.filter((id) =>
333 base.signalGroups.some((g) => g.id === id && g.mode === 'kfz'),
334 ),
335 })),
336 };
337 expect(
338 qualitaetsstufen(nurKfz).bericht.findings.some(
339 (f) => f.rule === 'leistungsfaehigkeit.fuss-rad-nicht-bewertet',
340 ),
341 ).toBe(false);
342 });
343 });
344
345 describe('Verkehrsstaerken werden nur verlangt, wo eine Saettigungsverkehrsstaerke gilt', () => {
346 it('fordert bei einem Projekt nur aus Furt und Radfurt keine Verkehrsstaerken', () => {
347 // Vor Fassung 5.4.0 loeste jede Gruppe mit mode !== 'fuss'
348 // die Forderung aus - also auch der Radverkehr, der nach HBS 2015 keine
349 // Saettigungsverkehrsstaerke hat.
350 const basis = createEmptyProject('Rad und Fuss', STICHTAG, 'knotenpunkt');
351 const rad = createSignalGroup({ name: 'R1', mode: 'rad', index: 0 });
352 const fuss = createSignalGroup({ name: 'F1', mode: 'fuss', index: 1 });
353 const phase1 = createPhase('Rad', [rad.id]);
354 const phase2 = createPhase('Fuss', [fuss.id]);
355 const projekt: Project = {
356 ...basis,
357 signalGroups: [rad, fuss],
358 conflicts: [
359 { ...createConflict(rad.id, fuss.id), clearingDistance: 10, enteringDistance: 3 },
360 { ...createConflict(fuss.id, rad.id), clearingDistance: 6, enteringDistance: 3 },
361 ],
362 phases: [phase1, phase2],
363 program: { ...basis.program, phaseOrder: [phase1.id, phase2.id] },
364 };
365 const bericht = validateProject(projekt, undefined, STICHTAG);
366 expect(
367 bericht.findings.some((f) => f.rule === 'leistungsfaehigkeit.keine-verkehrsstaerken'),
368 ).toBe(false);
369 });
370
371 it('fordert sie weiterhin, sobald ein Kfz-Strom ohne Verkehrsstaerke dabei ist', () => {
372 const base = vollstaendig();
373 const bericht = validateProject({ ...base, demands: [] }, undefined, STICHTAG);
374 expect(
375 bericht.findings.some((f) => f.rule === 'leistungsfaehigkeit.keine-verkehrsstaerken'),
376 ).toBe(true);
377 });
378 });
379
380 describe('Gespeicherte Verkehrsstaerken ohne Rechenwirkung', () => {
381 it('nennt eine Radverkehrsstaerke aus einer aelteren Datei, die in keine Rechnung mehr eingeht', () => {
382 // Bis Fassung 5.3.5 ging die Verkehrsstaerke einer Radgruppe mit einer
383 // Kfz-Saettigungsverkehrsstaerke in Kapazitaet und Umlaufzeit ein. Seit
384 // Fassung 5.4.0 (Befund B4) nicht mehr - das darf beim
385 // Oeffnen einer Altdatei nicht still geschehen.
386 const base = vollstaendig();
387 const rad = createSignalGroup({ name: 'R1', mode: 'rad', index: base.signalGroups.length });
388 const projekt: Project = {
389 ...base,
390 signalGroups: [...base.signalGroups, rad],
391 phases: base.phases.map((p, i) =>
392 i === 0 ? { ...p, signalGroupIds: [...p.signalGroupIds, rad.id] } : p,
393 ),
394 demands: [...base.demands, { signalGroupId: rad.id, volume: 300, heavyVehicleShare: 0 }],
395 };
396 const bericht = validateProject(projekt, undefined, STICHTAG);
397 const hinweis = bericht.findings.find(
398 (f) => f.rule === 'leistungsfaehigkeit.verkehrsstaerke-ohne-wirkung',
399 );
400 expect(hinweis).toBeDefined();
401 expect(hinweis?.severity).toBe('hinweis');
402 expect(hinweis?.message).toContain('"R1"');
403 expect(hinweis?.message).toContain('Umlaufzeit');
404 // Ohne solche Verkehrsstaerke schweigt der Bericht.
405 const ohne = validateProject(base, undefined, STICHTAG);
406 expect(
407 ohne.findings.some((f) => f.rule === 'leistungsfaehigkeit.verkehrsstaerke-ohne-wirkung'),
408 ).toBe(false);
409 });
410 });
411
412 describe('Verfahrensabhaengige Meldungen im HCM-Projekt', () => {
413 it('nennt bei einer Fuss/Rad-Verkehrsstaerke nach HCM kein HBS-Etikett', () => {
414 const base = vollstaendig();
415 const rad = createSignalGroup({ name: 'R1', mode: 'rad', index: base.signalGroups.length });
416 const projekt: Project = {
417 ...base,
418 signalGroups: [...base.signalGroups, rad],
419 phases: base.phases.map((p, i) =>
420 i === 0 ? { ...p, signalGroupIds: [...p.signalGroupIds, rad.id] } : p,
421 ),
422 demands: [...base.demands, { signalGroupId: rad.id, volume: 300, heavyVehicleShare: 0 }],
423 settings: { ...base.settings, serviceLevelScale: 'HCM' },
424 };
425 const bericht = validateProject(projekt, undefined, STICHTAG);
426 const hinweis = bericht.findings.find(
427 (f) => f.rule === 'leistungsfaehigkeit.verkehrsstaerke-ohne-wirkung',
428 );
429 expect(hinweis).toBeDefined();
430 expect(hinweis?.message).not.toContain('HBS');
431 expect(hinweis?.message).toContain('nach HCM erhalten sie keine Stufe');
432 expect(hinweis?.reference).toBe('HCM - Level of Service');
433 });
434
435 it('meldet eine Abweichung des Instationaritaetsfaktors nach HCM als wirkungslos, nach HBS mit HBS-Fundstelle', () => {
436 const base = vollstaendig();
437 const mitAbweichung = (verfahren: 'HBS' | 'HCM'): Project => ({
438 ...base,
439 settings: {
440 ...base.settings,
441 serviceLevelScale: verfahren,
442 rilsa: { instationaritaetsfaktor: 1.2 },
443 },
444 });
445 const hcm = validateProject(mitAbweichung('HCM'), undefined, STICHTAG).findings.find(
446 (f) => f.rule === 'vorgaben.abweichung.instationaritaetsfaktor',
447 );
448 expect(hcm?.message).toContain('geht in keine Zahl ein');
449 expect(hcm?.reference).toContain('HBS 2015');
450 const hbs = validateProject(mitAbweichung('HBS'), undefined, STICHTAG).findings.find(
451 (f) => f.rule === 'vorgaben.abweichung.instationaritaetsfaktor',
452 );
453 expect(hbs?.message).not.toContain('geht in keine Zahl ein');
454 expect(hbs?.reference).toContain('mittlere Wartezeit');
455 // Die Fundstelle einer Abweichung ist die Quelle des Kennwerts - nicht
456 // pauschal "RiLSA 2015 - Kennwerte".
457 const rast = validateProject(
458 { ...base, settings: { ...base.settings, rilsa: { cycleTimeStep: 2 } } },
459 undefined,
460 STICHTAG,
461 ).findings.find((f) => f.rule === 'vorgaben.abweichung.cycleTimeStep');
462 expect(rast?.reference).toContain('Planungspraxis');
463 });
464 });