Files
AH-Oil-Trader/tools/check_nfalle.py
T
Axel HocksandClaude Opus 5 3fb9c3fd2e 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>
2026-08-20 20:06:46 +02:00

573 lines
26 KiB
Python

"""Sucht die \\n-Falle in allen Projektdateien — in BEIDE Richtungen.
Die Falle: beim Schreiben ueber ein Shell-Heredoc wird aus einem gemeinten `\\n`
ein ECHTER Zeilenumbruch mitten im String-Literal. In Python bricht das den
Compile (zweimal heute passiert), in JS ebenso — aber nicht ueberall faellt es
sofort auf.
Geprueft wird:
A) Python: kompiliert jede Datei. Ein zerrissenes Literal faellt hier auf.
B) JS: Zeilen mit ungerader Zahl UNMASKIERTER Anfuehrungszeichen ausserhalb
von Kommentaren = angefangenes, nicht geschlossenes String-Literal.
C) Umgekehrter Fall: literales `\\n` als TEXT dort, wo es niemand lesen will
(HTML-Attribute, Markdown-Fliesstext) — das entsteht, wenn ein `\\\\n`
zu viel escaped wurde.
D) Verdaechtige Zeilenpaare: eine Zeile endet mit oeffnendem Quote, die
naechste besteht nur aus einem Quote — die Signatur der beiden heutigen
Faelle.
"""
import ast
import pathlib
import py_compile
import re
import subprocess
import sys
import tempfile
WURZEL = pathlib.Path(r"c:\Users\ah\Downloads\tradingrobot")
SKIP = {".git", "__pycache__", ".reminders", "node_modules", ".removed_backup"}
befunde = []
def dateien(*endungen):
for p in WURZEL.rglob("*"):
if any(s in p.parts for s in SKIP):
continue
if p.is_file() and p.suffix in endungen:
yield p
# ── A) Python ────────────────────────────────────────────────────────────
print("=" * 88)
print(" A) PYTHON — kompiliert jede Datei (ein zerrissenes Literal faellt hier auf)")
print("=" * 88)
npy = 0
tmp = tempfile.mkdtemp()
for p in dateien(".py"):
npy += 1
try:
py_compile.compile(str(p), cfile=f"{tmp}/x.pyc", doraise=True)
except py_compile.PyCompileError as e:
msg = str(e).strip().splitlines()[-1][:110]
befunde.append(f"PY {p.relative_to(WURZEL)}: {msg}")
print(f" FEHLER {p.relative_to(WURZEL)}")
print(f" {msg}")
print(f" {npy} Dateien geprueft.")
# ── A2) pyflakes: was py_compile NICHT sieht ─────────────────────────────
# ⚠⚠ WARUM DIESE STUFE NOETIG IST. `py_compile` prueft nur die SYNTAX. Ein
# Tippfehler in einem Namen ist syntaktisch einwandfrei und fliegt erst zur
# Laufzeit — und dieses Projekt hat sehr viele breite `except Exception`-Bloecke
# (bewusst, als Fail-safe im Trading-Betrieb). Ein `NameError` darin ist damit
# UNSICHTBAR: keine Meldung, kein Logeintrag, nur ein Zweig, der nichts tut.
# Real gefunden beim ERSTEN Lauf ueberhaupt (2026-08-07):
# · `core/trader.py` nutzte zweimal `log.` — den Namen gibt es dort nicht
# (nur `log_trade`/`log_hist`). Einer davon seit dem Initial-Commit.
# · Folge 1 (`_broker_offset_s`): der NameError lief in ein `except: pass`
# und nahm die Zuweisung `self._boff = int(off)` mit → ein echter
# Broker-Zeitzonen-Wechsel waere nie uebernommen worden.
# · Folge 2 (`modify_sltp`): kein try drumherum → der Fehler lief bis in
# `/api/sltp`; der Broker hatte SL/TP schon geaendert, der User bekam
# trotzdem einen Fehler, und `trail.deactivate()` wurde uebersprungen.
# GETRENNT NACH SCHWEREGRAD: eine Pruefung, die dauerhaft 100 harmlose Zeilen
# ausgibt, wird ignoriert — und dann auch die eine wichtige.
# ⚠ `redefinition of unused` steht BEWUSST NICHT hier drin. pyflakes meldet damit
# auch das voellig legitime Muster `hook = None` gefolgt von einem bedingten
# `def hook(...)` — real in `backtest_flipclose2.py` und
# `backtest_adverse15_squeeze.py`, beide geprueft und korrekt. Zwei unvermeidbare
# Dauer-Treffer sind genau der Weg, auf dem eine Pruefung ignoriert wird, und mit
# ihr dann auch die eine echte Meldung. Wer die Klasse doch sehen will, ruft
# `python -m pyflakes <datei>` direkt auf.
SCHWER = ("undefined name", "undefined local", "syntax error")
print()
print("=" * 88)
# ── A3) TYPPRUEFUNG: falsche Aufrufe ─────────────────────────────────────
# ⚠⚠ WARUM ES DAS BRAUCHT (2026-08-19): `paarweise(d)` wurde mit 1 von 2
# Pflichtargumenten gerufen. py_compile sah nichts (Syntax ok), ruff F821 auch
# nicht (der Name IST definiert), und kein Test deckte den Pfad ab. Der Fehler
# schlug erst zur Laufzeit zu. Genau diese Luecke schliesst `ty`.
# ⚠ BEWUSST ENGES REGELSET (`ty.toml`): der volle Lauf meldet 172 Befunde, davon
# 97 `unresolved-attribute` aus fehlenden MetaTrader5-Stubs. Eine Stufe mit
# Dauer-Treffern wird ignoriert, und mit ihr die eine echte Meldung. Aktiv
# sind nur die Aritaets-Regeln; `call-non-callable`/`not-iterable` sind AUS,
# weil sie hier nur Falsch-Positive aus heterogenen dicts liefern (geprueft).
# ⚠ EXIT-CODES wie bei ruff: 0 sauber · 1 Befunde · alles andere = Werkzeugfehler
# und darf NIEMALS als "sauber" durchgehen (die Lehre vom 07.08.).
def _stufe_typen():
print("")
print("=" * 88)
print(" A3) TYPPRUEFUNG — falsche Aufrufe (Aritaet)")
print("=" * 88)
cfg = WURZEL / "ty.toml"
if not cfg.exists():
print(" ty.toml fehlt — Stufe uebersprungen.")
return
try:
r = subprocess.run([sys.executable, "-m", "ty", "check",
"--config-file", str(cfg),
str(WURZEL / "core"), str(WURZEL / "server.py")],
capture_output=True, text=True, timeout=300)
except FileNotFoundError:
print(" `ty` nicht installiert — Stufe uebersprungen "
"(pip install ty). Eine frische Maschine darf hier nicht haengen.")
return
except Exception as e:
print(f" ⚠ ty nicht ausfuehrbar ({e}) — Stufe uebersprungen.")
return
if r.returncode not in (0, 1):
print(f" ⚠ ty meldet einen WERKZEUGFEHLER (Exit {r.returncode}) — "
f"NICHT als sauber gewertet:")
for z in (r.stderr or r.stdout).strip().splitlines()[:3]:
print(f" {z}")
befunde.append("TY Werkzeugfehler")
return
treffer = [z for z in (r.stdout or "").splitlines() if z.startswith("error[")]
for z in treffer:
befunde.append(f"TY {z.strip()}")
print(f" FEHLER {z.strip()}")
if not treffer:
print(" keine falschen Aufrufe.")
# ── B2) RENDER-PROBE: JS-Laufzeitfehler im Dashboard ─────────────────────
# ⚠⚠ `node --check` prueft nur SYNTAX. Ein ReferenceError (falsche Variable im
# falschen Scope) ist syntaktisch einwandfrei und bricht im Browser den
# GESAMTEN Render ab — alles ab der Fehlerstelle bleibt auf "—" stehen.
# Real am 19.08.: die RSI-Zeile griff in `renderM15(s)` auf `d.market` zu;
# sechs leere Zeilen und eine komplett leere Breakout-Karte.
# ⚠ Laeuft gegen einen GESPEICHERTEN Snapshot (`tests/fixtures/snapshot.json`),
# NICHT gegen den laufenden Server — der Hook darf nicht davon abhaengen, ob
# gerade ein Server laeuft.
def _stufe_render():
print("")
print("=" * 88)
print(" B2) RENDER-PROBE — JS-Laufzeitfehler (was `node --check` nicht sieht)")
print("=" * 88)
werkzeug = WURZEL / "tools" / "render_check.mjs"
fixture = WURZEL / "tests" / "fixtures" / "snapshot.json"
if not werkzeug.exists() or not fixture.exists():
print(" Werkzeug oder Fixture fehlt — Stufe uebersprungen.")
return
try:
r = subprocess.run(["node", str(werkzeug), str(fixture)],
capture_output=True, text=True, timeout=120)
except FileNotFoundError:
print(" node nicht im PATH — Stufe uebersprungen.")
return
except Exception as e:
print(f" ⚠ Render-Probe nicht ausfuehrbar ({e}) — uebersprungen.")
return
aus = (r.stdout or "") + (r.stderr or "")
if r.returncode != 0 or "EXCEPTION" in aus:
for z in aus.strip().splitlines()[:6]:
print(f" {z}")
befunde.append("RENDER " + aus.strip().splitlines()[0][:80])
else:
print(" alle Render-Funktionen laufen ohne Exception durch.")
print(" A2) LINTER — undefinierte Namen (was py_compile nicht sieht)")
print("=" * 88)
# ⚠ RUFF statt pyflakes (2026-08-07, Web-Recherche zum 2026er-Standard): gleiche
# Befunde, deutlich schneller, ein Werkzeug statt vieler. Bewusst NUR
# `ruff check` mit ENGER Regelauswahl — **kein `ruff format`**: das wuerde 161
# Dateien umformatieren, darunter alle Backtests, und damit die Migrations-Regel
# verletzen (deren Zahlen sind dokumentiert).
# F821 = undefined name ← die Klasse, die hier real weh tut
# E999 = syntax error
# F811 (redefinition) bleibt DRAUSSEN, s. Begruendung bei SCHWER oben.
# pyflakes bleibt als Rueckfall, damit die Pruefung auch ohne ruff laeuft.
# ⚠⚠ NUR F821 — `E999` gibt es in ruff 0.16 NICHT MEHR. Wird es trotzdem
# selektiert, bricht ruff KOMPLETT ab („Rule E999 was removed") mit Exit 2.
# Genau das ist beim ersten Bau passiert, und schlimmer: der Rueckgabecode
# wurde nicht geprueft, die Fehlermeldung ging nach stderr — die Stufe meldete
# also „keine undefinierten Namen", waehrend sie GAR NICHTS geprueft hat. Ein
# absichtlich eingebauter `F821`-Fehler kam glatt durch den Hook.
# Syntaxfehler deckt ohnehin Stufe A (`py_compile`) ab.
# ⚠ RUFF-EXIT-CODES: 0 = sauber · 1 = Befunde · 2 = Werkzeugfehler.
# 2 darf NIEMALS als „sauber" durchgehen.
_lint_ok = False
try:
r = subprocess.run([sys.executable, "-m", "ruff", "check",
"--select", "F821", "--no-cache",
"--output-format", "concise", str(WURZEL)],
capture_output=True, text=True, timeout=180)
if r.returncode not in (0, 1):
print(f" ⚠ ruff meldet einen WERKZEUGFEHLER (Exit {r.returncode}) — "
f"Rueckfall auf pyflakes:")
for z in (r.stderr or r.stdout).strip().splitlines()[:3]:
print(f" {z}")
raise RuntimeError("ruff nicht nutzbar")
_lint_ok = True
treffer = [z for z in r.stdout.splitlines()
if z.strip() and not z.startswith(("Found", "All checks", "warning:"))]
for z in treffer:
kurz = z.replace(str(WURZEL) + "\\", "").replace(str(WURZEL) + "/", "")
befunde.append(f"RUFF {kurz}")
print(f" FEHLER {kurz}")
if not treffer:
print(" keine undefinierten Namen (ruff, F821/E999).")
except Exception:
pass
if not _lint_ok:
try:
import io
from pyflakes.api import checkPath
from pyflakes.reporter import Reporter
aus, err = io.StringIO(), io.StringIO()
rep = Reporter(aus, err)
for p in dateien(".py"):
checkPath(str(p), rep)
zeilen = [z for z in (aus.getvalue() + err.getvalue()).splitlines()
if z.strip()]
schwer = [z for z in zeilen if any(s in z for s in SCHWER)]
for z in schwer:
kurz = z.replace(str(WURZEL) + "\\", "").replace(str(WURZEL) + "/", "")
befunde.append(f"PF {kurz}")
print(f" FEHLER {kurz}")
if not schwer:
print(" keine undefinierten Namen (pyflakes-Rueckfall).")
except ImportError:
print(" ⚠ weder ruff noch pyflakes installiert — Stufe uebersprungen.")
print(" Installation: python -m pip install -r requirements-dev.txt")
# ── B) JavaScript ────────────────────────────────────────────────────────
def js_offene_strings(text: str):
"""Zeilen mit angefangenem, nicht geschlossenem ' oder " (Template-Literale
duerfen mehrzeilig sein und werden uebersprungen)."""
treffer = []
in_tmpl = False
for nr, z in enumerate(text.splitlines(), 1):
# Backticks zaehlen -> Template-Literal-Zustand fortschreiben
bt = len(re.findall(r"(?<!\\)`", z))
if in_tmpl:
in_tmpl ^= bool(bt % 2)
continue
if bt % 2:
in_tmpl = True
continue
s = re.sub(r"//.*$", "", z) # Zeilenkommentar weg
# ⚠ Quotes NICHT einfach zaehlen — das erzeugt Fehlalarme: '"' ist ein
# gueltiges Literal, das EIN doppeltes Anfuehrungszeichen enthaelt (real
# in app.js aufgetreten). Stattdessen zeichenweise laufen und den jeweils
# AKTIVEN Quote-Typ verfolgen; Quotes der anderen Art zaehlen dann nicht.
aktiv, esc = None, False
for ch in s:
if esc:
esc = False
continue
if ch == "\\":
esc = True
continue
if aktiv:
if ch == aktiv:
aktiv = None
elif ch in ('"', "'"):
aktiv = ch
if aktiv:
treffer.append((nr, z.strip()[:100]))
return treffer
print()
print("=" * 88)
print(" B) JAVASCRIPT — Syntax (node) + unabgeschlossene String-Literale")
print("=" * 88)
def node_pruefen(p: pathlib.Path):
"""Echte Syntaxpruefung mit `node --check`. Gibt die Fehlerzeile zurueck oder
None; `False` heisst „Node nicht verfuegbar".
⚠ WARUM DAS DAZUKAM (2026-08-07). Die Klammern-Balance findet gemessen nur
1 von 5 realistischen Fehlern — `const x = ;`, ein doppeltes `let`/`const`
und ein `return` ausserhalb einer Funktion laufen glatt durch. Ein
Syntaxfehler in `app.js` bricht aber den GESAMTEN Render: das Dashboard
friert auf Altwerten ein. Bei 95 direkten Element-Zugriffen ist das kein
theoretisches Risiko.
⚠ GEPRUEFT WIRD ALS MODUL (.mjs), NICHT ALS CommonJS. Als CJS haelt Node ein
top-level `return` fuer legal — der Browser laedt `app.js` aber als
klassisches Script, wo es ein Syntaxfehler ist. Der Modul-Modus liegt naeher
am Browser; verifiziert, dass das echte `app.js` ihn ohne Anpassung besteht.
⚠ Kein npm, keine package.json, kein node_modules — `node --check` ist die
Laufzeit selbst. Damit entsteht NICHT die Divergenz „Pruefumgebung ist nicht
die Laufumgebung", an der dieses Projekt schon zweimal aufgelaufen ist:
Node und Chrome benutzen denselben Parser (V8).
⚠ Fehlt Node, faellt die Stufe still auf die alte Prueflogik zurueck — eine
frische Maschine soll den Hook nicht blockieren.
"""
try:
with tempfile.NamedTemporaryFile("w", suffix=".mjs", delete=False,
encoding="utf-8") as f:
f.write(p.read_text(encoding="utf-8", errors="replace"))
tmp = f.name
try:
r = subprocess.run(["node", "--check", tmp],
capture_output=True, text=True, timeout=60)
finally:
try:
pathlib.Path(tmp).unlink()
except OSError:
pass
except (FileNotFoundError, OSError, subprocess.TimeoutExpired):
return False
if r.returncode == 0:
return None
for z in (r.stderr or "").splitlines():
if "Error" in z:
return z.strip()
return "node --check meldet einen Fehler"
for p in dateien(".js"):
if "lightweight-charts" in p.name:
continue
rel = p.relative_to(WURZEL)
nd = node_pruefen(p)
if nd is False:
print(f" ⚠ node fehlt — nur Ersatzpruefung fuer {rel}")
elif nd:
befunde.append(f"JS {rel} (node)")
print(f" FEHLER {rel} {nd}")
continue # bei kaputter Syntax ist die Zeilenheuristik sinnlos
tr = js_offene_strings(p.read_text(encoding="utf-8", errors="replace"))
if tr:
for nr, z in tr:
befunde.append(f"JS {rel}:{nr}")
print(f" FEHLER {rel}:{nr} {z}")
else:
print(f" OK {rel}" + (" (node)" if nd is None else ""))
# ── C) literales \n als TEXT ─────────────────────────────────────────────
print()
print("=" * 88)
print(" C) UMGEKEHRT — literales \\n im sichtbaren Text (HTML-Attribute, Markdown)")
print("=" * 88)
for p in list(dateien(".html")) + list(dateien(".md")):
t = p.read_text(encoding="utf-8", errors="replace")
roh = []
for m in re.finditer(r'title="([^"]*)"', t):
if "\\n" in m.group(1):
roh.append(f"title=...{m.group(1)[:60]}")
if p.suffix == ".md":
for nr, z in enumerate(t.splitlines(), 1):
if "\\n" in z and "`" not in z and "\\\\n" not in z:
roh.append(f"Zeile {nr}: {z.strip()[:80]}")
if roh:
for r in roh[:5]:
befunde.append(f"TXT {p.relative_to(WURZEL)}: {r}")
print(f" FEHLER {p.relative_to(WURZEL)} {r}")
else:
print(f" OK {p.relative_to(WURZEL)}")
# ── D) Signatur der heutigen Faelle ──────────────────────────────────────
# ── C2) .ps1 ohne BOM mit Nicht-ASCII = Zeitbombe ────────────────────────
# ⚠⚠ REAL PASSIERT (2026-08-07): `tools/autologon_einrichten.ps1` liess sich nicht
# ausfuehren — „Schliessende ) fehlt", „Zeichenfolge hat kein Abschlusszeichen".
# Ursache war KEIN Syntaxfehler: **Windows PowerShell 5.1 (`powershell.exe`) liest
# .ps1-Dateien als CP1252, wenn kein BOM da ist.** Aus dem Gedankenstrich „—"
# werden dann drei Zeichen (â€"), und das enthaltene " schliesst den String
# vorzeitig — Folgefehler bis zum Dateiende.
# ⚠ Warum es beim Schreiben nicht auffiel: `pwsh` 7 (mein Werkzeug) liest UTF-8
# OHNE BOM klaglos. Die Datei war also fuer mich fehlerfrei und fuer den Nutzer
# kaputt. Genau die Sorte Divergenz, die dieses Projekt als Deployment-Drift
# kennt — hier auf der Werkzeug-Ebene.
# Fix: UTF-8 MIT BOM schreiben. Gefunden wurde dabei ein zweiter Fall
# (`scripts/send_daily_report.ps1`, mit Umlauten).
print()
print("=" * 88)
print(" C2) POWERSHELL — .ps1 mit Nicht-ASCII braucht ein BOM (sonst CP1252)")
print("=" * 88)
for p in dateien(".ps1"):
roh = p.read_bytes()
bom = roh[:3] == b"\xef\xbb\xbf"
try:
txt = roh.decode("utf-8-sig")
except UnicodeDecodeError:
txt = roh.decode("cp1252", errors="replace")
na = sorted({c for c in txt if ord(c) > 127})
if na and not bom:
befunde.append(f"PS1 {p.relative_to(WURZEL)}: Nicht-ASCII ohne BOM")
print(f" FEHLER {p.relative_to(WURZEL)} "
f"{len(na)} Nicht-ASCII-Zeichen, KEIN BOM → bricht unter "
f"powershell.exe 5.1")
else:
print(f" OK {p.relative_to(WURZEL)}"
+ (" (BOM)" if bom else " (rein ASCII)"))
print()
print("=" * 88)
print(' D) SIGNATUR — Zeile endet offen, naechste besteht nur aus einem Quote')
print("=" * 88)
sig = 0
for p in list(dateien(".py")) + list(dateien(".js")):
zeilen = p.read_text(encoding="utf-8", errors="replace").splitlines()
for i in range(len(zeilen) - 1):
if zeilen[i + 1].strip() in ('"', "'", '",', "',"):
sig += 1
befunde.append(f"SIG {p.relative_to(WURZEL)}:{i+2}")
print(f" VERDAECHTIG {p.relative_to(WURZEL)}:{i+2}")
print(" keine gefunden." if not sig else f" {sig} Stellen.")
print()
print("=" * 88)
# ── E) SLOT-TRENNUNG + ATTRIBUT-REIHENFOLGE ──────────────────────────────
# ⚠⚠ Beide Fehlerklassen haben binnen zwei Tagen je einmal zugeschlagen, und
# KEINE andere Stufe hat sie gefangen (py_compile, ruff F821, ty, pytest:
# alle gruen). Der Attribut-Reihenfolge-Fehler brach den Serverstart KOMPLETT
# ab — der Nutzer konnte zwei Minuten lang nicht schliessen. Gefunden hat ihn
# erst `deploy.py` Schritt 2, also NACH dem Kill des laufenden Servers.
# Diese Stufe findet ihn VORHER.
def _stufe_slots():
print("")
print("=" * 88)
print(" E) SLOTS — Attribut-Reihenfolge in __init__ + Slot-Trennung")
print("=" * 88)
try:
sys.path.insert(0, str(WURZEL / "tools"))
import check_slots
import importlib
importlib.reload(check_slots)
for z in check_slots.pruefe():
befunde.append("SLOT " + z)
print(" FEHLER " + z)
if not check_slots.pruefe():
print(" keine Slot-/Reihenfolge-Fehler.")
except Exception as e:
print(f" ⚠ Slot-Pruefung nicht ausfuehrbar ({e}) — NICHT als sauber gewertet.")
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_render()
_stufe_slots()
_stufe_ids()
_stufe_requirements()
print("")
print("=" * 88)
print(" ERGEBNIS: " + ("keine \\n-Falle gefunden" if not befunde
else f"{len(befunde)} Befund(e)"))
print("=" * 88)
sys.exit(1 if befunde else 0)