Ampel / ampel (push) Successful in 1m48s
Commander: "das funktioniert nicht, du kannst gerne am lebenen objekt
pruefen". Der Dialog blieb bei "Rippy liest die Titel der Disc ..."
stehen, ohne Fehler, ohne Ende.
Ursache, am lebenden Objekt gemessen: NICHT der Info-Lauf.
makemkvcon -r --noscan info dev:G: -> 0,8 s, TCOUNT:0
MSG:5042 "konnte keine verwendbaren optischen Laufwerke finden"
Der Lauf antwortet also sofort. Der Fehler lag in MEINEM Code von 5.2.0:
istFensterNachricht() kannte 'titel-lesen' und 'rip-ueberspringen' nicht
— beide standen in der FensterNachricht-Union, aber nicht im Pruefer.
Der Kern verwarf sie mit `if (!istFensterNachricht(frage)) return`.
STILL. Genau die R4-Wunde, vor der die Hausordnung warnt.
Auf drei Ebenen behoben:
1. Der Pruefer kennt beide Nachrichten.
2. Eine verworfene Nachricht ist jetzt LAUT: console.error plus
kern-fehler ins Fenster mit der Art, die verworfen wurde.
3. Neuer Waechter-Test (waechter.test.ts): Er liest das Schema und
vergleicht JEDE `art: '...'` der Unions mit dem jeweiligen Pruefer;
dazu prueft er, dass der Kern nicht mehr still verwirft.
GEGENPROBE gemacht: Mit zurueckgenommener Reparatur schlaegt er fehl
("expected [ 'titel-lesen' ] to deeply equal []"), mit Reparatur ist
er gruen. Kein Deko-Test.
Dazu: Der Info-Lauf reicht die URSACHE durch (DiscInfo.ursachen aus den
kritischen MSG-Nummern). Kam nichts heraus und MakeMKV hat einen Grund
genannt, steht im Dialog der Grund statt "keine Titel gefunden" — beim
5042-Zustand also die Abhilfe (Disc neu einlegen, Laufwerk ab- und
anstecken, notfalls neu starten) statt eines Fingerzeigs auf die Disc.
Neu test/messung.titelwahl.test.ts — faehrt am echten Laufwerk genau den
Weg des Kerns nach (Info-Lauf, Vorauswahl, Dialog-Fehlertext).
Zweimal in dieselbe Heredoc-Falle getappt (AGENTS.md warnt davor):
Escape-Sequenzen wurden halbiert, ein rohes CR landete in makemkv.ts.
Beides korrigiert; der Hygiene-Waechter haette das CR auch gefangen.
260 Tests gruen (vorher 257), Typpruefung sauber. Version 5.3.1.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
159 lines
7.0 KiB
TypeScript
159 lines
7.0 KiB
TypeScript
// 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([])
|
|
})
|
|
})
|