/* ===================================================================== pruef-meldungen.mjs — Kein Benutzer liest je eine Maschinenkennung. Filipe, 19.09.2026: „keine kryptischen oder technischen Fehlermeldungen, sondern klare Aussagen darüber, was passiert ist und wie man das Problem lösen kann." --------------------------------------------------------------------- WARUM DIESE PRÜFUNG DIE EIGENTLICHE ARBEIT IST Die Übersetzungstabelle in `workspace/assets/js/meldung.js` zu schreiben war eine Stunde Arbeit. Sie in einem halben Jahr noch vollständig zu haben, ist die schwierigere Aufgabe — und zwar aus einem Grund, den dieses Haus teuer gelernt hat: „Eine abgeschriebene Liste ist dieselbe Falle wie eine feste Zahl." (11.09.2026, als die Umstellung drei Spalten verlor, obwohl ein Kommentar direkt daneben davor warnte.) Wer morgen `fehler: "zu_frueh"` erfindet, wird nicht daran denken, hier einen Satz nachzutragen. Er merkt es auch nicht: Die Oberfläche zeigt dann klaglos den Ersatzsatz. Es sieht aus, als funktioniere es. Deshalb rechnet diese Prüfung die Liste der Kennungen AUS DEM SERVER aus, statt sie zu kennen. Eine neue Kennung ohne Satz macht sie rot. Man KANN es nicht mehr vergessen. --------------------------------------------------------------------- DREI DINGE WERDEN GEPRÜFT 1. VOLLSTÄNDIG — jede Kennung, die der Server schicken kann, hat einen Satz. 2. ANGESCHLOSSEN — keine Seite zeigt `.fehler` noch roh an, und jede Seite, deren Skript `sagWas` benutzt, lädt auch `meldung.js`. (Der zweite Teil ist der wichtigere: Ohne die Datei wäre `sagWas` nicht definiert, und aus einer Fehlermeldung würde ein Absturz — schlimmer als vorher.) 3. LESBAR — kein Satz enthält eine Kennung, eine nackte Zahl oder ein englisches Wort. Dazu drei GEGENPROBEN. Eine Prüfung, die immer bestätigt, bestätigt nichts (01.09.2026): Jede der drei muss beweisen, dass sie auch „nicht in Ordnung" sagen kann. AUFRUF: node server/pruef-meldungen.mjs Läuft in unter einer Sekunde, ohne Server, ohne Browser. ===================================================================== */ import { readFileSync, readdirSync, existsSync } 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, ".."); const JS = join(WURZEL, "workspace", "assets", "js"); const SEITEN = join(WURZEL, "workspace"); /* --------------------------------------------------------------------- Quelltext ohne Kommentare — an EINER Stelle. WARUM DAS SEIN MUSS: Eine Suche über eine Datei findet Wörter auch dort, wo sie nur ERWÄHNT werden. In diesem Haus stehen in vielen Dateien mehr Kommentarzeilen als Anweisungen — am 24.09.2026 meldete diese Prüfung einen längst behobenen Fehler weiter, weil der Kommentar daneben die alte Zeile zitierte. Zeichenketten mit // darin (Adressen) bleiben unberührt: geschnitten wird nur, wo kein Doppelpunkt und kein Anführungszeichen davorsteht. ------------------------------------------------------------------- */ const ohneKommentareJs = (roh) => roh .replace(/\/\*[\s\S]*?\*\//g, " ") .replace(/(^|[^:"'\\])\/\/[^\n]*/gm, "$1"); /* GEGENPROBE AN DER ENTFERNUNG SELBST. Räumt sie zu viel weg, ist jede Suche darüber grün — über einem leeren Text findet niemand etwas. Zwei Proben: ein Kommentar muss verschwinden, Quelltext und eine Adresse müssen stehen bleiben. */ { const probe = ohneKommentareJs( 'const a = 1; /* fehler: "weg_damit" */\n' + 'const b = "https://x.test/y"; // fehler: "auch_weg"\n' + 'res.json({ fehler: "bleibt_da" });'); const gut = !probe.includes("weg_damit") && !probe.includes("auch_weg") && probe.includes("bleibt_da") && probe.includes("https://x.test/y"); if (!gut) { console.log(" KONNTE NICHT NACHSEHEN — die Kommentar-Entfernung" + " arbeitet nicht wie erwartet. Ohne sie wäre jede Suche" + " darüber wertlos. Abbruch."); console.log(" Herausgekommen ist:\n" + probe + "\n"); process.exit(2); } } let ok = 0, fehl = 0; const pruef = (name, bedingung, zusatz = "") => { if (bedingung) { ok++; console.log(` OK ${name}`); } else { fehl++; console.log(` FEHL ${name}${zusatz ? " — " + zusatz : ""}`); } }; /* --------------------------------------------------------------------- Die Tabelle aus meldung.js lesen — ohne einen Browser zu bauen. `meldung.js` hängt sich an `window`. Statt eine halbe Browserwelt nachzustellen, wird der Block zwischen `var SAETZE = {` und der schließenden Klammer ausgeschnitten und als JavaScript ausgewertet. Das ist ehrlicher als ein zweiter Auszug der Namen: Es ist dieselbe Quelle, kein abgeschriebener Zwilling. ------------------------------------------------------------------- */ function saetzeLesen() { const quelle = readFileSync(join(JS, "meldung.js"), "utf8"); const start = quelle.indexOf("var SAETZE = {"); if (start < 0) return null; const auf = quelle.indexOf("{", start); let tiefe = 0, ende = -1; for (let i = auf; i < quelle.length; i++) { if (quelle[i] === "{") tiefe++; else if (quelle[i] === "}") { tiefe--; if (tiefe === 0) { ende = i; break; } } } if (ende < 0) return null; // eslint-disable-next-line no-new-func return Function(`"use strict"; return (${quelle.slice(auf, ende + 1)});`)(); } /* --------------------------------------------------------------------- Alle Kennungen, die der Server schicken kann. ABGELEITET, NICHT ABGESCHRIEBEN. Gesucht wird `fehler: "…"` in allen Server-Dateien; als Kennung gilt, was nur aus Kleinbuchstaben, Ziffern und Unterstrichen besteht. Alles andere ist ein fertiger Satz und braucht keine Übersetzung. ------------------------------------------------------------------- */ function kennungenAusServer() { const gefunden = new Map(); for (const datei of readdirSync(HIER)) { if (!datei.endsWith(".js")) continue; if (datei.startsWith("pruef-") || datei.startsWith("test-")) continue; /* KOMMENTARE ZAEHLEN NICHT ALS KENNUNG (24.09.2026). Gefunden an einer Stelle, die genau das dokumentierte, was diese Pruefung verlangt: In workspace-support.js stand fehler: "ungueltiger_stand" -- behoben, und der Kommentar daneben zitierte die alte Zeile als Begruendung. Die Pruefung las das Zitat und meldete den Fehler weiter, obwohl er behoben war. Dieselbe Falle wie ein Grep ueber eine Datei, die ihre eigene Geschichte enthaelt -- mir ist sie heute schon einmal passiert. Eine Pruefung, die verbietet, ueber einen behobenen Fehler zu SCHREIBEN, erzieht dazu, die Begruendung wegzulassen. Zeichenketten mit // darin (Adressen) bleiben unberuehrt: geschnitten wird nur, wo kein Doppelpunkt und kein Anfuehrungszeichen davorsteht. */ const text = ohneKommentareJs(readFileSync(join(HIER, datei), "utf8")); for (const m of text.matchAll(/fehler:\s*"([a-z0-9_]+)"/g)) { if (!gefunden.has(m[1])) gefunden.set(m[1], datei); } } return gefunden; } console.log("\n=== Meldungen: sagt das Haus ganze Sätze? ===\n"); const SAETZE = saetzeLesen(); /* DER DRITTE AUSGANG. Lässt sich die Tabelle nicht lesen, ist das weder „in Ordnung" noch „ein Satz fehlt" — es heißt, die Prüfung kann ihre Frage nicht beantworten. Dann bricht sie ab, statt grün zu melden. Genau dieser Fall hat am 03.09. wochenlang eine Pflichtprüfung wertlos gemacht. */ if (!SAETZE) { console.log(" KONNTE NICHT NACHSEHEN — in meldung.js ist kein Block `var SAETZE = {` zu finden."); console.log(" Grund: entweder wurde die Datei umgebaut oder sie fehlt."); console.log(" Ohne sie lässt sich nicht sagen, ob Sätze fehlen. Abbruch.\n"); process.exit(2); } const kennungen = kennungenAusServer(); console.log(` (${kennungen.size} Kennungen im Server, ${Object.keys(SAETZE).length} Sätze in der Tabelle)\n`); /* Die Browserdateien. SIE MUSS HIER STEHEN, nicht erst in Abschnitt 2: Prüfung 1 braucht sie über `woSonst`, und eine `const` weiter unten ist zu diesem Zeitpunkt noch nicht zugänglich. Gefunden hat das die Gegenprobe, nicht der grüne Lauf — im grünen Zustand wird `woSonst` nie gerufen, und der Absturz wäre genau dann gekommen, wenn ein Satz fehlt, also wenn man die Meldung am nötigsten braucht. */ const skripte = readdirSync(JS).filter((f) => f.endsWith(".js")); /* ---- 1. Vollständig ------------------------------------------------- */ const ohneSatz = [...kennungen.keys()].filter((k) => !SAETZE[k]); /* --------------------------------------------------------------------- WO STEHT DER SATZ SONST? (08.10.2026) Ohne diese Zeilen meldete die Prüfung „27 Kennungen ohne Satz" — und schickte in die falsche Richtung: Neun hatten sehr wohl einen Satz, nur in einer privaten Tabelle in `manager-ziele.js`. Wer das liest, schreibt 27 neue Sätze und hat danach DREI Tabellen. Ein Befund, der die halbe Wahrheit sagt, ist teurer als einer, der nichts sagt. ------------------------------------------------------------------- */ const NAMENSZEICHEN = "abcdefghijklmnopqrstuvwxyz0123456789_$"; const woSonst = (k) => { for (const f of skripte) { if (f === "meldung.js") continue; const text = ohneKommentareJs(readFileSync(join(JS, f), "utf8")); const flach = text.split(" ").join("").split("\t").join(""); for (const auf of ["'", '"', "`"]) { const nadel = k + ":" + auf; let i = -1; while ((i = flach.indexOf(nadel, i + 1)) !== -1) { /* ECHTE WORTGRENZE. Ohne sie trifft `unbekannt` mitten in `buddy_unbekannt` — beim Messen am 08.10.2026 genau so passiert, und der Befund sah richtig aus. */ const davor = i === 0 ? "" : flach[i - 1].toLowerCase(); if (!NAMENSZEICHEN.includes(davor)) return f; } } } return null; }; pruef("Jede Server-Kennung hat einen Satz", kennungen.size > 0 && ohneSatz.length === 0, ohneSatz.length ? `ohne Satz: ${ohneSatz.map((k) => { const sonst = woSonst(k); return `${k} (${kennungen.get(k)}` + (sonst ? ` — ein Satz steht in ${sonst}, er gehört nach meldung.js` : "") + ")"; }).join(", ")}` : ""); /* Die Zahl gehört in die BEDINGUNG, nicht nur in den Meldetext. `every()` auf einem leeren Feld liefert `true` — eine Prüfung, die nichts gefunden hat, wäre sonst grün (01.09.2026, Shop). */ pruef("Es wurden überhaupt Kennungen gefunden", kennungen.size >= 30, `nur ${kennungen.size} — findet der Suchausdruck noch, was er soll?`); /* ---- 2. Angeschlossen ----------------------------------------------- */ /* 2a: Keine Rohanzeige mehr. Gesucht wird `.fehler` in derselben Zeile wie eine Ausgabe — aber nur dort, wo KEIN `sagWas` und kein Vergleich (`===`) steht. Ein Vergleich übersetzt selbst. */ const roh = []; for (const f of skripte) { if (f === "meldung.js") continue; const zeilen = readFileSync(join(JS, f), "utf8").split("\n"); zeilen.forEach((z, i) => { if (!/\.fehler/.test(z)) return; if (!/(textContent\s*=|melde[n]?\(|alert\(|innerHTML\s*=)/.test(z)) return; if (/sagWas/.test(z) || /\.fehler\s*===/.test(z) || /\[\s*\w+\.fehler\s*\]/.test(z)) return; roh.push(`${f}:${i + 1}`); }); } pruef("Keine Seite zeigt eine Kennung roh an", roh.length === 0, roh.length ? roh.join(", ") : ""); /* 2b: Wer sagWas benutzt, muss meldung.js laden. DAS IST DIE WICHTIGERE HÄLFTE: Fehlt die Datei, ist `sagWas` nicht definiert, und aus einer Fehlermeldung wird ein Absturz. */ const benutzt = new Set(skripte.filter( (f) => f !== "meldung.js" && /\bsagWas\(/.test(readFileSync(join(JS, f), "utf8")))); const luecken = []; for (const seite of readdirSync(SEITEN).filter((f) => f.endsWith(".html"))) { const html = readFileSync(join(SEITEN, seite), "utf8"); const geladen = [...html.matchAll(/src="assets\/js\/([a-z0-9-]+)\.js/g)].map((m) => m[1] + ".js"); const braucht = geladen.filter((g) => benutzt.has(g)); if (braucht.length && !geladen.includes("meldung.js")) { luecken.push(`${seite} (braucht es wegen ${braucht.join(", ")})`); } } pruef("Jede Seite, die sagWas braucht, lädt meldung.js", benutzt.size > 0 && luecken.length === 0, luecken.length ? luecken.join("; ") : (benutzt.size ? "" : "keine Datei benutzt sagWas — stimmt das?")); /* --------------------------------------------------------------------- Jedes Objektliteral einer Datei — über Klammerzählung. NICHT ÜBERSPRINGEN, wenn eines gefunden ist: Der erste Treffer in einer Browserdatei ist der ganze Rumpf der Funktion, in der alles steckt. Wer danach weiterspringt, sieht nie eine Tabelle IM Rumpf. Gemessen am 08.10.2026: mit Übersprung 0 Treffer, obwohl zwei Tabellen dastanden. Der Rumpf selbst fällt unten durch, weil er Klammern enthält. ------------------------------------------------------------------- */ function literale(text) { const raus = []; for (let i = 0; i < text.length; i++) { if (text[i] !== "{") continue; let tiefe = 0, ende = -1; for (let j = i; j < text.length; j++) { if (text[j] === "{") tiefe++; else if (text[j] === "}") { tiefe--; if (tiefe === 0) { ende = j; break; } } } if (ende >= 0) raus.push(text.slice(i, ende + 1)); } return raus; } /* --------------------------------------------------------------------- Ist dieses Literal eine reine Zuordnung Kennung → Satz? WARUM DIE REGEL SO ENG IST. Ein weicherer Sucher („irgendwo steht ein Name mit einem Satz dahinter") meldete am 08.10.2026 vier Treffer, von denen zwei Fehlalarme waren: `leer:` in den Spaltenlisten von `aufgaben.js` und `chat.js` — Beschriftungen einer leeren Spalte, zufällig mit einem Namen, den es auch als Serverkennung gibt. Ein dritter Treffer, `reaktion.js`, war ebenfalls keiner: Dort steht kein Satz zu einer Absage, sondern ein Platzhaltertext für das Eingabefeld, und sein Schlüssel kommt nicht aus `fehler:`, sondern aus `schreib_grund`. Verlangt wird deshalb: jeder Wert ist eine Zeichenkette, im Literal steht nichts anderes, und JEDER Schlüssel ist eine Serverkennung. Damit fällt eine Satzliste auf und ein Datensatz nicht. Nachgemessen über alle Browserdateien: genau ein Treffer. ------------------------------------------------------------------- */ function nurSaetze(lit) { const inhalt = lit.slice(1, -1); if (/[{}()]/.test(inhalt)) return null; /* Alle drei Anführungsarten — sonst ist die Prüfung blind, sobald jemand doppelte statt einfacher benutzt. */ const found = [...inhalt.matchAll( /([a-z][a-z0-9_]*)\s*:\s*(?:'([^']*)'|"([^"]*)"|`([^`]*)`)/g)] .map((m) => [m[1], m[2] ?? m[3] ?? m[4]]); if (!found.length) return null; /* Die Zahl der Schlüsselzeilen muss zur Zahl der Paare passen — sonst steht im Literal noch etwas, das kein Satz ist. */ const zeilen = (inhalt.match(/^\s*[a-z][a-z0-9_]*\s*:/gm) || []).length; if (zeilen !== found.length) return null; return new Map(found); } /** Wo außerhalb von meldung.js steht eine Satztabelle für Kennungen? */ function zweittabellen() { const raus = []; for (const f of skripte) { if (f === "meldung.js") continue; const text = ohneKommentareJs(readFileSync(join(JS, f), "utf8")); for (const lit of literale(text)) { const m = nurSaetze(lit); if (!m || m.size < 2) continue; if (![...m.keys()].every((k) => kennungen.has(k))) continue; raus.push({ datei: f, keys: [...m.keys()] }); } } return raus; } /* ---- 4. EINE Tabelle, nicht zwei ------------------------------------ DER FALL, DER DAZU GEFÜHRT HAT (08.10.2026). `manager-ziele.js` führte eine eigene Tabelle mit zwölf Sätzen. Darüber stand als Begründung wörtlich: „An EINER Stelle: Stünde jeder dort, wo er gebraucht wird, hieße dieselbe Absage an zwei Stellen verschieden." Der Satz war richtig — nur war diese Tabelle selbst inzwischen die zweite Stelle. GEMESSEN, was das gekostet hat: Neun Kennungen hatten ihren Satz NUR dort; auf jeder anderen Seite, die dieselbe Absage bekam, erschien der Ersatzsatz. Drei standen in BEIDEN Tabellen mit verschiedenem Wortlaut. Und zwei Sätze der Haustabelle waren für eine fremde Seite geschrieben („Karte", „Stück") — sie wären beim Auflösen auf einer Seite erschienen, auf der es weder Karte noch Stück gibt. Die Pflicht ist deshalb doppelt: EINE Tabelle, und jeder Satz darin muss auf jeder Seite wahr sein, die ihn auslösen kann. AUSNAHMEN WERDEN BENANNT, nicht stillschweigend zugelassen — und die Gegenwache darunter prüft, dass jede Ausnahme noch existiert. Eine Ausnahmeliste, die niemand pflegt, ist dieselbe Falle wie eine abgeschriebene Liste (11.09.2026). ------------------------------------------------------------------- */ const ERLAUBT = new Map([ ["entwicklung.js", { keys: ["kein_hakt", "schon_da"], grund: "zwei Absagen am Schritt-Knopf, absichtlich knapper und auf " + "die Handlung bezogen; die Haustabelle sagt dasselbe allgemein", }], ]); const zweite = zweittabellen(); const unerlaubt = zweite.filter((z) => { const e = ERLAUBT.get(z.datei); return !e || z.keys.some((k) => !e.keys.includes(k)); }); pruef("Nur die Haustabelle übersetzt Server-Kennungen", unerlaubt.length === 0, unerlaubt.length ? unerlaubt.map((z) => `${z.datei} führt eigene Sätze für ` + `${z.keys.join(", ")} — sie gehören nach meldung.js`).join("; ") : ""); /* Gegenwache zur Ausnahmeliste: Jede benannte Ausnahme muss es noch geben. Sonst bleibt ein Freibrief stehen, nachdem die Stelle längst aufgeräumt ist — und deckt beim nächsten Mal etwas ab, das niemand entschieden hat. */ const toteAusnahmen = [...ERLAUBT.keys()].filter( (f) => !zweite.some((z) => z.datei === f)); pruef("Jede benannte Ausnahme gibt es wirklich noch", ERLAUBT.size > 0 && toteAusnahmen.length === 0, toteAusnahmen.length ? `nicht mehr vorhanden: ${toteAusnahmen.join(", ")}` : ""); for (const [f, e] of ERLAUBT) { /* Eine Ausnahme, die niemand liest, wird zur Gewohnheit. */ console.log(` (erlaubt: ${f} für ${e.keys.join(", ")} — ${e.grund})`); } /* ---- 3. Lesbar ------------------------------------------------------ */ const schlecht = []; for (const [k, satz] of Object.entries(SAETZE)) { if (/[a-z]+_[a-z]+/.test(satz)) schlecht.push(`${k}: enthält eine Kennung`); else if (/\b(?:400|401|403|404|409|429|500|503)\b/.test(satz)) schlecht.push(`${k}: enthält eine Fehlernummer`); else if (/\b(error|failed|invalid|denied|request|server error)\b/i.test(satz)) schlecht.push(`${k}: englisches Wort`); else if (satz.length < 15) schlecht.push(`${k}: zu kurz ("${satz}")`); } pruef("Jeder Satz ist deutsch, ohne Kennung und ohne Zahl", Object.keys(SAETZE).length > 0 && schlecht.length === 0, schlecht.length ? schlecht.join("; ") : ""); /* ===================================================================== DIE GEGENPROBEN Zu jeder Prüfung, die „alles in Ordnung" sagen kann, gehört der Beweis, dass sie auch „nicht in Ordnung" sagen kann. Sonst weiß niemand, ob sie überhaupt hinsieht. ===================================================================== */ console.log("\n --- Gegenproben (jede MUSS anschlagen) ---"); const g1 = (() => { const t = { ...SAETZE }; delete t.nicht_verfuegbar; return [...kennungen.keys()].filter((k) => !t[k]).length > 0; })(); pruef("Gegenprobe: fehlt ein Satz, schlägt Prüfung 1 an", g1); const g2 = (() => { const z = " $('x').textContent = d.fehler || 'Ging nicht.';"; return /\.fehler/.test(z) && /(textContent\s*=|melde[n]?\(|alert\(|innerHTML\s*=)/.test(z) && !/sagWas/.test(z) && !/\.fehler\s*===/.test(z); })(); pruef("Gegenprobe: eine Rohanzeige würde Prüfung 2a finden", g2); const g3 = (() => { const s = "Fehler 503: request denied"; return /[a-z]+_[a-z]+/.test(s) || /\b(?:400|401|403|404|409|429|500|503)\b/.test(s) || /\b(error|failed|invalid|denied|request)\b/i.test(s); })(); pruef("Gegenprobe: ein unlesbarer Satz würde Prüfung 3 finden", g3); console.log(`\n ${ok} bestanden, ${fehl} fehlgeschlagen\n`); process.exit(fehl ? 1 : 0);