Files
dogfather-universe/server/pruef-zeichen.mjs
T
DogFatherGitandClaude Opus 5 da7508b6ce Vier rote Pruefungen -- drei Phantome und ein echtes Gewicht
Weiter durch die rote Liste. Drei der vier Befunde zeigten auf
gesunden Code; die Pruefungen selbst waren seit einem Umbau nicht
nachgezogen worden.

pruef-schritt (2 rot -> 71 gruen statt 47)
  Meldete "1 von 6 Kategorien". Die Karte geht seit dem 01.10.2026
  gefiltert auf ("zugetragen"), Bloecke ohne solche Punkte werden gar
  nicht gezeichnet. Die Pruefung zaehlte danach alle. Schlimmer: Weil
  dann kein Block mehr zugeklappt war, lief der Klick auf einen
  zugeklappten 30 Sekunden in die Frist und riss den Lauf mit -- 24 von
  71 Pruefungen liefen nie. Jetzt wird erst die Vorgabe geprueft (sie
  ist eine Zusage), dann auf "Alle" gestellt.

pruef-glocke (1 rot -> 34 gruen)
  Meldete "auf der Anmeldeseite steht der Knopf". Gemessen landete sie
  auf start.html: Dieselbe Seite war eine Zeile zuvor angemeldet durch
  zwanzig Arbeitsseiten gelaufen, und angemeldet fuehrt /workspace/
  nicht zur Wand. Jetzt eigener Kontext ohne Keks -- und davor die
  Zeile, dass die Wand wirklich erreicht wurde.

pruef-zeichen (1 rot -> 8 gruen)
  Meldete content: "·" als Kodierungstruemmer. Die Bytes sind C2 B7,
  also korrektes UTF-8; der Mittelpunkt ist ein begruendeter Trennpunkt
  (seit 03.10.). Die Regel stammt aus einer Zeit, in der sie selbst
  festhielt: "KEINE einzige benutzt ein Zeichen aus diesem Block."
  Jetzt eine benannte Ausnahme MIT Veraltungsschutz: Jedes Zeichen
  darin muss wirklich vorkommen, sonst wird die Ausnahme rot.

pruef-tempo-workspace (1 rot -> 9 gruen) -- DER ECHTE BEFUND
  uebersicht.html 959 KB, bereich.html?b=live 1094 KB bei einer Grenze
  von 900. Die Pruefung sagt jetzt auch, WORAN es liegt, und die
  Antwort war eindeutig: ZWEI Buehnenbilder auf einer Seite, die eines
  zeigt (281 KB studio + 262 KB portal).

  Ursache: start.css traegt an body.start::before einen Rueckfall
  (studio), und kopf.js setzt `data-buehne` erst, wenn die Seite steht.
  Bis dahin ist der Rueckfall geholt -- auf 43 von 45 Seiten.

  Jetzt steht die Szene fest im <body>, erzeugt aus bereiche.js
  (tools/buehne-festschreiben.mjs, 20 Seiten). bereich.html bedient
  fuenf Bretter und kann das nicht; sie bekommt ein kleines Skript
  gleich hinter <body>, das die Szene aus `?b=` setzt, bevor gezeichnet
  wird. Gemessen: 1094 -> 879 KB, und die Grenze haelt wieder.

  DIE ZUORDNUNG STEHT DAMIT AN ZWEI STELLEN. Das ist nur vertretbar,
  weil sie nicht auseinanderlaufen kann: pruef-buehnen-fest.mjs (neu,
  13 Pruefungen) haelt HTML gegen bereiche.js, prueft die Lage des
  Skripts und dass jede genannte Szene eine Regel hat.

  Dabei gefunden und berichtigt -- zweimal an mir selbst: Der erste
  Entwurf verglich Szenen mit Dateinamen und meldete teamlage.html
  ("eingang") als Fehler; die Szene gibt es, ihre Regel zeigt nur auf
  ein crew-Bild. Und der Lage-Anker suchte `data-buehne`, das Skript
  schreibt `dataset.buehne` -- indexOf gab -1, die Rechnung wurde
  negativ und meldete "-2276 Zeichen dahinter".

Gegenproben gefahren: Szene verschoben -> rot mit Abstand; Ausnahme
verwaist -> rot; erfundene Szene -> erkannt.
Dazu gruen: css-klassen, workspace-seiten, neue-seiten, haus-trennung,
kopfleiste-farbe.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-10-07 13:05:54 +02:00

310 lines
13 KiB
JavaScript

/* =====================================================================
STEHT IN EINER AUSGELIEFERTEN DATEI EIN ZERSTOERTES ZEICHEN?
WARUM ES DIESE PRUEFUNG GIBT: Weil genau das zweimal live stand,
und zwar am selben Tag (23.09.2026), in zwei Dateien.
1. `content: "<0x15>C9<0x00>A0"` in chat.css. Gemeint war U+25C9
(dasselbe Zeichen, das chat.js fuer das Rudel benutzt) und ein
geschuetztes Leerzeichen. Im Quelltext standen stattdessen ZWEI
ROHE STEUERZEICHEN. Auf dem Bildschirm las man deshalb woertlich
"C9 A0" vor jedem hervorgehobenen @rudel -- die Steuerzeichen
selbst sind unsichtbar.
2. `content: "¹3"` in chat.css UND entwicklung.css. Gemeint
war ein Haken. Auf der gewaehlten Kachel stand "¹3".
DAS BESONDERE AN DIESER FEHLERART: Niemand sieht sie beim Lesen.
Punkt 1 war sogar committet, ausgeliefert und lag live -- und als
ich in der Datei danach suchte, meldete grep "Binary file matches"
und zeigte die Zeile gar nicht erst an. Ein Mensch, der den
Unterschied durchsieht, liest `content: "C9 A0"` und denkt sich
nichts dabei.
Gefunden hat es am Ende Filipes Bildschirmfoto, nicht der Quelltext.
Das ist zu spaet und zu zufaellig. Also eine Pruefung.
---------------------------------------------------------------------
WAS GEPRUEFT WIRD, UND WARUM DIE GRENZE DA LIEGT
AUSGELIEFERTE DATEIEN (workspace/, assets/, *.html) -- dort ist
beides ein Fehler: ein Steuerzeichen UND ein Ersatzzeichen (U+FFFD).
Was dort steht, sieht ein Mensch.
SERVERDATEIEN -- dort nur Steuerzeichen. Ein U+FFFD kann in einer
Pruefung absichtlich stehen: pruef-ics.mjs sucht genau danach in
Kalenderdateien. Eine Ausnahmeliste mit Dateinamen waere die
naheliegende Loesung und die schlechtere -- sie altert. Die Grenze
"wird es ausgeliefert?" altert nicht.
TABULATOR, ZEILENVORSCHUB UND WAGENRUECKLAUF sind erlaubt. Alles
andere unter 0x20 ist in Quelltext nie Absicht.
===================================================================== */
import { readdirSync, readFileSync, statSync, writeFileSync, unlinkSync } from "node:fs";
import { join, dirname, extname, relative } from "node:path";
import { fileURLToPath } from "node:url";
import { tmpdir } from "node:os";
const HIER = dirname(fileURLToPath(import.meta.url));
const WURZEL = join(HIER, "..");
let fehler = 0;
let geprueft = 0;
const ok = (b, t) => { geprueft++; console.log((b ? " ok " : " FEHL ") + t); if (!b) fehler++; };
/* NOTBREMSE. Auch eine Pruefung ohne Netzzugriff bekommt eine -- ein
Lauf, der haengt, ist schlimmer als einer, der scheitert (06.09.2026,
damals drei Stunden). */
const notbremse = setTimeout(() => {
console.log(" FEHL Notbremse: die Pruefung haengt.");
process.exit(1);
}, 60_000);
const ENDUNGEN = new Set([".js", ".mjs", ".html", ".css", ".json", ".svg",
".txt", ".webmanifest"]);
/* Ordner, die uns nicht gehoeren oder erzeugt sind. */
const AUSSEN = new Set([".git", "node_modules", "sicherungen", ".tmp"]);
/** Alle Dateien unterhalb eines Ordners, die uns angehen. */
function dateien(ordner) {
const raus = [];
const gehe = (o) => {
let eintraege;
try { eintraege = readdirSync(o, { withFileTypes: true }); } catch { return; }
for (const e of eintraege) {
if (e.name.startsWith(".tmp-")) continue;
const p = join(o, e.name);
if (e.isDirectory()) { if (!AUSSEN.has(e.name)) gehe(p); continue; }
if (ENDUNGEN.has(extname(e.name).toLowerCase())) raus.push(p);
}
};
gehe(ordner);
return raus;
}
/* ---------------------------------------------------------------------
Die beiden Sucher. Getrennt, damit die Gegenprobe jeden einzeln
nachweisen kann.
--------------------------------------------------------------------- */
/** Rohe Steuerzeichen -- alles unter 0x20 ausser \t \n \r. */
function steuerzeichenIn(roh) {
const treffer = [];
for (let i = 0; i < roh.length; i++) {
const b = roh[i];
if (b >= 0x20 || b === 0x09 || b === 0x0a || b === 0x0d) continue;
/* Die Zeile mitgeben -- eine Bytenummer allein sucht man lange. */
const zeile = roh.subarray(0, i).toString("utf8").split("\n").length;
treffer.push({ zeile, byte: b });
}
return treffer;
}
/** Ersatzzeichen U+FFFD -- in UTF-8 die drei Bytes ef bf bd. */
function ersatzzeichenIn(roh) {
const treffer = [];
let i = -1;
while ((i = roh.indexOf(Buffer.from([0xef, 0xbf, 0xbd]), i + 1)) !== -1) {
treffer.push({ zeile: roh.subarray(0, i).toString("utf8").split("\n").length });
}
return treffer;
}
/* =====================================================================
1. GEGENPROBE ZUERST
Eine Pruefung, die immer bestaetigt, bestaetigt nichts. Bevor sie
ueber echte Dateien redet, weist sie nach, dass sie ueberhaupt
anschlagen KANN -- an einer Datei, die sie sich selbst baut. Ohne
das waere ein gruener Lauf hier auch dann gruen, wenn beide Sucher
ins Leere greifen (der Fehler vom 01.09.2026, zweimal an einem Tag).
===================================================================== */
console.log("=== 0. Kann die Pruefung ueberhaupt Alarm schlagen? ===");
{
const probe = join(tmpdir(), "pruef-zeichen-probe.css");
try {
/* Genau die beiden Faelle, die live standen. */
writeFileSync(probe, Buffer.concat([
Buffer.from('a { content: "'),
Buffer.from([0x15]), Buffer.from("C9"),
Buffer.from([0x00]), Buffer.from('A0"; }\n'),
Buffer.from('b::after { content: "'), Buffer.from([0xef, 0xbf, 0xbd]), Buffer.from('"; }\n'),
]));
const roh = readFileSync(probe);
const st = steuerzeichenIn(roh);
const er = ersatzzeichenIn(roh);
ok(st.length === 2, `sie findet rohe Steuerzeichen (${st.length}, erwartet 2)`);
ok(er.length === 1, `sie findet Ersatzzeichen (${er.length}, erwartet 1)`);
/* Und die andere Richtung: saubere Datei, kein Alarm. Eine
Pruefung, die das richtige Verhalten bestraft, wird abgeschafft
(03.09.2026). */
writeFileSync(probe, 'a { content: "\\25C9\\A0"; }\n\tb { color: red; }\r\n');
const sauber = readFileSync(probe);
ok(steuerzeichenIn(sauber).length === 0 && ersatzzeichenIn(sauber).length === 0,
"und sie schweigt bei einer sauberen Datei -- auch mit Tabulator und CRLF");
} finally {
try { unlinkSync(probe); } catch { /* beim Aufraeumen ist Scheitern egal */ }
}
}
/* =====================================================================
2. DIE AUSGELIEFERTEN DATEIEN
===================================================================== */
console.log("");
console.log("=== 1. Was der Browser bekommt ===");
const geliefert = [
...dateien(join(WURZEL, "workspace")),
...dateien(join(WURZEL, "assets")),
...dateien(WURZEL).filter((p) => dirname(p) === WURZEL && extname(p) === ".html"),
];
{
const schlimm = [];
for (const p of geliefert) {
const roh = readFileSync(p);
for (const t of steuerzeichenIn(roh)) {
schlimm.push(`${relative(WURZEL, p)}:${t.zeile} (Byte 0x${t.byte.toString(16).padStart(2, "0")})`);
}
for (const t of ersatzzeichenIn(roh)) {
schlimm.push(`${relative(WURZEL, p)}:${t.zeile} (Ersatzzeichen)`);
}
}
/* DIE ANZAHL STEHT IN DER BEDINGUNG, nicht nur im Text. Waere der
Ordner leer oder der Pfad falsch, meldete "keine Fundstellen"
sonst Erfolg -- gruen, ohne eine einzige Datei angesehen zu
haben. */
ok(geliefert.length > 50 && schlimm.length === 0,
`${geliefert.length} ausgelieferte Dateien, ${schlimm.length} mit zerstoerten Zeichen`);
for (const s of schlimm.slice(0, 12)) console.log(" " + s);
if (schlimm.length > 12) console.log(` ... und ${schlimm.length - 12} weitere`);
}
/* =====================================================================
3. DIE SERVERDATEIEN -- nur Steuerzeichen
===================================================================== */
console.log("");
console.log("=== 2. Der Server ===");
{
const schlimm = [];
const serverDateien = [...dateien(join(WURZEL, "server")), ...dateien(join(WURZEL, "tools"))];
for (const p of serverDateien) {
const roh = readFileSync(p);
for (const t of steuerzeichenIn(roh)) {
schlimm.push(`${relative(WURZEL, p)}:${t.zeile} (Byte 0x${t.byte.toString(16).padStart(2, "0")})`);
}
}
ok(serverDateien.length > 50 && schlimm.length === 0,
`${serverDateien.length} Serverdateien, ${schlimm.length} mit Steuerzeichen`);
for (const s of schlimm.slice(0, 12)) console.log(" " + s);
}
/* =====================================================================
4. DIE STELLE, AN DER ES ZWEIMAL PASSIERT IST
`content:` in CSS ist der eine Ort, an dem ein Zeichen SICHTBAR
wird, ohne dass es im Text steht. Beide Fehler sassen dort.
---------------------------------------------------------------------
DIE REGEL WAR IM ERSTEN ANLAUF ZU WEIT -- und das ist hier notiert,
weil es der wichtigere Teil ist.
Zuerst stand hier "jedes Zeichen ueber 0x7f ist verdaechtig, schreib
es als Escape". Das meldete auf Anhieb ACHT Stellen: ✓ in vier
Dateien, ↯, ↻, „ und ⚠. Alle acht sind richtig, absichtlich und
seit Monaten in Ordnung. Eine Pruefung, die beim ersten Lauf acht
korrekte Stellen anmeckert, wird beim neunten Mal weggeklickt -- und
dann faengt sie auch den echten Fall nicht mehr (03.09.2026).
DIE ENGERE REGEL KOMMT AUS DEM BEFUND SELBST. Das kaputte Zeichen
war U+00B9 (hochgestellte Eins) -- ein Rest aus dem Latin-1-Block.
Genau dort, zwischen U+0080 und U+00BF, liegen die typischen
Truemmer einer verungluekten Kodierung: ¡ ¢ £ ¤ ¥ ¦ § ¨ © ª « ¬ ®
° ± ² ³ ´ µ ¶ · ¸ ¹ º » ¼ ½ ¾.
Nachgemessen ueber alle 203 content-Angaben im Haus: KEINE
einzige benutzt ein Zeichen aus diesem Block. Alle echten liegen
darueber (✓ = U+2713, ⚠ = U+26A0, „ = U+201E). Die Regel ist damit
heute gruen, haette den Fehler aber gefunden -- und sie ist nicht
geraten, sondern an den vorhandenen Daten geprueft.
===================================================================== */
console.log("");
console.log("=== 3. Zeichen, die CSS auf den Bildschirm schreibt ===");
/* ==== DIE AUSNAHME, DIE NICHT VERALTEN KANN (07.10.2026) ============
Der Absatz darueber sagt: „Nachgemessen ueber alle 203
content-Angaben im Haus: KEINE einzige benutzt ein Zeichen aus
diesem Block." Das stimmte am 03.09.2026 und stimmt seit dem
03.10.2026 nicht mehr: bereich.css setzt `content: "·"` als
Trennpunkt vor den Aufgabenzaehler -- absichtlich, begruendet, und
als korrektes UTF-8 gespeichert (Bytes C2 B7 nachgemessen).
DIE REGEL BLEIBT TROTZDEM. Sie hat den echten Fall gefunden (U+00B9,
hochgestellte Eins) und wuerde ihn wieder finden. Was nicht bleiben
darf, ist der Fehlalarm: Eine Pruefung, die bei jedem Lauf eine
richtige Stelle anmeckert, wird weggeklickt -- und nimmt den echten
Fund mit. Das steht vier Absaetze weiter oben in dieser Datei selbst.
DIE LISTE KANN NICHT ROTTEN, weil sie gegen die Wirklichkeit geprueft
wird: Jedes Zeichen darin MUSS irgendwo wirklich vorkommen. Faellt
der Trennpunkt eines Tages weg, wird die Ausnahme rot statt
stillschweigend weiterzugelten -- dieselbe Bauweise wie bei den
ausgenommenen Dateien in pruef-modi-wortleck. */
const MIT_ABSICHT = new Map([
[0x00b7, "Mittelpunkt als Trennzeichen vor dem Aufgabenzaehler (bereich.css)"],
]);
/** Truemmerverdacht: Latin-1-Block, wo echte Typografie nie hingehoert --
* bis auf die ausdruecklich benannten Ausnahmen. */
const istTruemmer = (c) => c >= 0x80 && c <= 0xbf && !MIT_ABSICHT.has(c);
{
const verdaechtig = [];
const gesehen = new Set();
let stellen = 0;
for (const p of geliefert.filter((x) => extname(x) === ".css")) {
const text = readFileSync(p, "utf8");
for (const m of text.matchAll(/content:\s*"([^"]*)"/g)) {
stellen++;
for (const z of m[1]) {
const c = z.codePointAt(0);
if (MIT_ABSICHT.has(c)) gesehen.add(c);
if (istTruemmer(c)) {
verdaechtig.push(`${relative(WURZEL, p)}: content "${m[1]}" enthaelt U+${c.toString(16).toUpperCase().padStart(4, "0")}`);
}
}
}
}
ok(stellen > 50 && verdaechtig.length === 0,
`${stellen} content-Angaben in CSS, ${verdaechtig.length} mit Kodierungstruemmern`
+ (verdaechtig.length ? ":\n " + verdaechtig.slice(0, 5).join("\n ") : ""));
/* Und die Gegenprobe zur Ausnahme: Was hier steht, muss es geben. */
const verwaist = [...MIT_ABSICHT.keys()].filter((c) => !gesehen.has(c));
ok(verwaist.length === 0,
`alle ${MIT_ABSICHT.size} benannten Ausnahmen kommen wirklich vor`
+ (verwaist.length
? ` -- verwaist: ${verwaist.map((c) => "U+" + c.toString(16).toUpperCase()).join(", ")}`
: ""));
for (const s of verdaechtig.slice(0, 12)) console.log(" " + s);
/* GEGENPROBE: Wuerde sie den echten Fall sehen? Nicht theoretisch --
an genau der Zeichenfolge, die in chat.css stand. */
const kaputt = [...'content: "¹3"'.matchAll(/content:\s*"([^"]*)"/g)]
.flatMap((m) => [...m[1]]).some((z) => istTruemmer(z.codePointAt(0)));
const heil = [...'content: "✓"'.matchAll(/content:\s*"([^"]*)"/g)]
.flatMap((m) => [...m[1]]).some((z) => istTruemmer(z.codePointAt(0)));
ok(kaputt && !heil,
'sie erkennt das echte "¹3" und laesst den richtigen Haken in Ruhe');
}
clearTimeout(notbremse);
console.log("");
console.log(fehler === 0
? `Alles in Ordnung — ${geprueft} Prüfungen`
: `${fehler} FEHLER — ${geprueft} Prüfungen`);
process.exit(fehler === 0 ? 0 : 1);