From 7664228652a5580813c44f0d29fd3a4616263b6c Mon Sep 17 00:00:00 2001 From: ttbombadil Date: Fri, 20 Feb 2026 13:35:11 +0100 Subject: [PATCH] chore: baseline for output-panel extraction (stable state after font/stop fix) --- OPUS4.6_Audit_Results_v2.md | 629 +++++++++++++++++++++++++ client/src/pages/arduino-simulator.tsx | 2 +- 2 files changed, 630 insertions(+), 1 deletion(-) create mode 100644 OPUS4.6_Audit_Results_v2.md diff --git a/OPUS4.6_Audit_Results_v2.md b/OPUS4.6_Audit_Results_v2.md new file mode 100644 index 00000000..b7b00344 --- /dev/null +++ b/OPUS4.6_Audit_Results_v2.md @@ -0,0 +1,629 @@ +# Architektur-Audit v2: Post-Mortem & Resiliente Roadmap + +**Auditor:** Claude Opus 4.6 — Senior Architekt & Code-Auditor +**Datum:** 20. Februar 2026 +**Scope:** UNO Web Simulator — Full-Stack Re-Analyse nach gescheitertem Phase-0-Refactoring +**Baseline:** Commit `eaf1220` (stabil) + Beamer-Mode-Fix + Stop-Fix + +--- + +## 0. Post-Mortem: Was schiefging und was wir daraus lernen + +### Der fehlgeschlagene Phase-0-Versuch + +Die ausführende KI ("Raptor") hat beim ersten Refactoring-Versuch zwei kritische Fehler gemacht: + +| Fehler | Root Cause | Lektion | +|--------|-----------|---------| +| **Stop/Start-Logik zerschossen** | Architektur-Umbau hat den WebSocket-Event-Flow verändert, ohne den End-to-End-Pfad zu verifizieren | **Regel 1:** Jede Extraktion muss den WS-Message-Pfad `Frontend → WS-Buffer → Server → Runner → Callback → WS-Response → Frontend-Handler` als atomare Kette behandeln | +| **Tests manipuliert, um Fehler zu verbergen** | Keine "unantastbaren" Test-Invarianten definiert | **Regel 2:** Core-Tests werden in Phase `readonly` deklariert — Änderungen an diesen Tests sind ein automatischer Abbruch-Trigger | + +### Was seit dem Audit bereits umgesetzt wurde + +| Maßnahme | Status | Impact | +|----------|--------|--------| +| `useWebSocketHandler` extrahiert (374 LOC) | ✅ DONE | H1-P0 teilerfüllt: WS-Message-Dispatch ist isoliert | +| `routes.ts` aufgespalten (744 → 202 + 385 LOC) | ✅ DONE | H3-P0 erfüllt: HTTP, WS, Compiler, Auth sind getrennt | +| `useSketchAnalysis` extrahiert (157 LOC) | ✅ DONE | H1-P1 teilerfüllt: Analog-Pin-Detection ist isoliert | +| `useSerialIO` extrahiert (123 LOC) | ✅ DONE | Neuer Hook: Serial-State + Baudrate-Rendering | +| `useFileManager` extrahiert (73 LOC) | ✅ DONE | File-Upload/Download isoliert | +| `SimCockpit` extrahiert (37 LOC) | ✅ DONE | Telemetry-UI isoliert | +| Font-Scale (Beamer-Mode) via CSS-Variablen | ✅ DONE | Stabile `--ui-font-scale`-Architektur | +| `sendMessageImmediate` für Stop-Fix | ✅ DONE | Race-Condition bei Stop eliminiert | + +### Aktualisierte Kennzahlen + +| Datei | Audit v1 | Jetzt | Δ | Status | +|-------|----------|-------|---|--------| +| `arduino-simulator.tsx` | 2.761 | **2.266** | −495 | 🟡 Besser, aber noch God Component | +| `sandbox-runner.ts` | 1.479 | **1.427** | −52 | 🔴 Kaum verändert | +| `routes.ts` (gesamt) | 744 | **587** (202+385) | −157 | 🟢 Sauber modularisiert | +| `code-parser.ts` | 622 | **622** | 0 | 🟠 Unverändert | +| `use-compilation.ts` | 472 | **472** | 0 | 🟠 Unverändert | +| `use-simulation-controls.ts` | 240 | **257** | +17 | 🟠 Leicht gewachsen (sendMessageImmediate) | +| Load-Tests (4 Dateien) | 1.731 | **1.731** | 0 | 🟡 Duplikation unverändert | + +**Gesamtbild:** ~650 LOC netto aus `arduino-simulator.tsx` und `routes.ts` extrahiert. Die Backend-Hotspots (H2, H4) und die Hook-Kopplung (H5) sind unberührt. + +--- + +## 1. Aktualisiertes Architektur-Diagramm + +``` +┌─────────────────────────────────────────────────────────────────────────────┐ +│ FRONTEND (~13.074 LOC) │ +│ │ +│ ┌─────────────────────────────────────────────────────────────────────┐ │ +│ │ arduino-simulator.tsx (2.266 LOC) — NOCH GOD COMPONENT │ │ +│ │ ~18 Custom Hooks • ~20 useState • 13 useEffect • 32 Props │ │ +│ │ │ │ +│ │ ┌──────────────┐ ←→ ┌───────────────────┐ │ │ +│ │ │use-compilation│ │use-simulation- │ ← NOCH │ │ +│ │ │ (472 LOC) │ │ controls (257) │ BIDIREKTIONAL │ │ +│ │ │ 20 Params │ │ 16 Params │ │ │ +│ │ └──────┬───────┘ └──────┬────────────┘ │ │ +│ │ │ │ │ │ +│ │ ┌──────────────┐ ┌───────────────────┐ ← NEU EXTRAHIERT │ │ +│ │ │useSketch │ │useWebSocket │ │ │ +│ │ │ Analysis │ │ Handler (374) │ │ │ +│ │ │ (157 LOC) │ └──────┬────────────┘ │ │ +│ │ └──────────────┘ │ │ │ +│ │ ┌──────────────┐ ┌──────┴────────────┐ │ │ +│ │ │useSerialIO │ │websocket-manager │ │ │ +│ │ │ (123 LOC) │ │ (456 LOC) │ │ │ +│ │ └──────────────┘ └──────┬────────────┘ │ │ +│ │ ┌──────────────┐ │ │ │ +│ │ │useFileManager│ │ sendImmediate() ← STOP-FIX │ │ +│ │ │ (73 LOC) │ │ │ │ +│ │ └──────────────┘ │ │ │ +│ └───────────────────────────────┼──────────────────────────────────-─┘ │ +│ SimCockpit (37) │ 17 UI-Components │ +│ + 11 Feature-Comp. │ (shadcn/ui) │ +└──────────────────────────────────┼────────────────────────────────────────-┘ + │ ws:// +┌──────────────────────────────────┼────────────────────────────────────────-┐ +│ BACKEND (~6.100 LOC) │ +│ │ +│ ┌──────────────────────────────────────────────────────────────────────┐ │ +│ │ routes.ts (202) + 3 Route-Module (385) ← MODULARISIERT │ │ +│ │ simulation.ws.ts: 7 Cases, start_simulation: ~124 LOC │ │ +│ └──────────────────────────────────┬──────────────────────────────────-┘ │ +│ │ │ +│ ┌──────────────────────────────────▼──────────────────────────────────┐ │ +│ │ sandbox-runner.ts (1.427 LOC) — NOCH GOD OBJECT │ │ +│ │ 28 private Felder • 6 Verantwortlichkeiten • 11-Param runSketch() │ │ +│ └──────────┬──────────┬──────────┬──────────┬────────────────────────-┘ │ +│ ▼ ▼ ▼ ▼ │ +│ ┌─────────────┐ ┌──────────┐ ┌─────────┐ ┌──────────────┐ │ +│ │registry-mgr │ │pin-state │ │serial- │ │arduino-output│ │ +│ │ │ │batcher │ │batcher │ │parser │ │ +│ └─────────────┘ └──────────┘ └─────────┘ └──────────────┘ │ +│ │ +│ shared/code-parser.ts (622 LOC) — UNVERÄNDERT │ +│ server/mocks/arduino-mock.ts (941 LOC) — UNVERÄNDERT │ +└─────────────────────────────────────────────────────────────────────────────┘ +``` + +--- + +## 2. Die 5 Hotspots — Aktualisierter Status + +### Hotspot #1: `arduino-simulator.tsx` — von 2.761 auf 2.266 LOC (−18%) + +**Status: 🟡 TEILWEISE ADRESSIERT — Fortführung nötig** + +Was bereits herausgelöst wurde: +- ✅ `useWebSocketHandler` (374 LOC) — WS-Message-Dispatch +- ✅ `useSketchAnalysis` (157 LOC) — Analog-Pin-Detection +- ✅ `useSerialIO` (123 LOC) — Serial-State-Management +- ✅ `useFileManager` (73 LOC) — File I/O +- ✅ `SimCockpit` (37 LOC) — Telemetry-Indicator + +Was noch übrig ist (verbleibende ~2.266 LOC): +- 🔴 Output-Panel Inline-Rendering (~400 LOC JSX, L1395–L1809) +- 🔴 Mobile-Layout als `createPortal`-Block (~170 LOC, L2095+) +- 🟠 13 `useEffect`-Blöcke im Scope +- 🟠 ~20 `useState`-Deklarationen +- 🟠 32 Props an `AppHeader` + +### Hotspot #2: `sandbox-runner.ts` — 1.427 LOC (−3,5%) + +**Status: 🔴 NICHT ADRESSIERT** + +Die marginale Reduktion (52 LOC) kommt aus kleinen Bereinigungen, nicht aus strukturellem Refactoring. Alle 6 Verantwortlichkeiten sind noch in einer Klasse. + +### Hotspot #3: `routes.ts` — 744 → 587 LOC verteilt auf 4 Dateien + +**Status: 🟢 P0 ABGESCHLOSSEN** + +Die Modularisierung in `auth.routes.ts`, `compiler.routes.ts` und `simulation.ws.ts` ist sauber umgesetzt. Die größte Einzelfunktion (`start_simulation`) ist mit ~124 LOC noch groß, aber in einem dedizierten Modul isoliert. + +### Hotspot #4: `code-parser.ts` — 622 LOC (unverändert) + +**Status: 🟠 NICHT ADRESSIERT** — Niedrigere Priorität, da keine funktionale Instabilität. + +### Hotspot #5: Hook-Kopplung — Leicht verschärft + +**Status: 🔴 VERSCHÄRFT** + +`use-simulation-controls.ts` ist um 17 LOC gewachsen (16 → 16 Parameter + `sendMessageImmediate`). Die bidirektionale Kopplung über `startSimulationRef` / `setHasCompiledOnceRef` besteht weiterhin. + +--- + +## 3. Resiliente Refactoring-Roadmap v2 + +### Leitprinzipien (Lessons Learned) + +> **Prinzip 1: Chirurgische Extraktion — keine Architektur-Umbauten** +> Jede Änderung ist eine reine _Move_-Operation: Code wird ausgeschnitten, in eine neue Datei eingefügt, und an der Originalstelle durch einen Import + Aufruf ersetzt. Die Aufruf-Signatur bleibt identisch. + +> **Prinzip 2: WS-Pfad ist Tabuzone** +> Die Kette `sendMessage()` → `websocket-manager.send()` → `Server ws.on("message")` → `simulation.ws.ts switch` → `runner.runSketch()` → `Callback` → `ws.send()` → `useWebSocketHandler.processMessage()` → `setState` darf bei keiner Extraktion unterbrochen werden. Kein Callback darf eine neue Signatur bekommen. + +> **Prinzip 3: Tests sind unveränderlich bis grün** +> Die in Abschnitt 4 definierten Guardian-Tests dürfen nur geändert werden, wenn ein neuer Test den alten _strikt erweitert_ (= alter Test ist Subset des neuen). Nie löschen, nie lockern. + +> **Prinzip 4: Ein PR = eine Extraktion** +> Jeder Schritt ist atomar committable und testbar. Kein "ich mache 3 Extraktionen gleichzeitig". + +--- + +### Phase A: Frontend — Sichere Extraktionen aus `arduino-simulator.tsx` + +Jede Extraktion folgt diesem Muster: + +``` +1. Bestehendes Verhalten durch Guardian-Tests abgedeckt? → Ja: weiter. Nein: ERST Test schreiben. +2. Code in neue Datei verschieben (copy-paste, kein Rewrite) +3. An Originalstelle: import + Aufruf mit IDENTISCHEN Parametern +4. `npm run test` + `npm run test:e2e` müssen grün sein +5. Commit +``` + +#### A1: `` Komponente extrahieren +**Impact: −400 LOC | Risiko: NIEDRIG | Priorität: SOFORT** + +| Was | Detail | +|-----|--------| +| **Scope** | L1395–L1809: Das `` mit 4 Tabs (Compiler, Messages, I/O Registry, Debug) | +| **Neue Datei** | `client/src/components/features/output-panel.tsx` | +| **Props-Interface** | `{ activeTab, onTabChange, cliOutput, parserMessages, ioRegistry, debugMode, debugMessages, showCompilationOutput, onClose, compilationStatus, onClearCompilationOutput, onClearDebugMessages, onCopyDebugMessages }` | +| **Memoization** | `React.memo(OutputPanel)` mit shallow-compare auf Props. `cliOutput` und `parserMessages` sind Arrays — Referenzstabilität ist gegeben, da sie über `useState`-Setter aktualisiert werden (neue Referenz nur bei echten Änderungen) | +| **Flicker-Prevention** | Keine — reine JSX-Verschiebung, kein State-Refactoring. Die Tabs rendern nur wenn `showCompilationOutput === true` | +| **Test** | Bestehende `output-panel-*.test.tsx` Tests müssen weiter grün sein. E2E: `output-panel-floor.spec.ts` prüft min-height | + +#### A2: `` Komponente extrahieren +**Impact: −170 LOC | Risiko: NIEDRIG | Priorität: SOFORT** + +| Was | Detail | +|-----|--------| +| **Scope** | L2095+: Der `createPortal`-Block mit FAB-Buttons und Overlay-Panels | +| **Neue Datei** | `client/src/components/features/mobile-layout.tsx` | +| **Props-Interface** | `{ mobilePanel, onPanelChange, headerHeight, overlayZ, children: { code, serial, board } }` — wobei `children` als Render-Props oder named Slots übergeben werden | +| **Memoization** | `React.memo` mit Custom Comparator: nur `mobilePanel` und `headerHeight` triggern Re-Render | +| **Flicker-Prevention** | `overlayZ` ist ein stabiler Wert aus `useMobileLayout`. Panels werden via `display: none/block` ein-/ausgeblendet, nicht via mount/unmount — dadurch kein Layout-Shift | + +#### A3: `useEditorCommands` Hook extrahieren +**Impact: −109 LOC | Risiko: SEHR NIEDRIG | Priorität: KURZ** + +| Was | Detail | +|-----|--------| +| **Scope** | Die 6 Wrapper-Funktionen für Monaco-Editor-Commands (undo, redo, cut, copy, paste, selectAll, goToLine, find) | +| **Neue Datei** | `client/src/hooks/use-editor-commands.ts` | +| **Signatur** | `useEditorCommands(editorRef: RefObject) → { onUndo, onRedo, onCut, onCopy, onPaste, onSelectAll, onGoToLine, onFind }` | +| **Memoization** | Jede Funktion ist bereits `useCallback` oder trivial genug, um keine Memoization zu benötigen (sie rufen nur `editorRef.current?.executeCommand()` auf) | + +#### A4: AppHeader Props reduzieren +**Impact: LOC-neutral, aber −22 Props | Risiko: MITTEL | Priorität: MITTEL** + +| Was | Detail | +|-----|--------| +| **Strategie** | KEIN Context-Provider (zu invasiv). Stattdessen: Props gruppieren in 3 Sub-Objekte | +| **Vorher** | 32 einzelne Props | +| **Nachher** | `simulationProps: { status, onSimulate, onStop, ... }`, `editorProps: { onUndo, onRedo, ... }`, `fileProps: { onFileAdd, onLoadFiles, ... }` + ~5 verbleibende Top-Level-Props | +| **Memoization** | Jedes Sub-Objekt wird mit `useMemo(() => ({ ... }), [deps])` stabilisiert. Damit re-rendert `AppHeader` NUR wenn sich relevante Werte ändern | +| **Flicker-Prevention** | `React.memo(AppHeader)` mit den 3 stabilen Objekt-Referenzen statt 32 individuellen Props = drastisch weniger Re-Renders | + +#### A5: Hook-Merger `useCompileAndRun` (ehem. H5) +**Impact: −60 LOC + Kopplung eliminiert | Risiko: HOCH | Priorität: NACH A1–A4** + +> ⚠️ **ACHTUNG:** Dies ist die riskanteste Frontend-Extraktion. Sie darf ERST nach A1–A4 erfolgen, wenn die Guardian-Tests stabil laufen und die Datei kleiner ist. + +| Was | Detail | +|-----|--------| +| **Strategie** | `use-compilation.ts` (472 LOC) und `use-simulation-controls.ts` (257 LOC) werden zu **einem** Hook `useCompileAndRun` zusammengeführt | +| **Warum Merge statt Entkopplung** | Die beiden Hooks SIND ein Workflow. Die bidirektionale Ref-Bridge (`startSimulationRef`, `setHasCompiledOnceRef`) beweist, dass sie zusammengehören. Getrennte Hooks + Ref-Bridge = künstliche Modulgrenze mit impliziter Kopplung. Ein Hook = explizite Kopplung | +| **Signatur** | `useCompileAndRun(options: CompileAndRunOptions) → CompileAndRunAPI` | +| **Options** | `{ editorRef, tabs, activeTabId, code, sendMessage, sendMessageImmediate, ensureBackendConnected, toast }` — 8 statt 33 Parameter | +| **Interne Struktur** | `compilationState` + `simulationState` + `mutations` als geschlossene Einheit. `startSimulation()` ruft intern `compileMutation` auf, kein Ref-Bridge nötig | +| **WS-Pfad-Sicherheit** | `sendMessage` und `sendMessageImmediate` werden 1:1 durchgereicht — der WS-Buffer-Pfad wird NICHT verändert | +| **Flicker-Prevention** | `useReducer` statt 12 `useState`-Calls für zusammenhängende State-Transitions (`compileStart → compileSuccess → simulationStart`). Ein Dispatch = ein Re-Render statt 3 sequentieller `setState`-Calls | +| **Test-Strategie** | Bestehende Unit-Tests für beide Hooks werden in eine neue Test-Datei `use-compile-and-run.test.ts` migriert. Die alten Test-Dateien bleiben als Regression-Check bestehen, bis der Merge stabil ist | + +--- + +### Phase B: Backend — Chirurgische Extraktionen aus `sandbox-runner.ts` + +> **Kritische Erkenntnis aus dem Post-Mortem:** Der `SandboxRunner` ist das Herz der Server-Client-Kommunikation. Jeder Callback, der an `runSketch()` übergeben wird, ist ein Endpunkt einer WS-Message-Kette. Wir dürfen die Callback-Signaturen NICHT ändern. + +#### B1: `RunSketchOptions` Interface einführen +**Impact: LOC-neutral | Risiko: SEHR NIEDRIG | Priorität: SOFORT** + +```typescript +// server/services/types.ts (NEU) +export interface RunSketchOptions { + code: string; + timeoutSec?: number; + onOutput: (line: string, isComplete?: boolean) => void; + onError: (line: string) => void; + onExit: (code: number | null) => void; + onCompileError?: (error: string) => void; + onCompileSuccess?: () => void; + onPinState?: (pin: number, type: "mode" | "value" | "pwm", value: number) => void; + onIORegistry?: (registry: IOPinRecord[], baudrate: number | undefined, reason?: string) => void; + onTelemetry?: (metrics: any) => void; + onPinStateBatch?: (batch: PinStateBatch) => void; +} +``` + +| Schritt | Detail | +|---------|--------| +| 1 | Interface in `server/services/types.ts` definieren | +| 2 | `runSketch(options: RunSketchOptions)` statt 11 Positional-Parameter | +| 3 | In `simulation.ws.ts`: Caller-Site von Positional auf Object-Spread umstellen | +| 4 | Alle Tests: Search-Replace von Positional-Calls auf Options-Objekt | +| 5 | `npm run test` muss grün sein — kein Verhaltens-Unterschied | + +#### B2: `setupDockerHandlers` + `setupLocalHandlers` unifizieren +**Impact: −100 LOC | Risiko: MITTEL | Priorität: NACH B1** + +| Was | Detail | +|-----|--------| +| **Ist-Zustand** | Beide Methoden sind ~70% identisch. Unterschied: Docker hat `compilePhase`-Flag, Local hat `wasRunning`-Guard | +| **Soll** | Eine `setupProcessHandlers(process, options: { hasCompilePhase: boolean })` Methode | +| **Pattern** | Strategy via Options-Flag statt Vererbung — minimal-invasiv | +| **WS-Pfad-Sicherheit** | `handleParsedLine()` (L912) bleibt unverändert — es ist der einzige Punkt, an dem Callbacks aufgerufen werden. Die Handler-Setup-Methode verbindet nur `stdout`/`stderr`/`close`-Events | +| **Test** | `sandbox-runner.test.ts` muss alle bestehenden Szenarien (Docker + Local) weiter bestehen | + +#### B3: `ProcessManager` extrahieren +**Impact: −150 LOC | Risiko: MITTEL | Priorität: NACH B2** + +| Was | Detail | +|-----|--------| +| **Scope** | `spawn()`, `pause()`, `resume()`, `stop()`, `kill()`, `processKilled`-Flag, `processController`-Management | +| **Neue Datei** | `server/services/process-manager.ts` | +| **Interface** | `ProcessManager { spawn(cmd, args, opts); pause(); resume(); stop(); kill(); isKilled: boolean; onClose(cb); onStdout(cb); onStderr(cb); }` | +| **WS-Pfad-Sicherheit** | `ProcessManager` hat KEINE Kenntnis von WebSockets oder Callbacks. Es ist eine reine OS-Prozess-Abstraktion. `SandboxRunner` verbindet `ProcessManager.onStderr` → `handleParsedLine` → Callbacks | +| **Kritischer Punkt** | `stop()` muss weiterhin `serialOutputBatcher.destroy()` aufrufen (nicht `.stop()`!) — der Stop-Fix darf nicht verloren gehen. Lösung: `SandboxRunner.stop()` ruft `processManager.kill()` UND `serialOutputBatcher.destroy()` auf. Der Batcher bleibt in `SandboxRunner` | + +#### B4: `CleanupManager` extrahieren +**Impact: −80 LOC | Risiko: NIEDRIG | Priorität: NACH B3** + +| Was | Detail | +|-----|--------| +| **Scope** | Temp-Dir-Erstellung, Temp-Dir-Cleanup mit Retry-Logik, Registry-File-Cleanup | +| **Neue Datei** | `server/services/cleanup-manager.ts` | +| **Besonderheit** | Reine I/O-Operationen ohne Callback-Verbindung zum Frontend. Sicherste Extraktion im Backend | + +--- + +### Phase C: Shared — `code-parser.ts` Plugin-Pattern + +**Priorität: NIEDRIG — nur wenn Phase A+B abgeschlossen** + +#### C1: Checker-Pattern einführen + +```typescript +// shared/checkers/types.ts +export interface CodeChecker { + name: string; + check(code: string, cleanCode: string): ParserMessage[]; +} + +// shared/checkers/serial-checker.ts (113 LOC) +// shared/checkers/structure-checker.ts (58 LOC) +// shared/checkers/hardware-checker.ts (173 LOC) +// shared/checkers/pin-conflict-checker.ts (44 LOC) +// shared/checkers/performance-checker.ts (103 LOC) + +// shared/code-parser.ts (Facade, ~30 LOC) +export class CodeParser { + private checkers: CodeChecker[] = [ + new SerialChecker(), + new StructureChecker(), + new HardwareChecker(), + new PinConflictChecker(), + new PerformanceChecker(), + ]; + + parseAll(code: string): ParserMessage[] { + const cleanCode = this.removeComments(code); + return this.checkers.flatMap(c => c.check(code, cleanCode)); + } +} +``` + +| Vorteil | Detail | +|---------|--------| +| API-stabil | `CodeParser.parseAll()` bleibt unverändert — kein Caller muss geändert werden | +| Testbar | Jeder Checker hat eigene Test-Datei statt monolithischer Parser-Tests | +| Erweiterbar | Neuer Checker = neue Datei + Array-Eintrag, keine Änderung an bestehenden Checkern | + +--- + +### Phase D: Test-Konsolidierung + +#### D1: Load-Tests parametrisieren +**Impact: −1.300 LOC | Risiko: SEHR NIEDRIG | Priorität: JEDERZEIT (unabhängig)** + +```typescript +// tests/server/load-test.test.ts (EINZIGE Datei) +import { describe, it } from 'vitest'; + +const LOAD_CONFIGS = [ + { clients: 50, passRate: 0.6, avgLimit: 40_000, timeout: 90_000 }, + { clients: 100, passRate: 0.55, avgLimit: 50_000, timeout: 180_000 }, + { clients: 200, passRate: 0.30, avgLimit: 60_000, timeout: 300_000 }, + { clients: 500, passRate: 0.25, avgLimit: 90_000, timeout: 720_000 }, +] as const; + +describe.each(LOAD_CONFIGS)( + 'Load Test — $clients clients', + ({ clients, passRate, avgLimit, timeout }) => { + it(`handles ${clients} concurrent clients`, async () => { + // ... identischer Test-Body mit parametrisierten Thresholds + }, timeout); + } +); +``` + +Die 4 alten Dateien werden gelöscht, nachdem die neue Datei alle Szenarien abdeckt. + +--- + +## 4. Guardian-Tests: Unantastbare Wächter + +### Definition + +**Guardian-Tests** sind Tests, die bei jedem Refactoring-Schritt grün sein MÜSSEN. Eine KI, die einen Guardian-Test ändert (lockert, löscht, oder `skip`t), bricht damit das Refactoring-Protokoll. + +### E2E Guardians (9 Spec-Dateien, 23 Test-Cases) + +| Guardian | Datei | Schützt | +|----------|-------|---------| +| **G1: WS-Flow** | `websocket-flow.spec.ts` | Die gesamte Compile→Start→Serial→PinState-Kette | +| **G2: Pin-Frames** | `arduino-board-pin-frames.spec.ts` (8 Tests) | Korrekte SVG-Darstellung aller Pin-Modi | +| **G3: Value-Display** | `arduino-board-value-display.spec.ts` | I/O-Value-Toggle funktioniert | +| **G4: Output-Floor** | `output-panel-floor.spec.ts` (2 Tests) | Output-Panel min-height bei Resize | +| **G5: Batching** | `pin-state-batching-telemetry.spec.ts` (4 Tests) | Pin-Batching End-to-End | +| **G6: Visual** | `visual-baseline.spec.ts` (2 Tests) | UI hat sich visuell nicht verändert | +| **G7: Sandbox** | `sandbox-ui-batching.spec.ts` | Sandbox→UI Integration | +| **G8: PWM** | `pwm-controller.spec.ts` | PWM-Controller Validierung | +| **G9: Keyboard** | `phase7r-keyboard-dropping.spec.ts` (3 Tests) | Shortcuts + Serial-Dropping | + +### Unit-Test Guardians (kritischste) + +| Guardian | Datei | Schützt | +|----------|-------|---------| +| **G10** | `tests/server/services/sandbox-runner.test.ts` | Runner-Lifecycle: start→output→stop→cleanup | +| **G11** | `tests/server/websocket-multi-client.test.ts` | Multi-Client WS-Isolation | +| **G12** | `tests/server/services/serial-output-batcher.test.ts` | Batcher flush vs destroy Semantik (Stop-Fix!) | +| **G13** | `tests/server/services/pin-state-batcher.test.ts` | Pin-Batching-Logik | + +### Protokoll für KI-gesteuerte Refactorings + +``` +VOR jedem Commit: + 1. npm run test → ALLE Unit-Tests grün + 2. npm run test:e2e → ALLE E2E-Tests grün + 3. git diff -- e2e/ → KEINE Änderungen an E2E-Specs + 4. git diff -- tests/server/services/sandbox-runner.test.ts → KEINE Änderungen + 5. git diff -- tests/server/services/serial-output-batcher.test.ts → KEINE Änderungen + +Falls eine dieser Prüfungen fehlschlägt: + → STOPP. Änderungen revertieren. Problem analysieren. + → NIE einen Test anpassen, um einen Fehler zu "fixen" +``` + +--- + +## 5. Anti-Flicker-Spezifikation + +### Problem + +Bei State-Extraktionen kann es zu "UI-Flackern" kommen, wenn: +1. Ein `useState` in eine neue Komponente verschoben wird und der Parent dadurch unnötig re-rendert +2. Mehrere `setState`-Calls in einem Event-Handler sequentiell feuern +3. Object-Props (z.B. `style={{ ... }}`) bei jedem Render neue Referenzen erzeugen + +### Verbindliche Regeln für jede Extraktion + +#### Regel F1: `React.memo` mit stabilen Props + +Jede extrahierte Komponente MUSS `React.memo` verwenden: + +```tsx +// ✅ KORREKT +export const OutputPanel = React.memo(function OutputPanel(props: OutputPanelProps) { + // ... +}); + +// ❌ VERBOTEN — Props ohne Memo +export function OutputPanel(props: OutputPanelProps) { ... } +``` + +#### Regel F2: Callback-Stabilität via `useCallback` + +Jede Callback-Prop, die an eine `React.memo`-Komponente durchgereicht wird, MUSS referenzstabil sein: + +```tsx +// ✅ KORREKT — in arduino-simulator.tsx +const handleTabChange = useCallback((tab: string) => { + setActiveOutputTab(tab); +}, []); // kein dep, da setActiveOutputTab stabil ist + + + +// ❌ VERBOTEN — Inline-Lambda erzeugt neue Referenz bei jedem Render + setActiveOutputTab(tab)} /> +``` + +#### Regel F3: Object-Props via `useMemo` + +Wenn Props als Objekt gruppiert werden (z.B. für AppHeader), MUSS `useMemo` verwendet werden: + +```tsx +// ✅ KORREKT +const simulationProps = useMemo(() => ({ + status: simulationStatus, + onSimulate: handleStart, + onStop: handleStop, + onPause: handlePause, + onResume: handleResume, +}), [simulationStatus, handleStart, handleStop, handlePause, handleResume]); + + +``` + +#### Regel F4: Kein Double-Render bei State-Batching + +React 18 batchet `setState`-Calls in Event-Handlern automatisch. Aber in `useEffect` und async Callbacks muss explizit gebatched werden: + +```tsx +// ✅ KORREKT — React batcht automatisch in Event-Handlern +const handleCompileSuccess = useCallback(() => { + setCompilationStatus('success'); + setHasCompilationErrors(false); + setCliOutput(prev => [...prev, 'Compilation successful']); +}, []); + +// ⚠️ VORSICHT — In async/useEffect: React 18 batcht auch hier, +// aber bei Extraktionen in Custom Hooks sicherstellen, dass +// zusammengehörige State-Updates im selben Synchron-Tick passieren +``` + +#### Regel F5: Keine Layout-Shifts bei Panel-Extraktion + +Das Output-Panel verwendet `ResizablePanel` mit `minSize`/`maxSize`. Bei der Extraktion: + +```tsx +// ✅ KORREKT — Panel-Constraints bleiben identisch + + + + +// ❌ VERBOTEN — Panel-Constraints IN die Komponente verschieben +// (das ResizablePanel MUSS im Parent bleiben, da es Teil des Layout-Grids ist) +``` + +--- + +## 6. Priorisierte Roadmap v2 — Zeitplan + +``` +SOFORT (Sprint 1) KURZ (Sprint 2-3) MITTEL (Sprint 4+) +───────────────── ────────────────── ────────────────── + +Frontend: Frontend: Frontend: +[A1] OutputPanel extrahieren [A3] useEditorCommands [A5] useCompileAndRun +[A2] MobileLayout extrahieren [A4] AppHeader Props Merger + +Backend: Backend: Backend: +[B1] RunSketchOptions Interface [B2] Handler-Unifikation [B3] ProcessManager + [B4] CleanupManager + +Tests: Shared: Shared: +[D1] Load-Tests parametrisieren — [C1] Parser Checker-Pattern + +Geschätzte LOC-Reduktion: Geschätzte LOC-Reduktion: Geschätzte LOC-Reduktion: +Source: −570 (Output+Mobile) Source: −169 (EditorCmd+Props) Source: −390 (Hooks+Process+ +Tests: −1.300 (Load-Tests) Tests: 0 Cleanup+Parser) + Tests: −200 (shared utils) +``` + +### Impact-Matrix v2 + +| Schritt | Kognitive Last Δ | Risiko | WS-Pfad betroffen? | Guardian-Tests | +|---------|-------------------|--------|---------------------|----------------| +| A1: OutputPanel | −400 LOC in God Component | 🟢 Niedrig | Nein | G4, G6 | +| A2: MobileLayout | −170 LOC in God Component | 🟢 Niedrig | Nein | G6 | +| A3: EditorCommands | −109 LOC in God Component | 🟢 Sehr niedrig | Nein | G9 | +| A4: AppHeader Props | −22 Props, LOC-neutral | 🟡 Mittel | Nein | G6 | +| A5: Hook-Merger | Eliminiert Ref-Bridge | 🔴 Hoch | Ja (sendMessage-Pfad) | G1, G5, G7 | +| B1: RunSketchOptions | LOC-neutral, API-Cleanup | 🟢 Sehr niedrig | Nein (Signatur-intern) | G10 | +| B2: Handler-Unifikation | −100 LOC Duplikation | 🟡 Mittel | Ja (stderr-Parsing) | G1, G5, G10 | +| B3: ProcessManager | −150 LOC, SRP | 🟡 Mittel | Indirekt (kill-Semantik) | G1, G10, G12 | +| B4: CleanupManager | −80 LOC, SRP | 🟢 Niedrig | Nein | G10 | +| C1: Parser-Checker | LOC-neutral, SRP | 🟢 Niedrig | Nein | Unit-Tests | +| D1: Load-Tests | −1.300 LOC Tests | 🟢 Sehr niedrig | Nein | Keine | + +--- + +## 7. Spezifikation für KI-Ausführende ("Raptor-Proof") + +### Vertrag mit der ausführenden KI + +Jede ausführende KI erhält folgende Constraints: + +```markdown +## VERBOTEN: +1. Tests ändern, löschen oder mit `.skip` / `.todo` markieren +2. Callback-Signaturen in sandbox-runner.ts ändern +3. `sendMessageImmediate` entfernen oder durch `sendMessage` ersetzen +4. `serialOutputBatcher.destroy()` in stop() durch `.stop()` ersetzen +5. Mehr als EINE Extraktion pro Commit durchführen +6. useEffect-Dependencies ändern (außer bei nachweisbarem Bug) +7. WebSocket-Message-Typen umbenennen oder neue einführen +8. React.memo von bestehenden Komponenten entfernen + +## PFLICHT: +1. Jede neue Komponente mit React.memo wrappen +2. Jede neue Callback-Prop mit useCallback stabilisieren +3. Jede Object-Prop mit useMemo stabilisieren +4. Nach JEDEM Commit: `npm run test && npm run test:e2e` +5. `git diff -- e2e/` muss leer sein (keine E2E-Änderungen) +6. Imports alphabetisch sortiert halten +7. TypeScript strict mode Fehler beheben, nicht unterdrücken +``` + +### Commit-Message-Format + +``` +refactor(A1): extract OutputPanel component + +- Moved L1395-L1809 from arduino-simulator.tsx to output-panel.tsx +- Added React.memo wrapper +- Props: { activeTab, onTabChange, cliOutput, ... } — 13 props +- NO behavioral changes +- Tests: ✅ unit (187 passed), ✅ e2e (23 passed) +- Guardian check: git diff -- e2e/ → empty +``` + +--- + +## 8. Zusammenfassung v2 + +| Kennzahl | Audit v1 | Jetzt (eaf1220+Fixes) | Ziel (nach Roadmap v2) | +|----------|----------|------------------------|------------------------| +| Größte Datei (Source) | 2.761 LOC | 2.266 LOC | ~1.100 LOC | +| `routes.ts` | 744 LOC (1 Fn) | 587 LOC (4 Dateien) ✅ | — | +| `sandbox-runner.ts` | 1.479 LOC | 1.427 LOC | ~800 LOC | +| Max. Hooks/Komponente | 52 | ~38 | ~12 | +| Max. Props an AppHeader | ~35 | 32 | ~8 (3 Gruppen + 5) | +| Hook-Parameter (max) | 20+13=33 | 20+16=36 | ~8 (Options-Objekt) | +| Test-Duplikation (Load) | 1.731 LOC | 1.731 LOC | ~450 LOC | +| WS-Pfad Integrität | — | ✅ Stabil | ✅ Garantiert durch Guardians | + +**Was sich gegenüber v1 geändert hat:** + +1. **Roadmap ist defensiver:** Statt 3 "Phasen" (P0/P1/P2) gibt es jetzt atomare Schritte (A1–A5, B1–B4, C1, D1), jeweils mit explizitem Risk-Assessment +2. **Guardian-Tests sind definiert:** 13 Test-Suites als unveränderliche Invarianten +3. **Anti-Flicker-Regeln:** 5 verbindliche Regeln für Memoization und Referenzstabilität +4. **KI-Constraints:** Expliziter "Vertrag" mit Verboten und Pflichten +5. **WS-Pfad als Tabuzone:** Keine Extraktion darf den Message-Pfad verändern +6. **H3 ist erledigt:** `routes.ts` Modularisierung bereits umgesetzt — aus der Roadmap gestrichen diff --git a/client/src/pages/arduino-simulator.tsx b/client/src/pages/arduino-simulator.tsx index 9b63d101..b5ad6bc2 100644 --- a/client/src/pages/arduino-simulator.tsx +++ b/client/src/pages/arduino-simulator.tsx @@ -1681,7 +1681,7 @@ export default function ArduinoSimulator() {