Files
dogfather-universe/server/pruef-kampagne-bild.mjs
T
DogFatherGitandClaude Opus 5 5a6641e8da Bildleser: eine Ueberschriftenregel, die Aufgaben verschluckt hat
GEFUNDEN IM ECHTEN BETRIEB, nicht beim Bauen. Filipe hat den
Bildschirmfoto-Weg am 08.10.2026 zum ersten Mal benutzt. Im Protokoll:

  09:15:17  kampagne_eingelesen  "...Refined visvals Goal 1.0" -> #94  (2 Aufgaben)
  09:15:26  kampagne_schon_da    derselbe Schluessel -> Eintrag 94
  09:15:52  eintrag_geloescht    agentur #94

Auf seinem Bild stehen DREI Ziele, in der Kachel kamen ZWEI an -- und
er hat sie danach geloescht. Er hat also gearbeitet, das Ergebnis war
still unvollstaendig, und der einzige Hinweis darauf war, dass er es
wegwirft.

DIE URSACHE war die Regel, die die Phasenueberschrift aussortiert. Sie
warf jede Zeile weg, in der "Ziele erreichen", "Leistung" oder
"Phasendetails" IRGENDWO vorkam -- also auch gewoehnliche Ziele, die
zufaellig dieselben Woerter benutzen. Gemessen:

  WEG     Ziele erreichen fuer mehr Reichweite
  WEG     Deine Leistung steigern
  bleibt  An 20 Tagen Ziele fuer optimierte Bildqualitaet erreichen

Die Ueberschrift wird jetzt an ZWEI Dingen erkannt statt an einem
Wort: Sie traegt den Countdown ("Endet in 23 Tage 22 : 50 : 44"), ODER
sie ist die Ueberschrift und sonst nichts (Zeilenende, nicht
Wortgrenze -- mit einer Wortgrenze fiel "Ziele erreichen fuer mehr
Reichweite" weiter durch).

IM ZWEIFEL BLEIBT DIE ZEILE. Eine Ueberschrift zu viel in der
Hakenliste loescht man in zwei Sekunden; eine fehlende Aufgabe merkt
niemand, bis jemand sie nicht erfuellt. Dieselbe Richtung wie der
dritte Ausgang: lieber etwas zu viel melden als still verschlucken.

DAZU DER TITEL, aus demselben Einsatz: Aus "Refined visuals" wurde
"Refined visvals" -- ein u als v. Nachgemessen, ob die Sprachwahl
hilft (deu, eng, deu+eng, eng+deu gegen die Pruefdatei): alle vier
lesen richtig, der Fehler liess sich nicht nachstellen. Eine
Einstellung zu aendern, deren Wirkung man nicht messen kann, ist
Raten. Stattdessen wird der Titel GEGENGELESEN: Er steht auf der Seite
zweimal (Ueberschrift gross und fett, Rasterfeld klein und normal),
also in zwei Schriftgroessen und damit mit zwei unabhaengigen
Gelegenheiten, sich zu verlesen. Gehen die Lesungen auseinander, sagt
die Kachel es -- als Hinweis, nicht als Ablehnung. Ein falsches Datum
ist eine Falschauskunft, ein verlesener Buchstabe im Titel ein
Schoenheitsfehler.

GEPRUEFT: 53 (Bildleser, +10), 190 (Route, unveraendert). Gegenproben
in beide Richtungen: die echte Ueberschrift mit Countdown faellt
weiterhin heraus, eine Zeile die NUR das Wort ist ebenfalls -- und der
Titel-Hinweis kommt NICHT, wenn beide Lesungen uebereinstimmen. Kein
ausgeliefertes Skript beruehrt, also kein neuer Stempel.

PARALLELE ARBEIT: Die Crew-Sitzung hat ausdruecklich zugestimmt und
haelt waehrenddessen still; ihre drei Dateien (reaktion.js,
reaktion.html, getippt.js) bleiben uncommittet in der Arbeitskopie.
Das Arbeitsschloss stand auf ihr -- mit ihrer Zusage ueberschrieben,
nicht eigenmaechtig.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-10-08 14:37:03 +02:00

515 lines
24 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");
/* ---- EINE AUFGABE DARF HEISSEN, WIE SIE WILL (08.10.2026) ---
GEFUNDEN IM ECHTEN BETRIEB, nicht beim Bauen: Filipes erster
Versuch am 08.10.2026 kam laut Protokoll mit ZWEI Aufgaben an,
waehrend auf seinem Bild DREI standen — und er hat die Kachel
danach geloescht.
Die Ursache war die Regel, die die Phasenueberschrift
aussortiert. Sie warf jede Zeile weg, in der „Ziele erreichen“,
„Leistung“ oder „Phasendetails“ IRGENDWO vorkam — also auch
gewoehnliche Ziele, die zufaellig dieselben Woerter benutzen.
Eine Ueberschriftenregel, die Aufgaben verschluckt.
IM ZWEIFEL BLEIBT DIE ZEILE. Eine Ueberschrift zu viel in der
Hakenliste loescht man in zwei Sekunden. Eine fehlende Aufgabe
merkt niemand — bis jemand sie nicht erfuellt. */
{
const mitFallen = [
"Kampagne mit Fallen",
"Gruppenname Kampagne mit Fallen Zeitzone Europe/Berlin",
"Daten 01.10.2026 ~ 31.10.2026",
"@ Ziele erreichen Endetin 23 Tage 22 : 50 : 44",
"· Ziele erreichen für mehr Reichweite",
"· Deine Leistung steigern",
"· An 20 Tagen Ziele für optimierte Bildqualität erreichen",
"· Phasendetails im Kalender eintragen",
].join("\n");
const g2 = leseBild(mitFallen);
ok(g2.kuer.aufgaben.length === 4,
`vier Aufgaben, obwohl drei davon Ueberschriftwoerter tragen `
+ `(${g2.kuer.aufgaben.length}): ${JSON.stringify(g2.kuer.aufgaben)}`);
ok(g2.kuer.aufgaben.includes("Ziele erreichen für mehr Reichweite"),
" „Ziele erreichen für mehr Reichweite“ ist eine Aufgabe");
ok(g2.kuer.aufgaben.includes("Deine Leistung steigern"),
" „Deine Leistung steigern“ auch");
/* GEGENPROBE: Die ECHTE Ueberschrift mit ihrem Countdown faellt
weiterhin heraus. Ohne sie waere „alles bleibt stehen“ auch
dann gruen, wenn gar nichts mehr aussortiert wird. */
ok(!g2.kuer.aufgaben.some((a) => /Endetin|Endet in/i.test(a)),
" die Zeile MIT Countdown bleibt draussen");
/* Und eine Zeile, die NUR aus dem Ueberschriftwort besteht,
ebenfalls -- sonst stuende „Phasendetails“ als Haken da. */
const nurWort = leseBild([...mitFallen.split("\n"), "· Phasendetails"].join("\n"));
ok(!nurWort.kuer.aufgaben.includes("Phasendetails"),
" und eine Zeile, die nur das Wort ist, auch");
}
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);