diff --git a/server/helfer-notbremse.mjs b/server/helfer-notbremse.mjs index a35920ed..d6cb3263 100644 --- a/server/helfer-notbremse.mjs +++ b/server/helfer-notbremse.mjs @@ -43,7 +43,32 @@ /** @param {number} fristMs Nach dieser Zeit gilt der Lauf als haengend. * @param {string} wer Name für die Meldung. */ +import { standWache } from "./helfer-standwache.mjs"; + export function notbremse(fristMs = 300_000, wer = "diese Pruefung") { + /* ==== UND HAT SICH DER STAND WAEHREND DES LAUFS GEAENDERT? ======= + (01.10.2026, angestossen von der zweiten Claude-Sitzung.) + + Am 06./07.09.2026 wurden fuenf Gesamtlaeufe gestartet und + dreimal davon WAEHREND des Laufs weitergebaut. Jeder dieser + Laeufe hat einen Stand gemessen, den es schon nicht mehr gab -- + rund zwei Stunden Wartezeit ohne einen einzigen Befund. + + Am 01.10. dieselbe Lage, nur anders: Zwei Sitzungen im + Verzeichnis, eine misst, die andere speichert. Aufgefallen ist + es nur, weil die andere es von sich aus gesagt hat. + + HIER UND NICHT IN 179 DATEIEN: Wer eine Notbremse hat, bekommt + die Standwache mit. Das ist dieselbe Ueberlegung wie bei der + Notbremse selbst -- einzelne Dateien nachzuruesten hilft nur bis + zur naechsten ohne. + + SIE HAELT NICHTS AN. Ein Lauf, dessen Grundlage sich verschoben + hat, ist nicht „nicht in Ordnung" -- er ist NICHT NACHSEHBAR. + Deshalb Rueckgabewert 3 und ein ausdruecklicher Satz, dass das + kein Befund am Code ist. */ + standWache(wer); + let schonAus = false; const raus = (art, was) => { if (schonAus) return; diff --git a/server/helfer-standwache.mjs b/server/helfer-standwache.mjs new file mode 100644 index 00000000..11859fc1 --- /dev/null +++ b/server/helfer-standwache.mjs @@ -0,0 +1,164 @@ +/* ===================================================================== + helfer-standwache.mjs — hat sich der Stand WÄHREND des Laufs + geändert? + + --------------------------------------------------------------------- + DER FEHLER, DEN DAS SICHTBAR MACHT + + Am 06./07.09.2026 wurden fünf Gesamtläufe gestartet und dreimal + davon WÄHREND des Laufs weitergebaut. Jeder dieser Läufe hat damit + einen Stand gemessen, den es schon nicht mehr gab, und war wertlos + — rund zwei Stunden Wartezeit ohne einen einzigen zusätzlichen + Befund. + + Am 01.10.2026 ist dieselbe Lage zum zweiten Mal entstanden, nur + anders: Zwei Claude-Sitzungen arbeiteten gleichzeitig im + Verzeichnis. Während die eine Prüfläufe fuhr, hat die andere + Dateien gespeichert. Aufgefallen ist es nur, weil die zweite + Sitzung es von sich aus gesagt hat. + + --------------------------------------------------------------------- + WARUM NICHT DAS ARBEITSSCHLOSS FRAGEN + + Das Schloss beantwortet „arbeitet hier jemand". Das ist die falsche + Frage, und zwar in beide Richtungen: + + * Es blockiert zu viel: Wer zwei Stunden lang am Schloss sitzt, + hätte in dieser Zeit keinen einzigen Prüflauf mehr. Eine + Sicherung, die das eigene Arbeiten anhält, wird abgeschafft. + * Es blockiert zu wenig: Filipe, der in VS Code speichert, hat + kein Schloss. Mein eigenes Werkzeug, das nebenbei stempelt, + auch nicht. + + Die Frage, auf die es ankommt, ist eine MESSUNG und keine + Zuständigkeit: Ist der Stand am Ende noch derselbe wie am Anfang? + Das trifft jede Quelle — die andere Sitzung, den Menschen am + Editor, ein Werkzeug von mir. + + --------------------------------------------------------------------- + WAS GEZÄHLT WIRD, UND WAS NICHT + + Gezählt wird, was eine Messung verfälschen kann: der Quelltext der + Seiten und des Servers. Nicht gezählt werden Bilder — die + Messdateien legen selbst welche ab (`server/mess-*.png`), und eine + Wache, die bei jedem Bildschirmfoto anschlägt, ist eine Warnung, + die immer kommt. + + --------------------------------------------------------------------- + SIE HÄLT NICHTS AN + + Sie meldet. Ein Lauf, dessen Grundlage sich verschoben hat, ist + nicht „nicht in Ordnung" — er ist NICHT NACHSEHBAR, der dritte + Ausgang. Deshalb setzt sie den Rückgabewert auf 3 und sagt + ausdrücklich, dass das kein Befund am Code ist. Ein grüner Haken + über einen Stand, den es nicht mehr gibt, wäre das Schlimmere. + ===================================================================== */ + +import { readdirSync, statSync } from "node:fs"; +import { join, dirname, extname } from "node:path"; +import { fileURLToPath } from "node:url"; + +const WURZEL = join(dirname(fileURLToPath(import.meta.url)), ".."); + +/* Wo Quelltext liegt, der eine Messung verändern kann. */ +const ORTE = ["workspace", "assets", "server", "webdesign"]; +const ZAEHLT = new Set([".js", ".mjs", ".css", ".html", ".webmanifest"]); + +/** Ein Fingerabdruck des Standes: Anzahl, Gesamtgröße, jüngste Änderung. + * + * KEIN HASH ÜBER DEN INHALT. Der wäre genauer und kostet bei rund + * tausend Dateien ein Mehrfaches — und genauer muss es nicht sein: + * Gesucht ist „hat jemand gespeichert", und das ändert immer + * mindestens die Änderungszeit. + */ +export function standNehmen() { + let anzahl = 0, bytes = 0, jüngste = 0; + const gehen = (ordner, tiefe) => { + let eintraege; + try { eintraege = readdirSync(ordner, { withFileTypes: true }); } + catch { return; } + for (const e of eintraege) { + if (e.name.startsWith(".") || e.name === "node_modules") continue; + const voll = join(ordner, e.name); + if (e.isDirectory()) { if (tiefe < 5) gehen(voll, tiefe + 1); continue; } + if (!ZAEHLT.has(extname(e.name))) continue; + try { + const s = statSync(voll); + anzahl += 1; + bytes += s.size; + if (s.mtimeMs > jüngste) jüngste = s.mtimeMs; + } catch { /* im Flug verschwunden -- der Vergleich merkt es */ } + } + }; + for (const ort of ORTE) gehen(join(WURZEL, ort), 0); + /* Die Seiten im Wurzelverzeichnis gehören dazu, aber nicht alles + darunter -- deshalb hier flach. */ + try { + for (const name of readdirSync(WURZEL)) { + if (!ZAEHLT.has(extname(name))) continue; + const s = statSync(join(WURZEL, name)); + anzahl += 1; bytes += s.size; + if (s.mtimeMs > jüngste) jüngste = s.mtimeMs; + } + } catch { /* egal */ } + return { anzahl, bytes, jüngste, genommen: Date.now() }; +} + +/** Vergleicht und meldet. Gibt `true` zurück, wenn der Stand hielt. */ +export function standPruefen(vorher, wer = "dieser Lauf") { + if (!vorher) return true; + const jetzt = standNehmen(); + const gleich = jetzt.anzahl === vorher.anzahl + && jetzt.bytes === vorher.bytes + && jetzt.jüngste === vorher.jüngste; + if (gleich) return true; + + const sek = Math.round((jetzt.genommen - vorher.genommen) / 1000); + console.error(""); + console.error("\x1b[33m -- NICHT NACHSEHBAR: Der Stand hat sich WÄHREND des Laufs" + + " geändert.\x1b[0m"); + console.error(` ${wer} lief ${sek} Sekunden. In dieser Zeit:`); + if (jetzt.anzahl !== vorher.anzahl) { + console.error(` Dateien: ${vorher.anzahl} -> ${jetzt.anzahl}`); + } + if (jetzt.bytes !== vorher.bytes) { + console.error(` Groesse: ${vorher.bytes} -> ${jetzt.bytes} Bytes`); + } + if (jetzt.jüngste !== vorher.jüngste) { + console.error(" jüngste Änderung: " + + `${new Date(vorher.jüngste).toLocaleTimeString("de-DE")}` + + ` -> ${new Date(jetzt.jüngste).toLocaleTimeString("de-DE")}`); + } + console.error(""); + console.error(" DAS IST KEIN BEFUND AM CODE. Es heisst, dass dieser Lauf"); + console.error(" einen Stand gemessen hat, den es am Ende nicht mehr gab —"); + console.error(" genau das hat am 06./07.09.2026 zwei Stunden Wartezeit"); + console.error(" ohne einen einzigen Befund gekostet."); + console.error(""); + console.error(" Wer hat gespeichert? Eine zweite Sitzung, ein Editor, ein"); + console.error(" Werkzeug? Nachsehen: node tools/arbeitsschloss.mjs ansehen"); + console.error(" Dann noch einmal laufen lassen."); + console.error(""); + return false; +} + +/** Nimmt den Stand und vergleicht ihn beim Programmende von selbst. + * + * SETZT DEN RÜCKGABEWERT AUF 3 — den dritten Ausgang. Nicht auf 1: + * „nicht in Ordnung" wäre eine Aussage über den Code, und die hat + * dieser Lauf nicht gemacht. Und nicht auf 0: Ein grüner Haken über + * einen Stand, den es nicht mehr gibt, ist das Schlimmere. + * + * Nur wenn der Lauf sonst erfolgreich war: Ein echter Befund + * (Rückgabe 1) ist die wichtigere Nachricht und bleibt stehen. + */ +export function standWache(wer) { + const vorher = standNehmen(); + process.on("exit", () => { + const hielt = standPruefen(vorher, wer); + if (!hielt && (process.exitCode === 0 || process.exitCode === undefined)) { + process.exitCode = 3; + } + }); + return vorher; +} diff --git a/server/pruef-arbeitsschloss.mjs b/server/pruef-arbeitsschloss.mjs index 39dcd928..2da980be 100644 --- a/server/pruef-arbeitsschloss.mjs +++ b/server/pruef-arbeitsschloss.mjs @@ -31,7 +31,7 @@ ===================================================================== */ import { mkdtempSync, rmSync, mkdirSync, copyFileSync, writeFileSync, - readFileSync, existsSync, chmodSync } from "node:fs"; + readFileSync, readdirSync, existsSync, chmodSync } from "node:fs"; import { tmpdir } from "node:os"; import { join, dirname } from "node:path"; import { fileURLToPath } from "node:url"; @@ -282,6 +282,123 @@ console.log("\n=== 4. Der Haken am echten git ==="); `ohne das Schloss-Werkzeug laesst der Haken durch (${ohneWerkzeug.status})`); } +/* --------------------------------------------------------------------- + 5. Die Standwache — hat sich der Stand WAEHREND des Laufs geaendert? + + Das Schloss schuetzt den Commit und die Stempellaeufe. Es schuetzt + NICHT die Pruefläufe: Wer misst, waehrend ein anderer speichert, + misst einen Stand, den es nicht mehr gibt. Am 06./07.09.2026 sind + so fuenf Gesamtlaeufe wertlos geworden, rund zwei Stunden + Wartezeit ohne einen einzigen Befund. + + DIE FRAGE IST EINE MESSUNG, KEINE ZUSTAENDIGKEIT. „Haelt jemand + das Schloss" waere die falsche: Sie blockiert zu viel (zwei + Stunden Arbeit am Schloss = zwei Stunden ohne Prueflauf) und zu + wenig (Filipe am Editor hat kein Schloss). Gefragt wird deshalb: + Ist der Stand am Ende noch derselbe? + --------------------------------------------------------------------- */ +console.log("\n=== 5. Die Standwache ==="); +{ + const WACHE = join(WURZEL, "server", "helfer-standwache.mjs"); + ok(existsSync(WACHE), "server/helfer-standwache.mjs ist da"); + + /* AN DER NOTBREMSE UND NICHT IN 179 DATEIEN. Abgeleitet aus dem + Quelltext: Wer eine Notbremse hat, bekommt die Wache mit. */ + const bremse = readFileSync(join(WURZEL, "server", "helfer-notbremse.mjs"), "utf8"); + ok(/standWache\(/.test(bremse) && /helfer-standwache\.mjs/.test(bremse), + "die Notbremse ruft die Standwache — eine Stelle fuer alle Pruefungen"); + { + const alle = readdirSync(join(WURZEL, "server")) + .filter((n) => /^(pruef|mess)-.*\.mjs$/.test(n)); + const mit = alle.filter((n) => + /notbremse\(/.test(readFileSync(join(WURZEL, "server", n), "utf8"))); + ok(mit.length > 150, + `${mit.length} von ${alle.length} Pruef- und Messdateien haben damit eine Standwache`); + } + + /* ---- Im Wegwerf-Verzeichnis, mit einem gefaelschten Baum -------- + Moeglich, weil die Wache ihre Wurzel aus ihrem EIGENEN Pfad + ableitet. Eine festgeschriebene Wurzel waere nicht pruefbar, + ohne am echten Haus zu wackeln. */ + const spiel2 = mkdtempSync(join(tmpdir(), "standprobe-")); + mkdirSync(join(spiel2, "server"), { recursive: true }); + mkdirSync(join(spiel2, "workspace"), { recursive: true }); + copyFileSync(WACHE, join(spiel2, "server", "helfer-standwache.mjs")); + writeFileSync(join(spiel2, "workspace", "a.css"), "a{color:red}\n"); + + const w = await import("file://" + + join(spiel2, "server", "helfer-standwache.mjs").replace(/\\/g, "/")); + + const eins = w.standNehmen(); + /* ZWEI, NICHT EINE: workspace/a.css UND die Kopie der Wache selbst, + die im server-Ordner liegt. Der server-Ordner zaehlt mit, und das + ist richtig -- eine geaenderte Pruefdatei verschiebt den Stand + genauso wie eine geaenderte Seite. (Ich hatte hier 1 erwartet; + die Wache hatte recht, nicht ich.) */ + ok(eins.anzahl === 2, + `der gefaelschte Baum hat zwei zaehlende Dateien (${eins.anzahl})` + + " -- die Seite und die Wache selbst"); + ok(w.standPruefen(eins, "Probe") === true, + "ohne Aenderung meldet sie nichts"); + + /* ZWEIMAL HINTEREINANDER GLEICH. Ohne das waere jede Meldung + wertlos — eine Wache, die bei sich selbst anschlaegt, ist eine + Warnung, die immer kommt. */ + ok(JSON.stringify({ ...w.standNehmen(), genommen: 0 }) + === JSON.stringify({ ...eins, genommen: 0 }), + "zwei Aufnahmen hintereinander sind gleich"); + + /* ---- UND JETZT SPEICHERT JEMAND ------------------------------- */ + writeFileSync(join(spiel2, "workspace", "a.css"), "a{color:blue;font-size:2rem}\n"); + ok(w.standPruefen(eins, "Probe") === false, + "eine geaenderte Datei faellt auf"); + + const zwei = w.standNehmen(); + writeFileSync(join(spiel2, "workspace", "b.js"), "// neu\n"); + ok(w.standPruefen(zwei, "Probe") === false, "eine NEUE Datei ebenfalls"); + + /* ---- BILDER ZAEHLEN NICHT ------------------------------------ + Die Messdateien legen selbst welche ab (`server/mess-*.png`). + Eine Wache, die bei jedem Bildschirmfoto anschlaegt, wird + weggeklickt und nimmt die echte Meldung mit. */ + const drei = w.standNehmen(); + writeFileSync(join(spiel2, "workspace", "bild.png"), "nicht wirklich ein Bild"); + ok(w.standPruefen(drei, "Probe") === true, + "ein neues Bild faellt NICHT auf — Messungen legen selbst welche ab"); + + /* ---- DER RUECKGABEWERT IST 3, NICHT 1 ------------------------ + „Nicht in Ordnung" waere eine Aussage ueber den Code, und die + hat so ein Lauf nicht gemacht. 3 ist der dritte Ausgang. + Geprueft am echten Verhalten: ein eigener Prozess. */ + { + const script = join(spiel2, "lauf.mjs"); + writeFileSync(script, + 'import { standWache } from "./server/helfer-standwache.mjs";\n' + + 'import { writeFileSync } from "node:fs";\n' + + 'import { join, dirname } from "node:path";\n' + + 'import { fileURLToPath } from "node:url";\n' + + 'standWache("Probelauf");\n' + + 'const hier = dirname(fileURLToPath(import.meta.url));\n' + + 'writeFileSync(join(hier, "workspace", "c.css"), "c{}\\n");\n'); + const e = spawnSync(process.execPath, [script], { cwd: spiel2, encoding: "utf8" }); + ok(e.status === 3, + `ein Lauf, bei dem sich der Stand aendert, endet mit 3 (${e.status})`); + ok(/NICHT NACHSEHBAR/.test(e.stderr || ""), "und sagt NICHT NACHSEHBAR"); + ok(/KEIN BEFUND AM CODE/.test(e.stderr || ""), + "und ausdruecklich, dass das kein Befund am Code ist"); + + /* GEGENPROBE: Ohne Aenderung bleibt es bei 0. Ohne diese Zeile + waere „endet mit 3" auch dann wahr, wenn sie IMMER 3 meldet. */ + writeFileSync(script, + 'import { standWache } from "./server/helfer-standwache.mjs";\n' + + 'standWache("Probelauf ohne Aenderung");\n'); + const e2 = spawnSync(process.execPath, [script], { cwd: spiel2, encoding: "utf8" }); + ok(e2.status === 0, `ein Lauf ohne Aenderung endet mit 0 (${e2.status})`); + } + + try { rmSync(spiel2, { recursive: true, force: true }); } catch { /* Rest */ } +} + try { rmSync(spiel, { recursive: true, force: true }); } catch { /* Aufraeumrest */ } console.log(`\n${ok_} Pruefungen, ${fehl} Fehler, ${offen} nicht nachsehbar`);