Standwache: hat sich der Stand WAEHREND des Laufs geaendert?

Die zweite Sitzung hat die Luecke benannt: Das Arbeitsschloss
schuetzt den Commit und die Stempellaeufe, 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. 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.

DIE FRAGE IST EINE MESSUNG, KEINE ZUSTAENDIGKEIT

„Haelt jemand das Schloss" waere die falsche Frage, und zwar in
beide Richtungen:

  * Sie blockiert zu viel: Wer zwei Stunden am Schloss sitzt, haette
    in der Zeit keinen Prueflauf mehr. Eine Sicherung, die das eigene
    Arbeiten anhaelt, wird abgeschafft.
  * Sie blockiert zu wenig: Filipe am Editor hat kein Schloss, mein
    eigenes Werkzeug auch nicht.

Gefragt wird deshalb: Ist der Stand am Ende noch derselbe wie am
Anfang? Das trifft jede Quelle.

AN DER NOTBREMSE, NICHT IN 179 DATEIEN

Gemessen: 179 von 231 Pruef- und Messdateien rufen `notbremse`. Das
ist die eine Stelle, an der alle etwas bekommen — dieselbe
Ueberlegung wie bei der Notbremse selbst. Einzelne Dateien
nachzuruesten hilft nur bis zur naechsten ohne.

Fingerabdruck: Anzahl, Gesamtgroesse und juengste Aenderung von
workspace, assets, server, webdesign und den Seiten im
Wurzelverzeichnis — 670 Dateien in 27 ms. Kein Hash ueber den
Inhalt: Gesucht ist „hat jemand gespeichert", und das aendert immer
mindestens die Zeit.

BILDER ZAEHLEN NICHT. Die Messdateien legen selbst welche ab; eine
Wache, die bei jedem Bildschirmfoto anschlaegt, wird weggeklickt und
nimmt die echte Meldung mit.

SIE HAELT NICHTS AN. Ein Lauf, dessen Grundlage sich verschoben hat,
ist nicht „nicht in Ordnung" — er ist NICHT NACHSEHBAR. Deshalb
Rueckgabewert 3 (nicht 1, das waere eine Aussage ueber den Code; und
nicht 0, ein gruener Haken ueber einen Stand, den es nicht mehr
gibt, waere das Schlimmere) und ein ausdruecklicher Satz, dass das
kein Befund ist.

GEPRUEFT — pruef-arbeitsschloss 35 -> 48 Pruefungen, 0 Fehler:

  zwei Aufnahmen hintereinander gleich   (sonst waere jede Meldung
                                          wertlos)
  geaenderte Datei            -> faellt auf
  neue Datei                  -> faellt auf
  neues Bild                  -> faellt NICHT auf
  Lauf mit Aenderung          -> Rueckgabe 3, sagt NICHT NACHSEHBAR
  Lauf ohne Aenderung         -> Rueckgabe 0  (Gegenprobe; sonst
                                 waere „endet mit 3" auch wahr, wenn
                                 sie IMMER 3 meldet)

Alles in einem Wegwerf-Verzeichnis mit gefaelschtem Baum — moeglich,
weil die Wache ihre Wurzel aus ihrem EIGENEN Pfad ableitet. Waere
sie festgeschrieben, liesse sich die Wache nicht pruefen, ohne am
echten Haus zu wackeln.

UND AM ECHTEN LAUF NACHGEWIESEN: Mitten in pruef-arten eine Datei
gespeichert -> Rueckgabe 3, mit Groesse und Uhrzeit. Ohne Eingriff
bei neun Laeufen (treff 85, kalender 141, material 159,
erwaehnung 129, arten, dabei-optik, tagesblick, aufgabenbrett,
kopfleiste, seiteninhalt) kein einziger Fehlalarm.

EINE ERWARTUNG VON MIR WAR FALSCH, und die Wache hatte recht: Ich
hatte im gefaelschten Baum eine zaehlende Datei erwartet, es sind
zwei — die Seite UND die Kopie der Wache selbst im server-Ordner.
Der zaehlt mit, und das ist richtig: Eine geaenderte Pruefdatei
verschiebt den Stand genauso wie eine geaenderte Seite.

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
2026-10-01 23:18:13 +02:00
co-authored by Claude Opus 5
parent ec0ee61691
commit 775aabef38
3 changed files with 307 additions and 1 deletions
+25
View File
@@ -43,7 +43,32 @@
/** @param {number} fristMs Nach dieser Zeit gilt der Lauf als haengend. /** @param {number} fristMs Nach dieser Zeit gilt der Lauf als haengend.
* @param {string} wer Name für die Meldung. */ * @param {string} wer Name für die Meldung. */
import { standWache } from "./helfer-standwache.mjs";
export function notbremse(fristMs = 300_000, wer = "diese Pruefung") { 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; let schonAus = false;
const raus = (art, was) => { const raus = (art, was) => {
if (schonAus) return; if (schonAus) return;
+164
View File
@@ -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;
}
+118 -1
View File
@@ -31,7 +31,7 @@
===================================================================== */ ===================================================================== */
import { mkdtempSync, rmSync, mkdirSync, copyFileSync, writeFileSync, 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 { tmpdir } from "node:os";
import { join, dirname } from "node:path"; import { join, dirname } from "node:path";
import { fileURLToPath } from "node:url"; 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})`); `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 */ } try { rmSync(spiel, { recursive: true, force: true }); } catch { /* Aufraeumrest */ }
console.log(`\n${ok_} Pruefungen, ${fehl} Fehler, ${offen} nicht nachsehbar`); console.log(`\n${ok_} Pruefungen, ${fehl} Fehler, ${offen} nicht nachsehbar`);