User: "baue alle und integriere in die pipeline vor dem commit". Beide Stufen
schliessen Luecken, die HEUTE real zugeschlagen haben.
A3) TYPPRUEFUNG (ty 0.0.73, Konfiguration in ty.toml)
Anlass: `paarweise(d)` wurde mit 1 von 2 Pflichtargumenten gerufen. py_compile
sah nichts (Syntax ok), ruff F821 auch nicht (der Name IST definiert), kein Test
deckte den Pfad ab - der Fehler schlug erst zur Laufzeit zu.
⚠ BEWUSST ENGES REGELSET: der volle ty-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 (die Lehre vom
07.08.). Aktiv sind nur die Aritaets-Regeln. `call-non-callable` und
`not-iterable` sind AUS - beide melden hier ausschliesslich Falsch-Positive aus
heterogenen dicts (`c["have"]()`, `"up" in c`); geprueft, nicht angenommen.
⚠ Der dokumentierte Einwand "Pruefumgebung != Laufumgebung" greift NICHT: ein
Typchecker fuehrt nichts aus, er kann also nicht divergieren. Er braucht auch
kein MT5.
⚠ Nebenbefund: die CLAUDE.md-Begruendung gegen mypy ("kaum Annotationen
vorhanden") ist veraltet - gemessen sind 305 von 443 Funktionen annotiert (69 %).
B2) RENDER-PROBE (tools/render_check.mjs gegen tests/fixtures/snapshot.json)
Anlass: `node --check` prueft nur SYNTAX. Der ReferenceError von heute Abend
(RSI-Zeile griff in renderM15(s) auf d.market zu) war syntaktisch einwandfrei
und brach im Browser den GESAMTEN Render ab - sechs leere Zeilen und eine leere
Breakout-Karte. Fuer Python gibt es ruff F821, fuer JS gab es nichts.
⚠ Laeuft gegen einen GESPEICHERTEN Snapshot, nicht gegen den laufenden Server -
der Hook darf nicht davon abhaengen, ob gerade ein Server laeuft.
⚠ KEIN ESLint: das braeuchte npm und node_modules, gegen die dokumentierte
Entscheidung ("kein npm, keine package.json").
BEIDE RICHTUNGEN VERIFIZIERT, je Stufe:
A3 Aritaetsfehler eingebaut -> "FEHLER too-many-positional-arguments",
1 Befund; zurueck -> gruen
B2 d.market zurueckgebaut -> "❌ EXCEPTION: d is not defined",
1 Befund; zurueck -> gruen
Beide haengen an tools/check_nfalle.py und laufen damit im pre-commit-Hook mit.
Fehlt das Werkzeug (ty nicht installiert, node nicht im PATH, Fixture fehlt),
wird die Stufe uebersprungen statt zu blockieren - eine frische Maschine darf am
Hook nicht haengenbleiben. Ein WERKZEUGFEHLER (Exit != 0/1) gilt dagegen
ausdruecklich NICHT als sauber.
ty in requirements-dev.txt gepinnt.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
432 lines
19 KiB
Python
432 lines
19 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)
|
|
_stufe_typen()
|
|
_stufe_render()
|
|
|
|
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)
|