import { describe, expect, it } from 'vitest'; import { validateProject } from '@/domain/validation/engine'; import { buildSignalPlan } from '@/domain/plan/signalPlan'; import { createArm, createConflict, createEmptyProject, createPhase, createSignalGroup, } from '@/domain/model/factory'; import type { Conflict, Project } from '@/domain/model/project'; /* * Strittiger Befund S2 (Fassung 5.11.0): Die Pruefung tastete je * Konfliktbeziehung die GANZE Konfliktliste erneut ab. * * engine.ts, checkConflicts: `if (!project.conflicts.some((c) => * c.fromId === conflict.toId && c.toId === conflict.fromId)) {` * * Die Zeile steht in der Schleife `for (const conflict of project.conflicts)` * und sucht die Gegenrichtung jedes Mal von vorn. Der Aufwand waechst damit * quadratisch in der Zahl der Beziehungen und, weil diese bei erfasster Matrix * selbst quadratisch mit der Zahl der Signalgruppen waechst, in vierter Potenz * der Gruppenzahl. `validateProject` laeuft ueber `ProjectStore.applyProject` * bei JEDER Aenderung synchron mit; weder das Einlesen noch die Oberflaeche * setzen eine Obergrenze fuer die Zahl der Signalgruppen. * * GEMESSEN am Altstand (dieselbe Anlage, nur groesser; Zugriffe auf die * Konfliktliste je Beziehung): K = 50 -> 29,5 · K = 200 -> 104,5 · * K = 800 -> 404,5 · K = 3200 -> 1604,5. Der Anteil ist exakt K/2 + 4,5, also * genau die halbe Liste je Beziehung. Laufzeit von `validateProject` allein: * 200 Signalgruppen, 20 000 Beziehungen: 2,2 s je Aenderung. * * DIE WACHE ZAEHLT STATT ZU MESSEN: Eine Zeitschranke haengt am Rechner, der * sie ausfuehrt. Gezaehlt werden deshalb die Elementzugriffe auf die * Konfliktliste - dieselbe Groesse, aber vom Rechner unabhaengig. Die Zahl je * Beziehung muss beschraenkt bleiben, statt mit der Liste zu wachsen. * * KEINE ZAHL AENDERT SICH: Die Pruefung liefert vorher wie nachher dieselben * Befunde; der Nachweis steht als eigener Fall darunter. * * GEGEN DEN ALTSTAND schlagen die beiden ersten Faelle fehl (404,5 bzw. * 1604,5 Zugriffe je Beziehung); der dritte besteht dort ebenso. */ const DATUM = new Date('2026-01-01T00:00:00Z'); /** * Knotenpunkt mit `n` Signalgruppen in zwei Phasen und vollstaendig erfasster * Konfliktmatrix ueber die Phasengrenze - in beiden Richtungen, also der * fehlerfreie Regelfall, in dem die Gegenrichtungsprobe die Liste bis zum Ende * durchlaeuft. */ function knotenpunkt(n: number): Project { const base = createEmptyProject('Grosse Anlage', DATUM, 'knotenpunkt'); const arm = createArm('Nord', 'Nord', 2, 50); const gruppen = Array.from({ length: n }, (_, i) => createSignalGroup({ name: `K${i + 1}`, mode: 'kfz', armId: arm.id, index: i }), ); const erste = gruppen.filter((_, i) => i % 2 === 0); const zweite = gruppen.filter((_, i) => i % 2 === 1); const p1 = createPhase( 'Phase 1', erste.map((g) => g.id), ); const p2 = createPhase( 'Phase 2', zweite.map((g) => g.id), ); const conflicts: Conflict[] = []; for (const a of erste) { for (const b of zweite) { conflicts.push({ ...createConflict(a.id, b.id), clearingDistance: 22, enteringDistance: 8 }); conflicts.push({ ...createConflict(b.id, a.id), clearingDistance: 22, enteringDistance: 8 }); } } return { ...base, intersection: { ...base.intersection, arms: [arm] }, signalGroups: gruppen, conflicts, phases: [p1, p2], program: { ...base.program, phaseOrder: [p1.id, p2.id] }, }; } /** Elementzugriffe auf `project.conflicts` waehrend einer Pruefung, je Beziehung. */ function zugriffeJeBeziehung(projekt: Project): number { const plan = buildSignalPlan(projekt); let zugriffe = 0; const beobachtet = new Proxy(projekt.conflicts as Conflict[], { get(ziel, eigenschaft, empfaenger) { if (typeof eigenschaft === 'string' && /^\d+$/.test(eigenschaft)) zugriffe += 1; return Reflect.get(ziel, eigenschaft, empfaenger) as unknown; }, }); validateProject({ ...projekt, conflicts: beobachtet }, plan, DATUM); return zugriffe / projekt.conflicts.length; } describe('Aufwand der Pruefung bei vielen Signalgruppen', () => { it('liest die Konfliktliste nicht je Beziehung einmal ganz durch', () => { const projekt = knotenpunkt(40); // Voraussetzung des Aufbaus: eine Liste, die gross genug ist, um den // Unterschied zu zeigen. expect(projekt.conflicts).toHaveLength(800); expect(zugriffeJeBeziehung(projekt)).toBeLessThan(20); }); it('haelt den Aufwand je Beziehung, wenn die Anlage waechst', () => { const klein = zugriffeJeBeziehung(knotenpunkt(40)); const gross = zugriffeJeBeziehung(knotenpunkt(80)); // Viermal so viele Beziehungen duerfen den Aufwand JE BEZIEHUNG nicht // vervierfachen; er darf ueberhaupt nicht mit der Listenlaenge wachsen. expect(gross).toBeLessThanOrEqual(klein + 1); }); it('meldet dabei unveraendert dieselben Befunde', () => { const projekt = knotenpunkt(12); const bericht = validateProject(projekt, buildSignalPlan(projekt), DATUM); // Erfasste Gegenrichtungen: keine Meldung. Sobald eine fehlt, meldet die // Regel sie - genau einmal, fuer die verbliebene Richtung. expect(bericht.findings.some((f) => f.rule === 'konflikte.gegenrichtung-fehlt')).toBe(false); const halb: Project = { ...projekt, conflicts: projekt.conflicts.slice(0, -1) }; const fehlend = validateProject(halb, buildSignalPlan(halb), DATUM); expect(fehlend.findings.filter((f) => f.rule === 'konflikte.gegenrichtung-fehlt')).toHaveLength( 1, ); }); });