// Die Wächter — mechanische Prüfung der Regeln aus KONZEPT-WINDOWS.md § 4.3. // Sie LESEN Quelltext, sie führen nichts aus. Jede Regel ist aus einem // bezahlten Fehler abgeleitet; die Fundstelle steht jeweils dabei. import { readdirSync, readFileSync, statSync } from 'node:fs' import { join, relative, sep } from 'node:path' import { describe, expect, it } from 'vitest' const WURZEL = join(__dirname, '..') const SRC = join(WURZEL, 'src') function quelldateien(ordner: string): string[] { const ergebnis: string[] = [] for (const name of readdirSync(ordner)) { const pfad = join(ordner, name) if (statSync(pfad).isDirectory()) { ergebnis.push(...quelldateien(pfad)) } else if (/\.(ts|tsx)$/.test(name)) { ergebnis.push(pfad) } } return ergebnis } function relativPosix(pfad: string): string { return relative(WURZEL, pfad).split(sep).join('/') } const dateien = quelldateien(SRC).map((pfad) => ({ pfad: relativPosix(pfad), inhalt: readFileSync(pfad, 'utf8'), })) describe('R1 — Win32 lebt nur an den zwei benannten Orten', () => { // § 4.3 R1: kein koffi-Import irgendwo sonst. Der zweite Ort ist die // Prozess-Leine im Haupt — Begründung im Kopf von src/haupt/leine.ts: // Das Job-Handle muss bei dem Prozess liegen, dessen Tod alles mitreißt. // Der DRITTE (seit 01.09.2026) ist der Fensterrand: DwmSetWindowAttribute // braucht das Fensterhandle und kann deshalb nur im Haupt liegen. Die // Regel bleibt, was sie war — „Win32 nur an benannten Orten"; sie zählt // jetzt drei statt zwei, und der Test hält alles andere weiterhin dicht. const erlaubt = new Set([ 'src/haupt/leine.ts', 'src/haupt/fensterrand.ts', 'src/kern/laufwerk/win32.ts', ]) it('koffi wird nirgendwo sonst angefasst', () => { const verstoesse = dateien .filter((d) => !erlaubt.has(d.pfad)) .filter((d) => /from\s+['"]koffi['"]|require\(\s*['"]koffi['"]\s*\)/.test(d.inhalt)) .map((d) => d.pfad) expect(verstoesse).toEqual([]) }) }) describe('R2/R4 — kein Fehlschlag verschwindet still', () => { it('kein leerer catch-Block', () => { // R4: „Kein stilles catch {}" — ein except-pass hat einmal eine Stunde // gekostet (AGENTS.md). Wer einen Fehler bewusst trägt, schreibt hin warum. const verstoesse = dateien .filter((d) => /catch\s*(\([^)]*\))?\s*\{\s*\}/.test(d.inhalt)) .map((d) => d.pfad) expect(verstoesse).toEqual([]) }) it('kein catch, das leere Listen/Objekte als Antwort erfindet', () => { // R2: Fünfmal catch(() => []) im alten UI — jeder verpasste Abruf hieß // „es gibt keine Jobs", die Liste leerte sich im Sekundentakt. const muster = /catch\s*(\([^)]*\))?\s*\{\s*return\s+(\[\s*\]|\{\s*\})/ const pfeilMuster = /catch\s*\(\s*\(\s*\)\s*=>\s*(\[\s*\]|\{\s*\})\s*\)/ const verstoesse = dateien .filter((d) => muster.test(d.inhalt) || pfeilMuster.test(d.inhalt)) .map((d) => d.pfad) expect(verstoesse).toEqual([]) }) }) describe('§ 4.1 — kein HTTP-Server, kein Port, kein localhost', () => { it('niemand öffnet einen Server oder lauscht auf einem Port', () => { // Ein lokaler Webserver in einer Einzelplatz-Anwendung ist eine offene // Tür ohne Gegenwert (§ 4.1/§ 14). ELECTRON_RENDERER_URL (dev-Server von // electron-vite) ist Werkzeug, kein Programmteil — der Treffer auf den // reinen Variablennamen ist erlaubt. const muster = /createServer\s*\(|\.listen\s*\(\s*\d|from\s+['"]express['"]|localhost:\d/ const verstoesse = dateien.filter((d) => muster.test(d.inhalt)).map((d) => d.pfad) expect(verstoesse).toEqual([]) }) }) describe('Quelltext-Hygiene — keine rohen Steuerbytes', () => { it('keine Datei enthält Steuerzeichen außer Tab/LF/CR', () => { // Zweimal passiert (30.08.2026): Escape-Sequenzen wie \x00 landeten // als ECHTE Bytes in der Datei — grep meldet „Binary file", Diffs // leiden, und ein Test sah dadurch anders aus, als sein Quelltext las. const testDateien = readdirSync(join(WURZEL, 'test')) .filter((name) => /\.(ts|tsx)$/.test(name)) .map((name) => ({ pfad: `test/${name}`, inhalt: readFileSync(join(WURZEL, 'test', name), 'utf8'), })) const verstoesse = [...dateien, ...testDateien] .filter((d) => /[\x00-\x08\x0b\x0c\x0e-\x1f]/.test(d.inhalt)) .map((d) => d.pfad) expect(verstoesse).toEqual([]) }) }) describe('Nachrichten-Schema — der Prüfer kennt JEDE Nachrichtenart', () => { // Der teure Fund vom 01.09.2026: 'titel-lesen' und 'rip-ueberspringen' // standen in der FensterNachricht-Union, aber NICHT in // istFensterNachricht(). Der Kern verwarf sie — still. Im Fenster stand // ewig „Rippy liest die Titel …", und nirgends stand ein Grund. // // Dieser Test liest den Quelltext: Jede `art: '…'` der Union muss im // Prüfer wörtlich vorkommen. Eine neue Nachricht ohne Prüfung fällt // damit sofort auf, statt erst am lebenden Objekt. const schema = readFileSync(join(WURZEL, 'src', 'gemeinsam', 'nachrichten.ts'), 'utf8') function artenAus(unionName: string): string[] { const start = schema.indexOf(`export type ${unionName} =`) expect(start, `${unionName} nicht gefunden`).toBeGreaterThan(-1) // Der Union-Block reicht bis zur nächsten Deklaration auf oberster Ebene. const rest = schema.slice(start + `export type ${unionName} =`.length) const ende = rest.indexOf('\nexport ') const block = ende > 0 ? rest.slice(0, ende) : rest return [...new Set([...block.matchAll(/art:\s*'([a-z0-9-]+)'/g)].map((m) => m[1]))] } it('istFensterNachricht kennt jede FensterNachricht', () => { const pruefer = schema.slice(schema.indexOf('export function istFensterNachricht')) const fehlend = artenAus('FensterNachricht').filter((art) => !pruefer.includes(`'${art}'`)) expect(fehlend).toEqual([]) }) it('istKernNachricht kennt jede KernNachricht', () => { const pruefer = schema.slice(schema.indexOf('export function istKernNachricht')) const fehlend = artenAus('KernNachricht').filter((art) => !pruefer.includes(`'${art}'`)) expect(fehlend).toEqual([]) }) it('der Kern verwirft nichts still', () => { // Wer eine Nachricht wegwirft, sagt WARUM (R4). const kern = readFileSync(join(WURZEL, 'src', 'kern', 'index.ts'), 'utf8') expect(kern).not.toMatch(/if\s*\(!istFensterNachricht\([^)]*\)\)\s*return/) }) }) describe('§ 6.7 — EINE Quelle für Einstellungen', () => { it('node:sqlite wird nur in kern/speicher/db.ts angefasst', () => { // Der rc11-Fund: Oberfläche schrieb in die Datenbank, der Betrieb las // aus einer Datei, die niemand schrieb. Hier ist die Datenbank EIN Modul. const verstoesse = dateien .filter((d) => d.pfad !== 'src/kern/speicher/db.ts') .filter((d) => /['"]node:sqlite['"]/.test(d.inhalt)) .map((d) => d.pfad) expect(verstoesse).toEqual([]) }) })