Files
dogfather-universe/tools/nachtlauf.mjs
T
DogFatherGitandClaude Opus 5 39ca642a52 Acht Pruefungen standen Nacht fuer Nacht als rot da -- sie waren gruen
Der naechtliche Lauf meldete heute frueh "37 Pruefungen sind rot".
Acht davon waren gar nicht rot: fehlerfrei durchgelaufen, Exitcode 0,
keine einzige FEHL-Zeile. Der Laeufer zaehlte bei ihnen aber NULL
Einzelpruefungen, und "null Pruefungen ist ein Fehler" -- eine Regel,
die richtig ist und hier das Falsche traf.

DER GRUND WAR EIN GROSSBUCHSTABE. Der Zaehler suchte `(ok|FEHL)`;
pruef-kachelfarben, -livepunkt, -tippziele, -turn-wege, -leerzustand,
-nachfrage, -tagesruf und -anruf-klingelt schreiben `  OK `.

Zwei Schaeden auf einmal:
  1. Ihre Punkte fehlten in der Gesamtzahl -- allein in fuenf der acht
     sind das 114 Stueck. Ausgerechnet die Zahl, die vor stillen
     Aussetzern schuetzen soll, war selbst eine Luecke.
  2. Acht dauerhaft rote Eintraege in einer Notiz, die morgens gelesen
     wird. Eine Warnung, die immer kommt, ist keine mehr.

Der Zaehler liest jetzt beide Schreibweisen. Die Dateien werden NICHT
vereinheitlicht: Acht umzuschreiben waeren acht Gelegenheiten, etwas
kaputtzumachen -- und die neunte, die morgen jemand anlegt, schreibt
ohnehin wieder, was sie will.

DAZU EINE PRUEFUNG, DIE DAS FESTHAELT (server/pruef-pruefzaehler.mjs).
Ein Kommentar haette den naechsten Fall nicht verhindert. Sie holt
sich den Ausdruck AUS alles-pruefen.mjs (zwei Fassungen desselben
Musters laufen auseinander), startet vier der schnellsten Pruefungen
wirklich -- je zwei in beiden Schreibweisen -- und hat drei
Gegenproben: Sie darf nicht wahllos zaehlen, sie muss zwei als zwei
zaehlen, und sie muss null als null sehen koennen, sonst waere "null
ist ein Fehler" blind.

UND DIE NOTIZ SAGT JETZT, WAS SIE MEINT. Statt "(keine Fehlerzeile
gefunden)" steht bei diesen Faellen: "Kein Befund -- es wurde keine
einzige Pruefung GEZAEHLT", mit der Erklaerung dazu. Fuer alles andere
ohne Fehlerzeile gibt es den dritten Ausgang ("Konnte nicht sagen,
woran es lag") samt den letzten Ausgabezeilen.

NEBENBEFUND, GEFUNDEN VON pruef-rueckmeldung: Die Tuer ins andere Haus
trug Ton 40 -- dieselbe Nummer wie "Entwicklung". Auf DogFathers Wand
standen zwei Kacheln in derselben Farbe, und er ist der Einzige, der
beide sieht; deshalb ist es nie jemandem aufgefallen.

  Die naheliegende Antwort waere eine 46. Farbe gewesen. Gerechnet kam
  ein Abstand von 0,0862 heraus -- unter der Hausgrenze von 0,09. Der
  Farbraum ist bei 45 Toenen voll, und eine zweite benannte Ausnahme
  am selben Tag waere der Anfang vom Ende der Regel.

  Richtig ist die andere Antwort: Die Tuer ist gar keine
  Bereichskachel. Sie fuehrt hinaus und gehoert keinem Bereich an --
  also bekommt sie keine Bereichsfarbe, sondern faellt auf --akzent
  zurueck. Damit unterscheidet sie sich von allen 45 anderen.

  Gefunden hat das die Pruefung erst, nachdem sie SAGEN konnte, welche
  Nummer doppelt ist. Vorher stand dort nur "kein Farbton doppelt (34
  Kacheln vom Server)" -- damit sucht man in vier Listen ueber zwei
  Dateien.

  Dazu: start.js schreibt kein data-ton="undefined" mehr (String()
  macht aus einem fehlenden Wert sonst ein Wort, und jede Pruefung,
  die Toene zaehlt, liest dann Text statt Zahl).

WAS MICH VOR DER FALSCHEN REPARATUR BEWAHRT HAT: Meine erste Vermutung
war, die FEHL-Zeilen staenden zu weit hinten im Ausgabefenster. Die
Gegenprobe dazu wurde rot -- null Dateien. Genau dafuer ist sie da.

GEMESSEN:
- pruef-pruefzaehler (neu): 7 Pruefungen, 0 Fehler.
- pruef-rueckmeldung: 30 geprueft, 0 Fehler (war rot).
- pruef-kachelfarben 26/0, pruef-haus-trennung 100/0,
  pruef-crew-adresse 153/0.
- tools/.nachtlauf-* ist jetzt ignoriert: Laufzeitzustand, der sich
  jede Nacht aendert. Das Ergebnis steht in der Vault-Notiz.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-25 06:46:49 +02:00

390 lines
17 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* =====================================================================
nachtlauf.mjs — einmal in der Nacht alles prüfen und es AUFSCHREIBEN
Filipe am 23.09.2026 auf die Frage, ob ein nächtlicher Lauf die
Prüfungen selbst anstoßen und morgens sagen soll, welche rot sind:
„ja".
---------------------------------------------------------------------
WARUM ES DAS BRAUCHT — drei Funde an EINEM Tag
Am 23.09.2026 sind mir drei Prüfungen aufgefallen, die rot waren,
ohne dass es jemandem aufgefallen wäre:
pruef-modi-wortleck seit dem 22.09. — der verborgene Rollenname
stand offen im Netz
pruef-zuteilung stürzte ab und prüfte danach gar nichts mehr
pruef-push-ziel meldete einen Befund, der in Wahrheit eine
erfüllte Bestellung war
Alle drei habe ich durch ZUFALL gefunden — weil sie neben dem lagen,
woran ich gerade gebaut habe. Prüfungen laufen nur, wenn jemand sie
startet, und man startet die, an denen man arbeitet.
---------------------------------------------------------------------
WOHIN DAS ERGEBNIS GEHT — und wohin ausdrücklich NICHT
In eine Notiz im Vault. NICHT in die laufende Anwendung.
Das ist keine Bequemlichkeit, sondern die Lehre vom 04.09.2026: Ein
Prüflauf ist kein harmloser Lesevorgang, sein Ergebnis wird
VERÖFFENTLICHT. Damals hat ein Netzproblem bei npm einen nächtlichen
Selbsttest rot gemacht, und ab da stand VanVan in roter Schrift auf
dem Startbildschirm, die Buchführung sei womöglich verletzt. An der
Buchführung war nichts.
Wer einen Prüflauf anstößt, muss vorher überlegen, was der
SCHLECHTESTE Ausgang auf dem Bildschirm einer Nutzerin anrichtet.
Hier richtet er nichts an: Er schreibt eine Notiz, die Filipe und ich
lesen. Die Community sieht davon nichts.
---------------------------------------------------------------------
DREI AUSGÄNGE, NICHT ZWEI
in Ordnung alles grün
nicht in Ordnung mindestens eine Datei rot
KONNTE NICHT NACHSEHEN der Lauf hat gar nicht stattgefunden
Der dritte ist der wichtige. Wenn der Rechner nachts aus war, gibt es
kein Ergebnis — und ein FEHLENDES Ergebnis sieht sonst genauso aus
wie ein altes gutes. Die Notiz sagt deshalb immer, WANN zuletzt
wirklich gemessen wurde, und ab zwei Tagen ohne Lauf steht das ganz
oben.
---------------------------------------------------------------------
DIE ZAHL WIRD MIT GESTERN VERGLICHEN
Ein grüner Lauf ist kein Beweis, solange nicht auch die ANZAHL
stimmt (28.08.2026, RunOne: 789 statt 804 Prüfungen, weil acht je
Gerät stillschweigend weggefallen waren). Diese Datei merkt sich
deshalb den letzten Stand und meldet jeden Rückgang — auch wenn
alles grün ist.
AUFRUF:
node tools/nachtlauf.mjs normal
node tools/nachtlauf.mjs --jetzt auch wenn gerade gearbeitet wird
===================================================================== */
import { spawn } from "node:child_process";
import {
writeFileSync, readFileSync, existsSync, mkdirSync, unlinkSync, statSync,
} from "node:fs";
import { join, dirname } from "node:path";
import { fileURLToPath } from "node:url";
const HIER = dirname(fileURLToPath(import.meta.url));
const WURZEL = join(HIER, "..");
/* Der Vault -- dort liest Filipe. Absichtlich fest und nicht geraten:
Wenn der Ordner umzieht, soll das auffallen und nicht still in einen
neuen Ordner schreiben, den niemand aufmacht. */
const VAULT = "C:/Users/qciga/Documents/Obelix/DogFather";
const NOTIZ = join(VAULT, "02 Projekte", "Pruefstand.md");
const ZUSTAND = join(WURZEL, "tools", ".nachtlauf-stand.json");
const SCHLOSS = join(WURZEL, "tools", ".nachtlauf-laeuft");
/* ZWEI LÄUFE GLEICHZEITIG SIND KEIN LAUF, SONDERN ZWEI KAPUTTE
(06.09.2026, RunOne). Beide belegen dieselben Ports und legen
dieselben Testdaten an; heraus kommen Fehler, die es nicht gibt --
und wer sie für echt hält, sucht stundenlang an Stellen, an denen
nichts kaputt ist.
DREI STUNDEN, dann gilt das Schloss als vergessen. Ein Schloss, das
nie verfällt, blockiert beim nächsten Stromausfall für immer -- und
was blockiert, wird abgeschafft. */
const SCHLOSS_FRIST_MS = 3 * 60 * 60 * 1000;
const zweiZiffern = (n) => String(n).padStart(2, "0");
function zeitWort(d = new Date()) {
return `${zweiZiffern(d.getDate())}.${zweiZiffern(d.getMonth() + 1)}.${d.getFullYear()}`
+ ` um ${zweiZiffern(d.getHours())}:${zweiZiffern(d.getMinutes())}`;
}
function schlossNehmen() {
if (existsSync(SCHLOSS)) {
let alter = Infinity;
try { alter = Date.now() - statSync(SCHLOSS).mtimeMs; } catch { /* egal */ }
if (alter < SCHLOSS_FRIST_MS) {
let wer = "";
try { wer = readFileSync(SCHLOSS, "utf8").trim(); } catch { /* egal */ }
return { ok: false, grund: `Es läuft schon einer (seit ${Math.round(alter / 60000)} min, ${wer}).` };
}
/* Älter als die Frist: Der gehört niemandem mehr. */
try { unlinkSync(SCHLOSS); } catch { /* egal */ }
}
try {
writeFileSync(SCHLOSS, `PID ${process.pid}, gestartet ${zeitWort()}\n`, { flag: "wx" });
return { ok: true };
} catch {
return { ok: false, grund: "Das Schloss ließ sich nicht setzen." };
}
}
function schlossAbgeben() { try { unlinkSync(SCHLOSS); } catch { /* egal */ } }
function standLesen() {
try { return JSON.parse(readFileSync(ZUSTAND, "utf8")); } catch { return null; }
}
/* ---------------------------------------------------------------------
Der Lauf selbst -- alles-pruefen.mjs macht die eigentliche Arbeit.
KEIN ZWEITER LÄUFER: Dort stecken die Frist je Datei, das Zählen der
Einzelpunkte und die Regel "null Prüfungen ist ein Fehler". Das hier
noch einmal zu bauen hiesse, zwei Rechnungen über dieselbe Frage zu
haben -- und eine davon wäre irgendwann anders.
--------------------------------------------------------------------- */
function laufen() {
return new Promise((fertig) => {
const start = Date.now();
const kind = spawn(process.execPath, [join("tools", "alles-pruefen.mjs")],
{ cwd: WURZEL, stdio: ["ignore", "pipe", "pipe"] });
let aus = "";
kind.stdout.on("data", (d) => { aus += d; });
kind.stderr.on("data", (d) => { aus += d; });
kind.on("error", (f) => fertig({ ging: false, grund: String(f?.message || f), aus }));
kind.on("close", (code) => fertig({
ging: true, code, aus,
minuten: Math.round((Date.now() - start) / 60000),
}));
});
}
/** Aus der Ausgabe die Zahlen holen -- gelesen, nicht geschätzt. */
function auswerten(aus) {
const dateien = aus.match(/Dateien:\s+(\d+) von (\d+)/);
const punkte = aus.match(/Einzelpunkte:\s+(\d+) von (\d+)/);
const rote = [];
for (const treffer of aus.matchAll(/^### (pruef-[\w-]+\.mjs) \(Exitcode (-?\d+)\)$/gm)) {
/* Die erste FEHL-Zeile darunter ist die Auskunft, die man braucht --
der Rest ist Zusammenhang.
ZWEI FENSTER, NICHT EINS (25.09.2026): Bisher wurde nur in den
ersten 900 Zeichen gesucht. Prüfungen mit viel Vorlauf (die
browsergestützten vor allem) haben ihre FEHL-Zeile weiter
hinten -- vier davon standen deshalb am 25.09. als „keine
Fehlerzeile gefunden" in der Notiz, und man konnte ihnen nicht
ansehen, ob sie kaputt waren oder nur lang. Jetzt wird zuerst
eng gesucht (damit die erste, wichtigste Zeile gewinnt) und
danach im ganzen Abschnitt. */
const ab = aus.indexOf(treffer[0]) + treffer[0].length;
const bisNaechste = aus.indexOf("\n### pruef-", ab);
const ganz = aus.slice(ab, bisNaechste === -1 ? ab + 20000 : bisNaechste);
const suchen = (text) => (text.split("\n")
.find((z) => /FEHL|Error|error:/.test(z)) || "").trim();
let erste = suchen(ganz.slice(0, 900)) || suchen(ganz);
/* UND WENN GAR NICHTS DASTEHT, IST DAS AUCH EINE AUSKUNFT.
Dann sind die letzten Zeilen das Beste, was es gibt -- zusammen
mit dem Exitcode. Eine Meldung „kaputt, weiss nicht warum" ist
ehrlich; „keine Fehlerzeile gefunden" klingt nach einer
Kleinigkeit und ist doch dasselbe. */
let ohneGrund = false;
if (!erste) {
ohneGrund = true;
const letzte = ganz.split("\n").map((z) => z.trim())
.filter(Boolean).slice(-3).join(" / ");
erste = letzte || "(die Prüfung hat gar nichts ausgegeben)";
}
/* DIE HAEUFIGSTE URSACHE HAT EINEN NAMEN -- und sie steht in der
Uebersichtszeile ganz oben, nicht im Fehlerabschnitt. Am
25.09.2026 waren acht der 37 roten Dateien von dieser Sorte:
fehlerfrei durchgelaufen, Exitcode 0, aber der Zaehler hat ihre
Punkte nicht erkannt (sie schreiben „OK" statt „ok"). „Null
Pruefungen ist ein Fehler" ist dann richtig UND irrefuehrend --
es klingt nach einer Pruefung, die nichts tut, und war eine,
die niemand zaehlt. */
const uebersicht = new RegExp(
`^\\s*FEHL\\s+${treffer[1].replace(/[.*+?^$()|[\]\\]/g, "\\$&")}\\s.*$`, "m");
const zeile = (aus.match(uebersicht) || [""])[0];
const nullPunkte = /KEINE PRUEFUNG GELAUFEN/.test(zeile);
rote.push({
datei: treffer[1], code: Number(treffer[2]),
erste: erste.slice(0, 220), ohneGrund, nullPunkte,
});
}
return {
dateienOk: dateien ? Number(dateien[1]) : null,
dateien: dateien ? Number(dateien[2]) : null,
punkteOk: punkte ? Number(punkte[1]) : null,
punkte: punkte ? Number(punkte[2]) : null,
rote,
};
}
/* ---------------------------------------------------------------------
Die Notiz
--------------------------------------------------------------------- */
function notizSchreiben({ lage, vorher, minuten, konnteNicht }) {
const jetzt = new Date();
const zeilen = [];
zeilen.push("# Prüfstand Team Dogi");
zeilen.push("");
zeilen.push("*Diese Notiz schreibt `tools/nachtlauf.mjs` — von Hand ändern bringt nichts,");
zeilen.push("beim nächsten Lauf ist es wieder überschrieben.*");
zeilen.push("");
if (konnteNicht) {
zeilen.push("## ⚠ Konnte nicht nachsehen");
zeilen.push("");
zeilen.push(konnteNicht);
zeilen.push("");
if (vorher?.wann) {
const tage = Math.floor((Date.now() - vorher.zeit) / 86400000);
zeilen.push(`Zuletzt wirklich gemessen: **${vorher.wann}**`
+ (tage >= 1 ? ` — das ist **${tage} Tag${tage === 1 ? "" : "e"} her**.` : "."));
} else {
zeilen.push("Es gibt noch kein früheres Ergebnis.");
}
return zeilen.join("\n") + "\n";
}
const rot = lage.rote.length;
zeilen.push(rot
? `## ${rot} Prüfung${rot === 1 ? "" : "en"} ${rot === 1 ? "ist" : "sind"} rot`
: "## Alles in Ordnung");
zeilen.push("");
zeilen.push(`Gemessen am **${zeitWort(jetzt)}**, Dauer ${minuten} Minuten.`);
zeilen.push("");
zeilen.push("| | jetzt | vorher |");
zeilen.push("|---|---|---|");
zeilen.push(`| Dateien in Ordnung | ${lage.dateienOk} von ${lage.dateien} | `
+ `${vorher ? `${vorher.dateienOk} von ${vorher.dateien}` : "—"} |`);
zeilen.push(`| Einzelprüfungen bestanden | ${lage.punkteOk} von ${lage.punkte} | `
+ `${vorher ? `${vorher.punkteOk} von ${vorher.punkte}` : "—"} |`);
zeilen.push("");
/* EIN RÜCKGANG DER ANZAHL IST EIN BEFUND, AUCH WENN ALLES GRÜN IST.
Am 28.08.2026 meldete ein Lauf 789 statt 804 Prüfungen und war
dabei grün: Acht Prüfungen je Gerät waren stillschweigend
weggefallen -- ausgerechnet die, die die wichtigste Funktion
absichern. Wer nur "bestanden" liest, hätte sie ab da dauerhaft
ungeprüft gelassen. */
if (vorher && lage.punkte != null && vorher.punkte != null
&& lage.punkte < vorher.punkte) {
zeilen.push(`> **Es werden ${vorher.punkte - lage.punkte} Prüfungen WENIGER gemacht als beim letzten Lauf.**`);
zeilen.push("> Das ist auch dann ein Befund, wenn alles grün ist: Eine Prüfung, die");
zeilen.push("> still nicht mehr läuft, beweist nichts — und niemandem fällt es auf.");
zeilen.push("");
}
if (vorher && lage.dateien != null && vorher.dateien != null
&& lage.dateien < vorher.dateien) {
zeilen.push(`> **Es gibt ${vorher.dateien - lage.dateien} Prüfdatei(en) weniger als beim letzten Lauf.**`);
zeilen.push("");
}
if (rot) {
zeilen.push("## Was rot ist");
zeilen.push("");
for (const r of lage.rote) {
zeilen.push(`### \`${r.datei}\``);
zeilen.push("");
if (r.nullPunkte) {
zeilen.push(`**Kein Befund – es wurde keine einzige Prüfung GEZÄHLT** `
+ `(Exitcode ${r.code}).` );
zeilen.push("");
zeilen.push("Das ist etwas anderes als „kaputt“: Die Datei ist "
+ "durchgelaufen. Der Zähler hat ihre Punkte nur nicht erkannt – "
+ "meist, weil sie ihre Ergebnisse anders schreibt als erwartet "
+ "(` OK ` statt ` ok `, andere Einrückung, eigenes Format). "
+ "Nachsehen lohnt trotzdem: Eine Datei, deren Punkte niemand "
+ "zählt, kann still aufhören zu prüfen, ohne dass es auffällt. "
+ "Zuletzt stand da:");
zeilen.push("");
} else if (r.ohneGrund) {
zeilen.push(`**Konnte nicht sagen, woran es lag** (Exitcode ${r.code}). `
+ "Das ist etwas anderes als ein Befund: Die Prüfung ist nicht "
+ "durchgelaufen, hat aber auch keine FEHL-Zeile hinterlassen. "
+ "Meist steckt dahinter eine Zeitüberschreitung oder ein Absturz "
+ "im Vorlauf, kein Fehler im Programm. Zuletzt stand da:");
zeilen.push("");
}
zeilen.push(`\`\`\`\n${r.erste}\n\`\`\``);
zeilen.push("");
zeilen.push(`Einzeln nachsehen: \`node server/${r.datei}\``);
zeilen.push("");
}
zeilen.push("**Rot heißt nicht automatisch kaputt.** Dreimal am 23.09.2026 war");
zeilen.push("die Prüfung veraltet und das Programm richtig — einmal war es");
zeilen.push("umgekehrt und ein Rollenname stand offen im Netz. Erst nachsehen,");
zeilen.push("dann entscheiden, wer recht hat.");
} else {
zeilen.push("Keine Datei ist rot. Der nächste Lauf kommt in der nächsten Nacht.");
}
zeilen.push("");
return zeilen.join("\n") + "\n";
}
/* ---------------------------------------------------------------------
Los
--------------------------------------------------------------------- */
const vorher = standLesen();
const erzwingen = process.argv.includes("--jetzt");
const schloss = erzwingen ? { ok: true } : schlossNehmen();
if (!schloss.ok) {
/* DER DRITTE AUSGANG. Kein Fehler -- nur: hier war nichts zu messen. */
mkdirSync(dirname(NOTIZ), { recursive: true });
writeFileSync(NOTIZ, notizSchreiben({ vorher, konnteNicht: schloss.grund }), "utf8");
console.log("Konnte nicht nachsehen: " + schloss.grund);
process.exit(0);
}
let ergebnis;
try {
console.log("Nachtlauf gestartet — das dauert. " + zeitWort());
ergebnis = await laufen();
} finally {
if (!erzwingen) schlossAbgeben();
}
if (!ergebnis.ging) {
mkdirSync(dirname(NOTIZ), { recursive: true });
writeFileSync(NOTIZ, notizSchreiben({
vorher, konnteNicht: `Der Läufer ließ sich nicht starten: ${ergebnis.grund}`,
}), "utf8");
console.log("Konnte nicht nachsehen: " + ergebnis.grund);
process.exit(0);
}
const lage = auswerten(ergebnis.aus);
/* KONNTE DER LAUF ÜBERHAUPT ETWAS MESSEN? Steht keine Zahl in der
Ausgabe, ist etwas anderes schiefgegangen als eine rote Prüfung --
und "0 von 0 in Ordnung" wäre die beruhigendste Lüge von allen. */
if (lage.dateien == null || lage.punkte == null || lage.punkte === 0) {
mkdirSync(dirname(NOTIZ), { recursive: true });
writeFileSync(NOTIZ, notizSchreiben({
vorher,
konnteNicht: "Der Lauf hat keine einzige Zahl geliefert — vermutlich ist er"
+ " abgebrochen, bevor er anfangen konnte.\n\nLetzte Ausgabe:\n\n```\n"
+ ergebnis.aus.slice(-700) + "\n```",
}), "utf8");
console.log("Konnte nicht nachsehen: keine Zahlen in der Ausgabe.");
process.exit(0);
}
mkdirSync(dirname(NOTIZ), { recursive: true });
writeFileSync(NOTIZ, notizSchreiben({ lage, vorher, minuten: ergebnis.minuten }), "utf8");
/* Der Stand für den nächsten Vergleich -- erst NACH der Notiz, damit
ein Absturz beim Schreiben nicht den alten Vergleichswert wegnimmt. */
writeFileSync(ZUSTAND, JSON.stringify({
wann: zeitWort(), zeit: Date.now(),
dateien: lage.dateien, dateienOk: lage.dateienOk,
punkte: lage.punkte, punkteOk: lage.punkteOk,
rote: lage.rote.map((r) => r.datei),
}, null, 1), "utf8");
/* Die ganze Ausgabe daneben -- für den Fall, dass die Notiz nicht
reicht. Sie wird jedes Mal überschrieben; zwei Läufe zu vergleichen
ist Sache der Zahlen oben, nicht eines wachsenden Protokolls. */
writeFileSync(join(WURZEL, "tools", ".nachtlauf-letzte-ausgabe.txt"), ergebnis.aus, "utf8");
console.log(`Fertig nach ${ergebnis.minuten} min: `
+ `${lage.dateienOk} von ${lage.dateien} Dateien, `
+ `${lage.punkteOk} von ${lage.punkte} Einzelprüfungen.`);
console.log(`Ergebnis steht in: ${NOTIZ}`);
process.exit(lage.rote.length ? 1 : 0);