6. Durchgang: Slots, JavaScript, Bibliotheken - jede Achse jetzt eine Pipeline-Stufe

Die vorigen Durchgaenge waren Handarbeit und liefen nur, wenn jemand daran
dachte. Jetzt sind alle drei automatisiert.

JAVASCRIPT - DIE RENDER-PROBE DECKTE 2 VON 15 FUNKTIONEN AB.
render_check.mjs schnitt per Regex genau ZWEI Funktionen heraus (renderM15,
renderBrk). renderKontext und 13 weitere waren nie abgedeckt - und genau dort
ist mir heute der d-statt-snap-Beinahefehler unterlaufen. Die Probe haette ihn
DURCHGELASSEN.
Neu geschrieben: die komplette app.js laeuft in einem VM-Kontext gegen ein
Minimal-DOM, jede render*-Funktion wird aufgerufen - zweimal, mit dem Fixture
UND mit einem leeren Snapshot (der Kaltstart, in dem jedes Feld fehlt).
13 Funktionen laufen durch. Mutationsprobe mit genau dem heutigen Fehler:
4 Treffer, waehrend node --check NICHTS meldet - der blinde Fleck ist damit zu.
Kein npm, kein jsdom: das DOM ist eine Attrappe von 20 Zeilen, Pruef- und
Laufumgebung bleiben derselbe Parser.

ZWEI NEUE STUFEN, beide mutationsgeprueft:
  F) ELEMENT-IDs - JS gegen HTML. Stand: 133 IDs, keine verwaist, keine doppelt.
     Kommentare werden vorher entfernt, sonst waeren die Beispiele im
     Kopfkommentar zwei Dauer-Fehlalarme (die Lehre vom Linter).
  G) ABHAENGIGKEITEN - jeder Import aus server.py/core/ gegen requirements.txt.
     Stand: 39 Dateien, alle 9 externen Importe aufgefuehrt, keine Versionsdrift.

UND DIE PIPELINE HAT SICH SELBST ERWISCHT: mein erster Entwurf von Stufe G
importierte `ast` nicht. Der stille except-continue liess JEDE Datei
durchfallen, und die Stufe meldete "alle 0 externen Laufzeit-Importe sind
aufgefuehrt" - ein gruener Haken, hinter dem nichts geprueft wurde. Fuenfte
Wiederholung dieser Fehlerklasse. Gefangen hat es ruff F821 in derselben
Pipeline. Behoben doppelt: Import ergaenzt UND Plausibilitaetspruefung (weniger
Dateien gelesen als vorhanden oder 0 Importe -> Befund).

SLOTS - keine neuen Defekte, drei Punkte praezisiert:
  · Kein geteilter Modul-Zustand. Die modulweiten Container in trailing.py und
    trader.py sind reine Nachschlagetabellen, kein global-Statement. Die beiden
    Instanzen sind tatsaechlich unabhaengig - Voraussetzung des ganzen Umbaus.
  · set_sltp bleibt Slot-1-only, verhaelt sich aber SICHER: liegt nur eine
    BRK-Position, gibt modify_sltp einen Fehlertext zurueck. Es fasst NICHT die
    falsche Position an. Bekannte Grenze, kein Fehler.
  · trail_brk.enabled wird nicht persistiert; _rebind_brk schaltet es nach einem
    Neustart bedingungslos ein. Sichere Richtung, bewusst so gelassen.

BIBLIOTHEKEN: nichts fehlt, nichts driftet. pandas/scikit-learn stehen in
requirements-dev und werden in 0 Dateien importiert (so dokumentiert); numba,
joblib, tqdm, pandas_ta, rich, httpx sind Beifang und ungenutzt - kein
Handlungsbedarf, sie stehen nicht im Laufzeit-Pfad.
Struktureller Rest benannt: engine.py importiert weekly_review und
measurement_reminder, zwei Skripte der obersten Ebene - dieselbe Klasse wie der
frueher direkte backtest-Import. Hier entschaerft, weil lazy und in try, mit
sichtbarer Fehlermeldung. In Stufe G als projekteigen ausgenommen, mit
Begruendung im Code.

Pipeline jetzt A A2 A3 B B2 C C2 D E F G - alle gruen, 97 Tests, deployt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Axel Hocks
2026-08-20 20:06:46 +02:00
co-authored by Claude Opus 5
parent 3391e04f32
commit 3fb9c3fd2e
3 changed files with 311 additions and 30 deletions
+85
View File
@@ -9257,6 +9257,91 @@ liegen — meine Invariante war zu streng; (2) zwei inhaltsgleiche dicts sind
mich selbst: auf einer frischen Maschine soll der Bot mit möglichst kleiner mich selbst: auf einer frischen Maschine soll der Bot mit möglichst kleiner
Oberfläche wieder laufen. Ergänzt wurde ausschliesslich `requirements-dev.txt`. Oberfläche wieder laufen. Ergänzt wurde ausschliesslich `requirements-dev.txt`.
## ⚠⚠ SECHSTER DURCHGANG: SLOTS · JAVASCRIPT · BIBLIOTHEKEN (2026-08-20)
User-Auftrag. Diesmal wurde aus **jeder** der drei Achsen eine **Pipeline-Stufe**
gemacht — die vorigen Durchgänge waren Handarbeit und liefen nur, wenn jemand
daran dachte.
### JavaScript — die Render-Probe deckte 2 von 15 Funktionen ab
⚠⚠ **`tools/render_check.mjs` schnitt per Regex genau ZWEI Funktionen heraus**
(`renderM15`, `renderBrk`). **`renderKontext` und 13 weitere waren nie
abgedeckt** — und genau in `renderKontext` ist mir am selben Tag der
`d`-statt-`snap`-Beinahe-Fehler unterlaufen. Die Probe hätte ihn **durchgelassen**.
**Neu geschrieben**: die **komplette** `app.js` läuft in einem VM-Kontext gegen
ein Minimal-DOM, und **jede** gefundene `render*`-Funktion wird aufgerufen —
zweimal: mit dem Fixture **und mit einem leeren Snapshot** (der Kaltstart, in dem
jedes Feld fehlt; dort schlagen fehlende Null-Prüfungen zu, nicht bei vollen
Daten). **13 Funktionen laufen durch.**
✅ **Mutationsprobe mit genau dem heutigen Fehler:**
```
❌ renderKontext(Fixture): ReferenceError: d is not defined
❌ render(Fixture): ReferenceError: d is not defined
❌ renderKontext(leerer Snapshot): …
```
**`node --check` meldet dabei NICHTS** — die Datei ist syntaktisch einwandfrei.
Das ist der blinde Fleck, den diese Stufe schliesst.
⚠ Kein npm, kein jsdom: das DOM ist eine Attrappe von 20 Zeilen. Prüf- und
Laufumgebung bleiben derselbe Parser (V8).
### Zwei neue Pipeline-Stufen
**F) ELEMENT-IDs** — greift die JS auf IDs zu, die es in der HTML nicht gibt, und
gibt es doppelte? Stand: **133 IDs, keine verwaist, keine doppelt.**
⚠ Kommentare werden vorher entfernt: `$("neu")` und `$("x")` stehen als
**Beispiele** im Kopfkommentar von `app.js` und wären sonst zwei Dauer-Fehlalarme
— und eine Prüfung mit Dauer-Treffern wird ignoriert (die Lehre vom Linter).
**G) ABHÄNGIGKEITEN** — ist jeder Import aus `server.py`/`core/` in
`requirements.txt`? Stand: **39 Dateien gelesen, alle 9 externen Importe
aufgeführt**, keine Versions-Drift zwischen gepinnt und installiert.
✅ Beide **mutationsgeprüft**: ID umbenannt → Befund; `feedparser` aus der Liste
gestrichen → Befund.
⚠⚠ **UND DIE PIPELINE HAT SICH SELBST ERWISCHT.** Mein erster Entwurf von Stufe G
importierte `ast` nicht. Der stille `except Exception: continue` liess dadurch
**jede** Datei durchfallen — und die Stufe meldete *„alle 0 externen
Laufzeit-Importe sind aufgeführt."* Ein grüner Haken, hinter dem **nichts**
geprüft wurde: die **fünfte** Wiederholung dieser Fehlerklasse im Projekt.
Gefangen hat es **`ruff F821` in derselben Pipeline**.
✅ Behoben doppelt: `ast` importiert **und** eine Plausibilitätsprüfung
(„weniger Dateien gelesen als vorhanden, oder 0 externe Importe → Befund"). Eine
Stufe, die nichts gelesen hat, darf nicht „sauber" melden.
### Slots — keine neuen Defekte, drei Punkte präzisiert
**Kein geteilter Modul-Zustand**: die modulweiten Container in `trailing.py`
und `trader.py` (`_TF_LABELS`, `_PHASE_RANK`, `_RETCODE_MSG`) sind reine
Nachschlagetabellen, kein `global`-Statement. Die beiden Instanzen sind
tatsächlich unabhängig — das war die Voraussetzung des ganzen Umbaus.
**`set_sltp` bleibt Slot-1-only, verhält sich aber SICHER**: liegt nur eine
BRK-Position, gibt `modify_sltp` einen Fehlertext zurück (`if not self.ticket`).
Es fasst also **nicht** die falsche Position an — die SL/TP-Felder der
Trade-Leiste sind lediglich auf Slot 1 beschränkt. Bekannte Grenze, kein Fehler.
**`trail_brk.enabled` wird NICHT persistiert** (nur `brk_ticket`).
`_rebind_brk` schaltet es nach einem Neustart bedingungslos ein. Das ist die
sichere Richtung — aber wer es bewusst abgeschaltet hat, bekommt es nach einem
Neustart zurück. Bewusst so gelassen: eine BRK-Position ohne Trailing ist
gemessen die schlechtere Variante.
### Bibliotheken
**Nichts fehlt, nichts driftet.** `pandas` und `scikit-learn` stehen in
`requirements-dev.txt` und werden in **0** Dateien importiert (so dokumentiert);
`numba`, `joblib`, `tqdm`, `pandas_ta`, `rich`, `httpx` sind als Beifang anderer
Pakete installiert und ebenfalls ungenutzt. Kein Handlungsbedarf — sie stehen
nicht im Laufzeit-Pfad.
**Struktureller Rest, jetzt benannt**: `core/engine.py` importiert
`weekly_review` und `measurement_reminder` — zwei Skripte der obersten Ebene.
Dieselbe Klasse wie der frühere `backtest_breakout_squeeze`-Import, der am 07.08.
nach `core/squeeze_scan.py` gezogen wurde. **Hier entschärft**, weil beide
**lazy** und in `try` importiert werden (1×/Tag bzw. 1×/Woche) und ein
Fehlschlag sichtbar gemeldet wird. In Stufe G als projekteigen ausgenommen —
mit Begründung im Code, damit es nicht als Versehen gelesen wird.
**Pipeline jetzt: A · A2 · A3 · B · B2 · C · C2 · D · E · F · G** — alle grün,
97 Tests, deployt und verifiziert.
## ⚠⚠⚠ P(break) IST LIVE **INVERTIERT** — und es steuert echtes Geld (2026-08-19) ## ⚠⚠⚠ P(break) IST LIVE **INVERTIERT** — und es steuert echtes Geld (2026-08-19)
Die fällige Messung ist entscheidbar geworden: **n=530 entkoppelt** gegen die Die fällige Messung ist entscheidbar geworden: **n=530 entkoppelt** gegen die
+112
View File
@@ -16,6 +16,7 @@ Geprueft wird:
naechste besteht nur aus einem Quote — die Signatur der beiden heutigen naechste besteht nur aus einem Quote — die Signatur der beiden heutigen
Faelle. Faelle.
""" """
import ast
import pathlib import pathlib
import py_compile import py_compile
import re import re
@@ -448,9 +449,120 @@ def _stufe_slots():
befunde.append("SLOT Werkzeugfehler") befunde.append("SLOT Werkzeugfehler")
# ⚠⚠ Zwei Pruefungen, die die Gesamtdurchsicht vom 2026-08-20 als Luecke
# gezeigt hat — beide waren bis dahin HANDARBEIT und liefen nur, wenn jemand
# daran dachte.
def _stufe_ids():
"""F) Greift die JS auf Element-IDs zu, die es in der HTML NICHT gibt?
⚠ Ein fehlendes Element wird seit v=150 vom Auffang-`$` abgefangen (die
Regel war bei 0 von 95 Zugriffen befolgt) — aber `$streng` gibt weiter
`None`, und genau dort haengt der ORDER-Pfad. Ausserdem ist eine verwaiste
ID fast immer der Rest eines halb entfernten Features.
⚠ Kommentare werden ENTFERNT, bevor gesucht wird: `$("neu")` und `$("x")`
stehen als BEISPIELE im Kopfkommentar von `app.js` und waeren sonst zwei
Dauer-Fehlalarme — und eine Pruefung mit Dauer-Treffern wird ignoriert.
"""
print("")
print("=" * 88)
print(" F) ELEMENT-IDs — JS gegen HTML")
print("=" * 88)
h = WURZEL / "web" / "index.html"
j = WURZEL / "web" / "app.js"
if not (h.exists() and j.exists()):
print(" web/ fehlt — Stufe uebersprungen.")
return
html = h.read_text(encoding="utf-8")
js = j.read_text(encoding="utf-8")
js = re.sub(r"/\*.*?\*/", "", js, flags=re.S)
js = re.sub(r"(?m)^\s*//.*$", "", js)
html_ids = re.findall(r'id="([^"]+)"', html)
doppelt = sorted({i for i in html_ids if html_ids.count(i) > 1})
fehlt = sorted(set(re.findall(r'\$(?:streng)?\("([^"]+)"\)', js)) - set(html_ids))
for i in fehlt:
befunde.append(f"ID verwaist: $(\"{i}\") ohne Element in index.html")
print(f" FEHLER verwaiste ID: {i}")
for i in doppelt:
befunde.append(f"ID doppelt in index.html: {i}")
print(f" FEHLER doppelte ID: {i}")
if not fehlt and not doppelt:
print(f" {len(set(html_ids))} IDs, keine verwaist, keine doppelt.")
def _stufe_requirements():
"""G) Ist jeder LAUFZEIT-Import in `requirements.txt` aufgefuehrt?
⚠ Bis 2026-08-07 gab es gar keine Liste; nach einem Maschinenausfall waere
die Rekonstruktion Raten gewesen. Die Liste zu HABEN reicht aber nicht — sie
veraltet beim ersten neuen Import still. Geprueft wird nur `server.py` +
`core/` (der Live-Pfad); Backtests duerfen dev-Pakete nutzen.
"""
print("")
print("=" * 88)
print(" G) ABHAENGIGKEITEN — Laufzeit-Importe gegen requirements.txt")
print("=" * 88)
req_p = WURZEL / "requirements.txt"
if not req_p.exists():
print(" requirements.txt fehlt — Stufe uebersprungen.")
return
req = req_p.read_text(encoding="utf-8").lower()
alias = {"bs4": "beautifulsoup4", "deep_translator": "deep-translator",
"yaml": "pyyaml", "dotenv": "python-dotenv"}
stdlib = set(sys.stdlib_module_names)
# ⚠ Projekt-EIGENE Module, keine pip-Pakete. `weekly_review` und
# `measurement_reminder` sind Skripte auf oberster Ebene — `engine.py`
# importiert sie beide LAZY (im Funktionskoerper, in `try`), also nur
# 1x/Tag bzw. 1x/Woche. Das ist dieselbe Klasse wie der frueher direkte
# `backtest_breakout_squeeze`-Import, der am 07.08. nach
# `core/squeeze_scan.py` gezogen wurde — hier aber entschaerft, weil der
# Live-Pfad sie nicht beim Start braucht und ein Fehlschlag gemeldet wird.
eigen = {"core", "tools", "server", "weekly_review", "measurement_reminder"}
dateien_ = [WURZEL / "server.py"] + sorted((WURZEL / "core").rglob("*.py"))
mods = set()
gelesen = 0
for p_ in dateien_:
try:
b = ast.parse(p_.read_text(encoding="utf-8"))
gelesen += 1
except Exception as e:
# ⚠⚠ NICHT still ueberspringen. Der erste Entwurf tat genau das —
# `ast` war nicht importiert, JEDE Datei fiel in diesen Zweig, und
# die Stufe meldete "alle 0 externen Laufzeit-Importe sind
# aufgefuehrt". Ein gruener Haken, hinter dem NICHTS geprueft
# wurde: die vierte Wiederholung dieser Fehlerklasse im Projekt.
# Gefangen hat es `ruff F821` in derselben Pipeline.
befunde.append(f"REQ {p_.name} nicht lesbar: {e}")
print(f" FEHLER {p_.name} nicht analysierbar: {e}")
continue
for n in ast.walk(b):
if isinstance(n, ast.Import):
mods |= {a.name.split(".")[0] for a in n.names}
elif isinstance(n, ast.ImportFrom) and n.level == 0 and n.module:
mods.add(n.module.split(".")[0])
fehlend = [m for m in sorted(mods)
if m and m not in stdlib and m not in eigen
and alias.get(m, m).lower() not in req]
for m in fehlend:
befunde.append(f"REQ Laufzeit-Import `{m}` fehlt in requirements.txt")
print(f" FEHLER `{m}` wird im Live-Pfad importiert, steht aber nicht in "
f"requirements.txt")
extern = [m for m in mods if m and m not in stdlib and m not in eigen]
if gelesen < len(dateien_) or not extern:
# Eine Stufe, die nichts gelesen hat, darf nicht "sauber" melden.
befunde.append(f"REQ nur {gelesen}/{len(dateien_)} Dateien gelesen, "
f"{len(extern)} externe Importe gefunden — unplausibel")
print(f" FEHLER nur {gelesen}/{len(dateien_)} Dateien gelesen, "
f"{len(extern)} externe Importe — die Stufe hat nichts geprueft")
elif not fehlend:
print(f" {gelesen} Dateien gelesen, alle {len(extern)} externen "
f"Laufzeit-Importe sind aufgefuehrt.")
_stufe_typen() _stufe_typen()
_stufe_render() _stufe_render()
_stufe_slots() _stufe_slots()
_stufe_ids()
_stufe_requirements()
print("") print("")
print("=" * 88) print("=" * 88)
+114 -30
View File
@@ -1,34 +1,118 @@
// Render-Probe: laesst renderM15 + renderBrk gegen ein Minimal-DOM laufen. // Render-Probe: laesst ALLE render*-Funktionen gegen ein Minimal-DOM laufen.
// //
// ⚠⚠ WARUM ES DAS GIBT (2026-08-19): `node --check` prueft nur die SYNTAX. Ein // ⚠⚠ WOZU. Ein `ReferenceError` mitten im Render bricht den GESAMTEN Durchlauf
// ReferenceError (falsche Variable im falschen Scope) ist syntaktisch // ab: das Dashboard friert auf Altwerten ein, Server und Snapshot sind dabei
// einwandfrei und faellt erst im Browser auf - dort bricht er aber den // korrekt. `node --check` sieht das NICHT — die Datei ist syntaktisch einwandfrei.
// GESAMTEN Render ab: alles ab der Fehlerstelle bleibt auf "—", und die // Real passiert:
// danach gerufenen Renderer laufen gar nicht mehr. Real passiert: die // · 01.08. — ein direkter Zugriff auf ein noch nicht ausgeliefertes Element
// RSI-Zeile griff in `renderM15(s)` auf `d.market` zu; das Dashboard zeigte // · 20.08. — die RSI-Zeile griff in `renderM15(s)` auf `d.market` zu
// sechs leere Zeilen und eine komplett leere Breakout-Karte. // · 20.08. — der Trailing-Block landete in `renderKontext(snap)` und las `d.…`
// Python hat dafuer `ruff F821` - fuer JS gab es nichts Vergleichbares. // Der dritte Fall waere durch die ALTE Fassung dieser Probe geschluepft: sie
// schnitt per Regex genau ZWEI Funktionen heraus (`renderM15`, `renderBrk`).
// `renderKontext` und 13 weitere waren nie abgedeckt.
//
// ⚠ Jetzt wird die KOMPLETTE `app.js` in einem VM-Kontext geladen und jede
// gefundene `render*`-Funktion aufgerufen — mit dem Fixture-Snapshot UND mit
// einem leeren Objekt (der Kaltstart-Fall, in dem jedes Feld fehlt).
//
// ⚠ Kein npm, kein jsdom: das DOM ist eine Attrappe von 20 Zeilen. Damit bleibt
// die Pruefumgebung dieselbe Laufzeit wie die Zielumgebung (V8) — genau die
// Divergenz, die beim `.ps1`-BOM und beim pre-commit-Framework zugeschlagen hat.
// //
// Aufruf: node tools/render_check.mjs <snapshot.json> // Aufruf: node tools/render_check.mjs <snapshot.json>
// Snapshot holen: curl -s http://127.0.0.1:8000/api/snapshot > snap.json import fs from "node:fs";
import fs from "fs"; import vm from "node:vm";
const snap = JSON.parse(fs.readFileSync(process.argv[2], "utf-8"));
const pfad = process.argv[2] || "tests/fixtures/snapshot.json";
const snap = JSON.parse(fs.readFileSync(pfad, "utf-8"));
const src = fs.readFileSync("web/app.js", "utf-8"); const src = fs.readFileSync("web/app.js", "utf-8");
const store = {};
const mk = (id) => ({ id, set textContent(v){ store[id]=v; }, get textContent(){return store[id]||"";}, function el() {
set className(v){}, get className(){return "";}, classList:{add(){},remove(){},toggle(){}}, const o = {
style:{}, value:"", title:"", addEventListener(){}, blur(){}, focus(){} }); textContent: "", innerHTML: "", className: "", title: "", value: "",
const cache = {}; disabled: false, checked: false, style: {}, dataset: {},
global.document = { getElementById: (id) => (cache[id] ||= mk(id)), classList: { add() {}, remove() {}, toggle() {}, contains() { return false; } },
querySelectorAll: () => [], querySelector: () => null, addEventListener(){}, appendChild() {}, removeChild() {}, setAttribute() {}, removeAttribute() {},
get activeElement(){return null;}, createElement: () => mk("_") }; addEventListener() {}, focus() {}, blur() {}, remove() {},
const m = src.match(/function renderM15\([\s\S]*?\n\}/); getBoundingClientRect() { return { top: 0, left: 0, width: 0, height: 0 }; },
const b = src.match(/function renderBrk\([\s\S]*?\n\}/); querySelector() { return el(); }, querySelectorAll() { return []; },
const helfer = 'const _F=document.createElement("span");' + children: [], parentNode: null, firstChild: null,
'const $=(id)=>document.getElementById(id)||_F; const $streng=(id)=>document.getElementById(id);'; };
const fn = new Function("snapshot", helfer + "\n" + m[0] + "\n" + (b?b[0]:"function renderBrk(){}") + return o;
"\nrenderM15(snapshot.m15_setup, snapshot); renderBrk(snapshot);"); }
try { fn(snap); console.log(" ✅ renderM15 + renderBrk laufen OHNE Exception durch"); }
catch (e) { console.log(" ❌ EXCEPTION: " + e.message); process.exit(1); } const ctx = {
for (const k of ["m15-rsi","m15-m5","m15-motor","brk-pos","brk-state"]) console,
console.log(" " + k.padEnd(12) + " = " + (store[k] || "(leer)")); document: {
getElementById: () => el(), querySelector: () => el(),
querySelectorAll: () => [], createElement: () => el(),
addEventListener() {}, body: el(), documentElement: el(),
activeElement: null, hidden: false, visibilityState: "visible",
},
window: {
addEventListener() {}, setTimeout() {}, clearTimeout() {},
location: { href: "", protocol: "http:", host: "x" },
localStorage: { getItem: () => null, setItem() {}, removeItem() {} },
matchMedia: () => ({ matches: false, addEventListener() {} }),
},
navigator: { userAgent: "node" },
location: { href: "", protocol: "http:", host: "x" },
localStorage: { getItem: () => null, setItem() {}, removeItem() {} },
setInterval: () => 0, clearInterval() {}, setTimeout: () => 0, clearTimeout() {},
requestAnimationFrame: () => 0,
fetch: () => new Promise(() => {}),
WebSocket: function () { return { close() {}, send() {} }; },
AudioContext: function () {
return { createOscillator: () => ({ connect() {}, start() {}, stop() {},
frequency: { setValueAtTime() {} }, type: "" }),
createGain: () => ({ connect() {}, gain: { setValueAtTime() {},
exponentialRampToValueAtTime() {} } }),
currentTime: 0, destination: {}, resume: () => Promise.resolve(),
state: "running" };
},
Notification: function () {},
alert() {}, confirm: () => true, prompt: () => null,
};
ctx.window.document = ctx.document;
ctx.globalThis = ctx;
ctx.self = ctx;
vm.createContext(ctx);
try {
vm.runInContext(src, ctx, { filename: "app.js" });
} catch (e) {
console.log(` ❌ app.js liess sich nicht laden: ${e.constructor.name}: ${e.message}`);
process.exit(1);
}
// Jede render*-Funktion einsammeln (die im Kontext wirklich existiert).
const namen = Object.keys(ctx).filter(
(k) => /^render/.test(k) && typeof ctx[k] === "function");
if (namen.length < 5) {
console.log(` ❌ nur ${namen.length} render*-Funktionen gefunden — Probe greift nicht`);
process.exit(1);
}
// ⚠ Zwei Durchlaeufe. Der LEERE Snapshot ist der wichtigere: er stellt den
// Kaltstart nach, in dem noch kein Feld befuellt ist — dort schlagen fehlende
// Null-Pruefungen zu, nicht bei vollen Daten.
const faelle = [["Fixture", snap], ["leerer Snapshot", {}]];
let fehler = 0;
for (const [lbl, daten] of faelle) {
for (const n of namen) {
try {
// `renderM15` nimmt (setup, snapshot), alle uebrigen nur (snapshot).
if (n === "renderM15") ctx[n](daten.m15_setup || {}, daten);
else ctx[n](daten);
} catch (e) {
fehler++;
console.log(`${n}(${lbl}): ${e.constructor.name}: ${e.message}`);
}
}
}
if (fehler) {
console.log(` ${fehler} Fehler in ${namen.length} render*-Funktionen`);
process.exit(1);
}
console.log(`${namen.length} render*-Funktionen laufen durch `
+ `(Fixture UND leerer Snapshot)`);