diff --git a/server/pruef-meldungen.mjs b/server/pruef-meldungen.mjs index 763bd37b..4556740c 100644 --- a/server/pruef-meldungen.mjs +++ b/server/pruef-meldungen.mjs @@ -55,6 +55,43 @@ 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}`); } @@ -116,10 +153,7 @@ function kennungenAusServer() { Zeichenketten mit // darin (Adressen) bleiben unberuehrt: geschnitten wird nur, wo kein Doppelpunkt und kein Anfuehrungszeichen davorsteht. */ - const ohneKommentare = (roh) => roh - .replace(/\/\*[\s\S]*?\*\//g, " ") - .replace(/(^|[^:"'\\])\/\/[^\n]*/gm, "$1"); - const text = ohneKommentare(readFileSync(join(HIER, datei), "utf8")); + 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); } @@ -145,11 +179,57 @@ if (!SAETZE) { 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) => `${k} (${kennungen.get(k)})`).join(", ")}` : ""); + 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 @@ -158,7 +238,6 @@ pruef("Es wurden überhaupt Kennungen gefunden", kennungen.size >= 30, `nur ${kennungen.size} — findet der Suchausdruck noch, was er soll?`); /* ---- 2. Angeschlossen ----------------------------------------------- */ -const skripte = readdirSync(JS).filter((f) => f.endsWith(".js")); /* 2a: Keine Rohanzeige mehr. Gesucht wird `.fehler` in derselben Zeile wie eine Ausgabe — aber nur dort, wo KEIN `sagWas` und kein @@ -195,6 +274,137 @@ 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)) { diff --git a/workspace/anleitung.html b/workspace/anleitung.html index 31f6dfae..7c368954 100644 --- a/workspace/anleitung.html +++ b/workspace/anleitung.html @@ -7,17 +7,17 @@ - - - - - + + + + + - - + +
@@ -289,16 +289,16 @@ Und anleitung.js braucht es doppelt: Symbol, Farbe, Name und Adresse jeder Karte kommen von dort, nicht vom Server. --> - + - - - - - - + + + + + +