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]>
310 lines
13 KiB
JavaScript
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);
|