Files
dogfather-universe/server/pruef-kampagne-bild.mjs
T
DogFatherGitandClaude Opus 5 354dfd9be9 Bildleser: den Titel gegenlesen -- und ein Protokollwort, das kein Satz war
ZWEI KLEINE SACHEN, BEIDE DERSELBEN ART: Etwas steht still da und
sieht fast richtig aus.

1. DER TITEL WIRD GEGENGELESEN

Anlass war ein echter Fall aus dem Betrieb: Filipe hat den Bildweg
benutzt, und aus "Refined visuals Goal 1.0" wurde "Refined visvals
Goal 1.0" -- ein u als v. Genau die Verwechslung, die man beim
Korrekturlesen ueberliest, weil das Wort noch fast richtig aussieht.

NACHGEMESSEN, OB DIE SPRACHWAHL HILFT: auf dem Server gegen die
gespeicherte Pruefdatei mit deu, eng, deu+eng und eng+deu -- alle vier
lesen "visuals" RICHTIG. Der Fehler liess sich also nicht nachstellen,
und eine Einstellung zu aendern, deren Wirkung man nicht messen kann,
ist Raten, das danach wie eine Loesung aussieht. Es bleibt bei `deu`.

Was stattdessen hilft: Der Titel steht auf der Seite ZWEIMAL -- oben
als Ueberschrift (gross, fett) und im Raster hinter "Gruppenname"
(klein, normal). Zwei Schriftgroessen heisst zwei Gelegenheiten, sich
zu verlesen, und zwar unabhaengig voneinander. Gehen die Lesungen
auseinander, sagt die Kachel es, und beide stehen im Auszug.

EIN HINWEIS, KEINE ABLEHNUNG -- anders als beim Zeitraum. Ein falsches
Datum ist eine Falschauskunft: Die Kachel behauptet einen Termin, den
sie nicht kennt, und niemand sieht es ihr an. Ein verlesener Buchstabe
im Titel ist ein Schoenheitsfehler; daran eine ganze Kachel scheitern
zu lassen waere unverhaeltnismaessig.

2. "Kampagne bremse" IST KEIN SATZ

Die Protokollseite baut ihren Anzeigetext aus dem Schluessel:
`aktionLesbar()` in personen.js macht aus Unterstrichen Leerzeichen
und schreibt nur den ersten Buchstaben gross. Aus `kampagne_bremse`
wurde damit "Kampagne bremse" -- Maschinensprache mit einem grossen
Anfangsbuchstaben.

Gemessen an allen sechs Woertern dieses Hauses war es das einzige
kaputte: "Kampagne eingelesen", "Kampagne gescheitert", "Event
geteilt" und sogar "Event zurueckgezogen" (die Umlautliste macht
daraus "zurueckgezogen" mit u-Umlaut) lesen sich als Satz. Die Regel
dahinter: Hauptwort plus Mittelwort, nie zwei Hauptwoerter.

UND DAS UMBENENNEN WAR UMSONST ZU HABEN -- vorher gezaehlt, weil es
bei Altbestand eine andere Rechnung waere: In der Live-Datenbank steht
KEINE Zeile mit dem alten Wort (die Bremse hat nie gegriffen; zum
Vergleich event_geteilt 7, kampagne_eingelesen 3). Es gibt also
nichts, was danach krumm angezeigt wuerde.

GEPRUEFT: 48 (Bildleser, +5 mit Gegenprobe, dass der Hinweis NICHT
immer kommt), 190 (Route, unveraendert). Keine ausgelieferte Datei
beruehrt, also kein neuer Stempel noetig.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-10-08 12:17:57 +02:00

468 lines
22 KiB
JavaScript

/* =====================================================================
EINE KAMPAGNE AUS EINEM BILDSCHIRMFOTO — GEPRUEFT (08.10.2026)
Filipe: „bei den agentur events soll ich kein link sondern immer so
ein screen reinschicke und dan soll daraus alles genommen werden und
daraus die kampagne kachel gemacht werden."
ZWEI DINGE, ZWEI MESSUNGEN, UND SIE HAENGEN NICHT ANEINANDER:
1. DAS AUSLESEN (Abschnitte 1-4). Text hinein, Daten heraus. Laeuft
ueberall, auch ohne Tesseract -- deshalb ist der Leser ueberhaupt
von der Texterkennung getrennt. Geprueft wird gegen einen
GESPEICHERTEN Erkennungstext, dessen Pruefsumme hier steht.
2. DIE TEXTERKENNUNG (Abschnitt 5). Bild hinein, Text heraus. Dafuer
braucht es Tesseract. Ist es nicht da, ist das Ergebnis NICHT
„bestanden" und auch nicht „uebersprungen", sondern der dritte
Ausgang: „konnte nicht nachsehen", mit Grund und Anleitung
(CLAUDE.md, 03.09.2026).
WARUM DER ERKENNUNGSTEXT GESPEICHERT IST und nicht bei jedem Lauf
neu erzeugt wird: Sonst misst diese Pruefung die Tagesform von
Tesseract statt unseren Code. Faellt die Texterkennung einmal anders
aus, soll Abschnitt 5 rot werden -- und die Abschnitte 1-4 sollen
trotzdem weiter sagen, ob das Auslesen stimmt.
===================================================================== */
import { readFileSync, existsSync } from "node:fs";
import { createHash } from "node:crypto";
import { execFileSync } from "node:child_process";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
import { notbremse } from "./helfer-notbremse.mjs";
notbremse(90_000, "pruef-kampagne-bild");
const HIER = dirname(fileURLToPath(import.meta.url));
const DATEN = join(HIER, "pruefdaten");
let fehler = 0;
let geprueft = 0;
const melde = (t) => console.log(t);
const ok = (b, t) => {
geprueft++;
console.log((b ? " ok " : " FEHL ") + t);
if (!b) fehler++;
};
/* Der dritte Ausgang: nicht in Ordnung, aber auch nicht „bestanden". */
let offen = 0;
const nichtNachsehbar = (was, wie) => {
offen++;
melde(` -- NICHT NACHSEHBAR: ${was}`);
melde(` ${wie}`);
};
/* ---- Die gespeicherten Daten ---------------------------------- */
const DATEIEN = {
text: {
name: "bild-agentur-erkannt.txt",
sha: "49de95db94482ebd", gross: 768,
was: "was Tesseract am 08.10.2026 aus dem Nachbau gelesen hat",
},
bild: {
name: "bild-agentur-nachbau.png",
was: "der Nachbau von Filipes Bildschirmfoto, als Bild",
},
seite: {
name: "bild-agentur-nachbau.html",
was: "derselbe Nachbau als Seite -- daraus entsteht das Bild neu",
},
};
melde("=== 0. Sind die gespeicherten Daten da? ===");
for (const [, d] of Object.entries(DATEIEN)) {
const pfad = join(DATEN, d.name);
if (!existsSync(pfad)) {
melde(` FEHL ${d.name} fehlt in ${DATEN}`);
melde(" Sie gehoert ins Verzeichnis und ist in Gitea versioniert.");
process.exit(3);
}
const roh = readFileSync(pfad);
const sha = createHash("sha256").update(roh).digest("hex").slice(0, 16);
if (d.sha) {
ok(sha === d.sha && roh.length === d.gross,
`${d.name}: ${roh.length} Bytes, sha ${sha} — ${d.was}`
+ (sha === d.sha ? "" : ` (ERWARTET ${d.sha}/${d.gross})`));
} else {
ok(roh.length > 100, `${d.name}: ${roh.length} Bytes — ${d.was}`);
}
}
const { leseBild, BildUnlesbar } = await import("./kampagne-bild-lesen.mjs");
const ERKANNT = readFileSync(join(DATEN, DATEIEN.text.name), "utf8");
/* =====================================================================
1. WAS AUF DER KACHEL LANDET
===================================================================== */
melde("");
melde("=== 1. Aus dem Erkennungstext wird eine Kachel ===");
{
const g = leseBild(ERKANNT);
ok(g.pflicht.titel === "[Star Light] Refined visuals Goal 1.0",
`der Titel: ${JSON.stringify(g.pflicht.titel)}`);
/* OHNE DAS STATUSABZEICHEN und ohne die Knoepfe daneben. Auf dem
Bild steht rechts „Kampagne duplizieren" und links „In
Bearbeitung" -- beides gehoert nicht in einen Kacheltitel. */
ok(!/Bearbeitung|duplizieren|anzeigen/i.test(g.pflicht.titel),
" ohne Statusabzeichen und ohne die Knoepfe daneben");
ok(g.pflicht.start === "2026-10-01" && g.pflicht.ende === "2026-10-31",
`der Zeitraum: ${g.pflicht.start} bis ${g.pflicht.ende}`);
/* DIE DEUTSCHE SCHREIBWEISE GEWINNT -- sie ist eindeutig. Die
amerikanische oben (10/01/2026) ist die Gegenprobe. */
ok(g.auszug.zeitraum_quelle === "deutsch",
`gelesen aus der deutschen Angabe (${g.auszug.zeitraum_quelle})`);
ok(g.auszug.zeitraum_gegenprobe === "2026-10-01..2026-10-31",
`und die amerikanische sagt dasselbe (${g.auszug.zeitraum_gegenprobe})`);
ok(g.pflicht.zone === "Europe/Amsterdam", `die Zeitzone: ${g.pflicht.zone}`);
ok(g.auszug.status_anzeige === "In Bearbeitung",
`der Stand: ${JSON.stringify(g.auszug.status_anzeige)}`);
/* ---- Die Ziele --------------------------------------------- */
ok(g.kuer.aufgaben.length === 3,
`drei Ziele (${g.kuer.aufgaben.length}): ${JSON.stringify(g.kuer.aufgaben)}`);
ok(g.kuer.aufgaben[0] === "Geh LIVE für 900 Minuten",
" das erste wortgleich, ohne den Punkt, den die Texterkennung anhaengt");
ok(g.kuer.aufgaben[2] === "An 20 Tagen Ziele für optimierte Bildqualität erreichen",
" und das dritte mit Umlauten");
/* DIE PHASENUEBERSCHRIFT IST KEINE AUFGABE. Sie traegt auf der
Seite ein Zielscheiben-Zeichen, und daraus macht die
Texterkennung ein `@` -- sie sah damit aus wie eine
Aufzaehlungszeile. Beim ersten Lauf hiess die erste „Aufgabe"
wirklich „Ziele erreichen Endetin 23 Tage 22 : 50 : 44". */
ok(!g.kuer.aufgaben.some((a) => /Endet|Ziele erreichen|Leistung/i.test(a)),
" und die Phasenueberschrift mit ihrem Countdown ist keine");
ok(g.kuer.einleitung === "Zusätzliche Prämien für das Team im Oktober.",
`die Beschreibung: ${JSON.stringify(g.kuer.einleitung)}`);
ok(g.kuer.steckbrief.verbund === "Spicy Media",
`erstellt von: ${JSON.stringify(g.kuer.steckbrief.verbund)}`);
/* ---- Die Nebenangaben -------------------------------------- */
const zeilen = g.kuer.weitereRegeln[0]?.zeilen || [];
ok(zeilen.includes("Sichtbar für: Privat"),
`„Sichtbar für" steht sauber da: ${JSON.stringify(zeilen)}`);
/* HALBE SCHILDER WAREN DER FEHLER. Mit dem Stueck „Sichtbar" kam
„Sichtbar für: für Privat" heraus -- das zweite Wort gehoert zum
Schild, nicht zum Wert. */
ok(!zeilen.some((z) => /: (für|erforderlich|von|innen)/i.test(z)),
" und kein Schild steht doppelt im eigenen Wert");
/* UND „Creator*innen" SCHNEIDET NICHT MEHR IM NACHBARN. Es steht
mitten in „1 Creator Network, 55 Creator*innen" und machte daraus
„1 Creator Network, 55". */
ok(zeilen.some((z) => /Teilnehmer.*55 Creator/i.test(z)),
" und die Teilnehmerzahl ist vollstaendig");
/* DIE ZAHL, DER MAN NICHT TRAUEN KANN, FEHLT. Auf der Seite steht
„1 ⓘ" -- der Kreis wird als Null gelesen, heraus kaeme „10".
Keine Angabe ist richtig, eine falsche Angabe ist falsch. */
ok(!zeilen.some((z) => /^Creator\*innen:/i.test(z)),
" und die unsichere Creator-Zahl steht gar nicht erst da");
/* ---- Die Form ist dieselbe wie beim Link-Leser -------------- */
/* DAS IST DIE TRAGENDE ZUSICHERUNG DIESER DATEI. Weicht sie ab,
greifen Gliederung, Geschenke, Aufklappen und Teilen nicht mehr
-- und zwar still. */
const { leseKampagne } = await import("./kampagne-lesen.mjs");
const { brotliDecompressSync } = await import("node:zlib");
const vomLink = leseKampagne(brotliDecompressSync(
readFileSync(join(DATEN, "kampagne-gipfelstuermer.html.br"))).toString("utf8"));
const felderBild = Object.keys(g.kuer).sort();
const felderLink = Object.keys(vomLink.kuer).sort();
ok(felderBild.join(",") === felderLink.join(","),
"kuer hat dieselben Felder wie beim Link-Leser "
+ `(${felderBild.length} gegen ${felderLink.length})`);
const aBild = Object.keys(g.auszug).sort();
const aLink = Object.keys(vomLink.auszug).sort();
const fehltImBild = aLink.filter((f) => !aBild.includes(f));
ok(fehltImBild.length === 0,
`und der Auszug laesst kein Feld des Link-Lesers aus`
+ (fehltImBild.length ? `: ${fehltImBild.join(", ")}` : ""));
ok(["pflicht", "kuer", "hinweise", "auszug"].every((f) => f in g),
"und die vier Teile heissen gleich");
/* ---- Die Kachel sagt, woher sie kommt ---------------------- */
ok(g.auszug.gelesen_aus === "bildschirmfoto",
`der Auszug nennt die Quelle (${g.auszug.gelesen_aus})`);
ok(typeof g.auszug.erkannter_text === "string"
&& g.auszug.erkannter_text.includes("Star Light"),
`und traegt den Erkennungstext mit (${g.auszug.erkannter_text.length} Zeichen)`);
}
/* =====================================================================
2. WAS NICHT GEHT — UND WARUM DAS RICHTIG IST
===================================================================== */
melde("");
melde("=== 2. Lieber keine Kachel als eine mit erfundenem Termin ===");
{
const wirft = (text, art, was) => {
let f = null;
try { leseBild(text); } catch (x) { f = x; }
ok(f instanceof BildUnlesbar && f.art === art,
`${was} -> ${f ? `${f.name}/${f.art}` : "KEIN Fehler"}`);
return f;
};
wirft("", "aufbau", "gar kein Text");
wirft("nur\nzwei", "aufbau", "fast kein Text");
wirft("Gruppenname Test\nZeitzone Europe/Berlin\nnoch eine Zeile",
"pflicht", "ein Bild ohne Zeitraum");
/* DIE GEGENPROBE ZWISCHEN DEN ZWEI SCHREIBWEISEN -- der Kern
dieser Datei. Eine Texterkennung macht aus einer 3 eine 8, und
„bis 08.10." liest sich genauso plausibel wie „bis 03.10.". */
const widerspruch = [
"Kampagne X",
"10/01/2026 00:00:00 ~ 10/31/2026 22:59:59 (UTC+02:00)",
"Gruppenname Kampagne X Zeitzone Europe/Berlin",
"Daten 01.10.2026 00:00 ~ 28.10.2026 22:59",
].join("\n");
const f = wirft(widerspruch, "pflicht", "zwei Zeitraeume, die sich widersprechen");
ok(/zweimal und beide Male anders/.test(f?.message || ""),
` und die Meldung sagt, was los ist: „${f?.message?.slice(0, 70)}…"`);
/* GEGENPROBE ZUR GEGENPROBE: Stimmen beide ueberein, entsteht sehr
wohl eine Kachel. Ohne diese Zeile waere „es wirft" auch dann
gruen, wenn es IMMER wirft. */
const einig = widerspruch.replace("28.10.2026", "31.10.2026");
let g = null;
try { g = leseBild(einig); } catch { g = null; }
ok(g !== null && g.pflicht.ende === "2026-10-31",
`Gegenprobe: stimmen beide ueberein, entsteht die Kachel `
+ `(${g ? g.pflicht.ende : "KEINE"})`);
/* Steht nur EINE Schreibweise da, geht es -- mit Hinweis. Das ist
der mittlere Fall: nicht falsch, aber auch nicht doppelt
geprueft. */
const nurDeutsch = "Kampagne Y\nGruppenname Kampagne Y Zeitzone Europe/Berlin\n"
+ "Daten 05.11.2026 00:00 ~ 20.11.2026 22:59";
const h = leseBild(nurDeutsch);
ok(h.pflicht.start === "2026-11-05" && h.pflicht.ende === "2026-11-20",
`eine Schreibweise allein reicht (${h.pflicht.start}..${h.pflicht.ende})`);
ok(h.hinweise.some((x) => /nur an einer Stelle/i.test(x)),
` und die Kachel sagt, dass nicht gegengeprueft werden konnte`);
/* ---- DER TITEL WIRD GEGENGELESEN (08.10.2026) --------------
ANLASS WAR EIN ECHTER FALL: Filipe hat den Bildweg benutzt, und
aus „Refined visuals Goal 1.0“ wurde „Refined visvals Goal 1.0“
— ein u als v. Genau die Verwechslung, die man beim
Korrekturlesen überliest, weil das Wort noch fast richtig
aussieht. Auf der Kachel stand sie still da.
NACHGEMESSEN, OB DIE SPRACHWAHL HILFT: Auf dem Server gegen die
gespeicherte Prüfdatei mit `deu`, `eng`, `deu+eng` und
`eng+deu` — alle vier lesen „visuals“ RICHTIG. Der Fehler ließ
sich also nicht nachstellen, und eine Einstellung zu ändern,
deren Wirkung man nicht messen kann, ist Raten. Es bleibt bei
`deu`.
WAS STATTDESSEN HILFT: Der Titel steht auf der Seite ZWEIMAL —
oben als Überschrift (groß, fett) und im Raster hinter
„Gruppenname“ (klein, normal). Zwei Schriftgrößen heißt zwei
Gelegenheiten, sich zu verlesen, und zwar unabhängig
voneinander. Gehen die beiden Lesungen auseinander, sagt die
Kachel es.
EIN HINWEIS, KEINE ABLEHNUNG — anders als beim Zeitraum. Ein
falsches Datum ist eine Falschauskunft; ein verlesener
Buchstabe im Titel ein Schönheitsfehler. Daran eine ganze
Kachel scheitern zu lassen wäre unverhältnismäßig. */
{
const zweierlei = [
"【Star Light】Refined visvals Goal 1.0 In Bearbeitung",
"Gruppenname 【Star Light】Refined visuals Goal 1.0 Zeitzone Europe/Berlin",
"Daten 01.10.2026 00:00 ~ 31.10.2026 22:59",
].join("\n");
const g = leseBild(zweierlei);
/* ERST BELEGEN, DASS DIE KACHEL ÜBERHAUPT ENTSTEHT. Ohne diese
Zeile wäre das Folgende auch bei einer Ablehnung grün. */
ok(g.pflicht.titel.includes("Refined"),
`die Kachel entsteht trotzdem: ${JSON.stringify(g.pflicht.titel)}`);
ok(g.hinweise.some((h) => /zweimal auf dem Bild/i.test(h)),
`und sagt, dass der Titel zweimal verschieden gelesen wurde: `
+ `${JSON.stringify(g.hinweise.find((h) => /zweimal/i.test(h)) || null)}`);
/* BEIDE LESUNGEN KOMMEN MIT. Fällt später auf, dass die falsche
gewonnen hat, steht die andere schon da. */
ok(g.auszug.titel_raster && g.auszug.titel_kopf
&& g.auszug.titel_raster !== g.auszug.titel_kopf,
`und beide stehen im Auszug: ${JSON.stringify(
[g.auszug.titel_raster, g.auszug.titel_kopf])}`);
/* GEGENPROBE: Stimmen beide überein, gibt es KEINEN Hinweis.
Ohne sie wäre „der Hinweis kommt“ auch dann grün, wenn er
immer käme — und ein Hinweis, der immer kommt, wird
überlesen (CLAUDE.md, 03.09.2026). */
const einig = zweierlei.replace("visvals", "visuals");
const h = leseBild(einig);
ok(!h.hinweise.some((x) => /zweimal auf dem Bild/i.test(x)),
`Gegenprobe: bei zwei gleichen Lesungen kommt kein Hinweis `
+ `(${h.hinweise.length} Hinweise)`);
/* UND DER ECHTE ERKENNUNGSTEXT LÖST IHN AUCH NICHT AUS. Sonst
stünde er an jeder Kachel, und das wäre derselbe Fehler. */
const echt = leseBild(ERKANNT);
ok(!echt.hinweise.some((x) => /zweimal auf dem Bild/i.test(x)),
"und am gespeicherten Erkennungstext ebenso wenig");
}
/* Ein Ende vor dem Anfang ist ein Zahlendreher, kein Zeitraum. */
wirft("Kampagne Z\nGruppenname Kampagne Z\nDaten 20.11.2026 ~ 05.11.2026",
"pflicht", "Ende vor Anfang");
/* Und ein Datum, das es nicht gibt. */
wirft("Kampagne Q\nGruppenname Kampagne Q\nDaten 31.02.2026 ~ 05.03.2026",
"pflicht", "der 31. Februar");
}
/* =====================================================================
3. DAS WASSERZEICHEN
===================================================================== */
melde("");
melde("=== 3. Filipes Mailadresse liegt ueber dem ganzen Bild ===");
{
/* Auf seinen Bildschirmfotos steht sie vielfach und schraeg ueber
der Seite. Gemessen: Sie taucht im Erkennungstext kein einziges
Mal auf -- zu wenig Kontrast. Das ist Glueck, kein Verdienst, und
deshalb wird es festgehalten: Wuerde eine kuenftige
Tesseract-Fassung sie lesen, stuende sie mitten in den Zielen. */
ok(!/dogfather1608|gmail/i.test(ERKANNT),
"das Wasserzeichen steht nicht im Erkennungstext");
const g = leseBild(ERKANNT);
const alles = JSON.stringify(g);
ok(!/dogfather1608|gmail/i.test(alles),
"und damit auch nirgends in der Kachel");
/* GEGENPROBE: Stuende es doch im Text, wuerde es auch in den Zielen
landen -- also waere die Zeile oben nicht nur Zierde. */
const mitWasser = ERKANNT.replace("« 15 gültige", "« [email protected] 15 gültige");
const g2 = leseBild(mitWasser);
ok(g2.kuer.aufgaben.some((a) => /dogfather1608/.test(a)),
" Gegenprobe: stuende es im Text, stuende es auch in den Zielen");
}
/* =====================================================================
4. KEINE UHR, KEIN NETZ, KEINE DATENBANK
===================================================================== */
melde("");
melde("=== 4. Der Leser haengt an nichts ===");
{
/* OHNE DIE KOMMENTARE (berichtigt 08.10.2026, erster Lauf).
Die Datei ERKLAERT, warum sie die Texterkennung nicht aufruft --
und nannte sie dabei beim Namen. Die Muster unten fanden
„execFile" und „Tesseract" in genau diesen Saetzen und meldeten
einen Schaden, den es nicht gab.
Das ist derselbe Fehlgriff wie am 07.10.2026 beim Teilen, wo das
Wort „agentur" im eingebetteten Stilblatt fuer ein Leck gehalten
wurde: Eine Pruefung, die eine Erklaerung fuer die Sache haelt,
meldet jeden Tag einen Schaden -- und wird nach dem zweiten Mal
weggeklickt.
Gemessen wird der CODE. Der Kommentarraeumer ist absichtlich
schlicht; er muss nur Kommentare entfernen, nicht JavaScript
verstehen. */
const mitAllem = readFileSync(join(HIER, "kampagne-bild-lesen.mjs"), "utf8");
const quelle = mitAllem
.replace(/\/\*[\s\S]*?\*\//g, " ")
.replace(/(^|[^:])\/\/.*/g, "$1 ");
const verboten = [
[/\bimport\s/, "ein import"],
[/\bfetch\s*\(/, "fetch"],
[/\bDate\.now\s*\(/, "Date.now"],
[/new\s+Date\s*\(\s*\)/, "new Date() ohne Angabe"],
[/\bdb\s*\(/, "die Datenbank"],
[/readFileSync|writeFileSync/, "das Dateisystem"],
/* `(?<![.\w])` SCHLIESST `regex.exec(...)` AUS (berichtigt
08.10.2026). Das Muster traf `Z_DEUTSCH.exec(roh)` -- eine
Mustersuche, kein fremder Prozess. Ein Fehlalarm, der jeden Tag
kommt, wird nach dem zweiten Mal weggeklickt, zusammen mit dem
echten Befund darin (CLAUDE.md, 26.09.2026). */
[/\b(execFile|spawnSync|execSync|spawn)\s*\(|(?<![.\w])exec\s*\(/,
"ein fremder Prozess"],
[/tesseract/i, "die Texterkennung selbst"],
];
const drin = verboten.filter(([m]) => m.test(quelle));
ok(drin.length === 0,
`die Datei fuehrt NICHTS ein und fasst nichts an`
+ (drin.length ? ` — gefunden: ${drin.map((x) => x[1]).join(", ")}` : ""));
/* GEGENPROBE: Die Muster finden sehr wohl etwas, wenn es dasteht.
Ohne sie waere die Zeile oben auch bei kaputten Mustern gruen. */
ok(verboten.filter(([m]) => m.test('import x from "y"; fetch(); db(); tesseract')).length >= 4,
" Gegenprobe: die Muster schlagen an, wenn es wirklich dasteht");
/* UND DER KOMMENTARRAEUMER WIRFT NICHT ALLES WEG. Ohne diese Zeile
waere die Pruefung oben auch dann gruen, wenn `quelle` leer ist --
ein gruener Haken ueber einem leeren Feld. */
ok(quelle.length > 1500 && quelle.includes("export function leseBild"),
` und der Code selbst steht noch da (${quelle.length} von `
+ `${mitAllem.length} Zeichen, Rest sind Kommentare)`);
ok(/execFile|tesseract/i.test(mitAllem),
" Gegenprobe: in den KOMMENTAREN kommen die Woerter sehr wohl vor");
/* GLEICHE EINGABE, GLEICHES ERGEBNIS. Eine Lesefunktion mit einer
Uhr darin liefert zu verschiedenen Zeiten Verschiedenes -- und
eine Pruefung darauf ist eine Zeitbombe. */
const a = JSON.stringify(leseBild(ERKANNT));
const b = JSON.stringify(leseBild(ERKANNT));
ok(a.length > 500 && a === b,
`zwei Laeufe, ${a.length} Zeichen, Zeichen fuer Zeichen gleich`);
}
/* =====================================================================
5. DIE TEXTERKENNUNG SELBST
===================================================================== */
melde("");
melde("=== 5. Vom Bild zum Text ===");
{
let da = false;
try {
execFileSync("tesseract", ["--version"], { stdio: "ignore", timeout: 10_000 });
da = true;
} catch { da = false; }
if (!da) {
nichtNachsehbar(
"Tesseract ist auf dieser Maschine nicht eingerichtet — "
+ "der Schritt vom BILD zum TEXT wurde nicht gemessen.",
"Auf dem Server ist es vorhanden (5.5.0 mit deutschen Sprachdaten, "
+ "gemessen 08.10.2026); dort misst dieser Abschnitt wirklich. "
+ "Lokal nachruesten: winget install UB-Mannheim.TesseractOCR — "
+ "die Abschnitte 1 bis 4 laufen auch ohne.");
} else {
const bild = join(DATEN, DATEIEN.bild.name);
let text = "";
try {
text = execFileSync("tesseract",
[bild, "stdout", "-l", "deu", "--psm", "4"],
{ encoding: "utf8", timeout: 30_000, stdio: ["ignore", "pipe", "ignore"] });
} catch (f) {
text = "";
melde(` -- Tesseract brach ab: ${f?.message?.slice(0, 80)}`);
}
ok(text.length > 200, `aus dem Bild kommen ${text.length} Zeichen Text`);
/* NICHT „Zeichen fuer Zeichen gleich wie gespeichert": Eine
andere Tesseract-Fassung liest ein Komma anders, und dann waere
diese Pruefung rot, ohne dass etwas kaputt ist. Gemessen wird,
was zaehlt -- dass die KACHEL dieselbe wird. */
let g = null;
try { g = leseBild(text); } catch (f) { void f; g = null; }
ok(g !== null, "und der Leser macht daraus eine Kachel");
ok(g?.pflicht.titel === "[Star Light] Refined visuals Goal 1.0",
`mit demselben Titel: ${JSON.stringify(g?.pflicht.titel)}`);
ok(g?.pflicht.start === "2026-10-01" && g?.pflicht.ende === "2026-10-31",
`demselben Zeitraum: ${g?.pflicht.start}..${g?.pflicht.ende}`);
ok(g?.kuer.aufgaben.length === 3,
`und denselben drei Zielen (${g?.kuer.aufgaben.length})`);
}
}
melde("");
melde(`${geprueft} Pruefungen, ${fehler} Fehler`
+ (offen ? `, ${offen} nicht nachsehbar` : ""));
if (!geprueft) {
melde("ABBRUCH: nichts geprueft -- das ist ein Fehler, kein Erfolg.");
process.exit(3);
}
melde(fehler ? "NICHT IN ORDNUNG" : (offen ? "IN ORDNUNG, mit Luecke" : "ALLES IN ORDNUNG"));
process.exit(fehler ? 1 : 0);