User: "Uebernehme die Logikpruefung auf allen Slots in die Deployment pipeline" -
nach dem Ausfall, bei dem der Serverstart komplett abbrach und zwei Minuten lang
kein Close moeglich war.
ZWEI FEHLERKLASSEN, beide binnen zwei Tagen real aufgetreten, beide von KEINER
bestehenden Stufe gefangen (py_compile, ruff F821, ty, pytest: alle gruen):
(A) ATTRIBUT VOR SEINER ZUWEISUNG. `self.trader_brk.history = self.history`
stand in `__init__` VOR `self.trader_brk = TradeManager(...)`.
AttributeError im Konstruktor -> Serverstart brach ab -> "Netzwerkfehler" am
Close-Knopf. Syntaktisch korrekt, und kein Test instanziiert die Engine (sie
braucht MT5). Gefunden hat es erst deploy.py Schritt 2 - also NACH dem Kill
des laufenden Servers. Derselbe Fehler am Vortag mit `_margin_brk` vor
`_load_runtime_state()`, dort sogar still.
Die Pruefung sammelt je `self.X` die ZEILE der ersten Zuweisung und meldet
jede Benutzung davor.
(B) SLOT-VERWECHSLUNG. BRK-Pfade (_check_auto_squeeze, _manage_squeeze_pending,
_check_pending_fill) duerfen den SLOT-ZUSTAND von `self.trader` nicht
anfassen (close/snapshot/open_*/set_open_context/refresh). ⚠ Bewusst eine
WEISSE Liste der Pfade - eine schwarze wuerde jede neue Funktion still
durchlassen. `pending_orders`/`cancel_pending`/`place_stop` sind ausdruecklich
ERLAUBT: magic-basierte Broker-Abfragen ohne Slot-Zustand (geprueft).
Dazu die Invariante, dass `TradingEngine.close` beide Slots erwaehnen muss -
sonst waere eine BRK-Position per Dashboard nicht schliessbar.
DREI RICHTUNGEN VERIFIZIERT: sauber -> gruen; Reihenfolge-Fehler zurueckgebaut ->
"self.trader_brk benutzt, aber erst in Zeile 270 zugewiesen"; Slot-Verwechslung
zurueckgebaut -> "self.trader.close im BRK-Pfad". Danach wieder gruen.
Statisch (AST), kein MT5, Millisekunden. Werkzeugfehler gilt NICHT als sauber.
Laeuft ueber check_nfalle.py im pre-commit-Hook mit.
⚠ Was diese Stufe NICHT kann: ein Startfehler AUSSERHALB von __init__. Dafuer
bliebe ein echter Rauchtest mit gestubbtem MT5 - der ist nicht gebaut.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
461 lines
21 KiB
Python
461 lines
21 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 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")
|
|
|
|
|
|
_stufe_typen()
|
|
_stufe_render()
|
|
_stufe_slots()
|
|
|
|
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)
|