Files
claude-regelwerk/.vault-tools/plan-kopie/rot_nachweis.py
T
Pascal MichelandClaude Opus 5 f86a54a6e7 Automatischer Push: 46 Datei(en) geändert
- .claude/agents/planer.md
- .claude/skills/plan-executer-pruefung/SKILL.md
- .claude/workflows/plankritik.js
- .claude/workflows/planung.js
- .claude/workflows/pruefung.js
- .claude/workflows/verbrauch.js
- .claude/workflows/welle-mit-pruefung.js
- .vault-tools/plan-kopie/LIESMICH.md
- .vault-tools/plan-kopie/plan_zeichen.py
- .vault-tools/plan-kopie/rot_nachweis.py
- .vault-tools/trading-plattform-pruefung/LIESMICH.md
- .vault-tools/trading-plattform-pruefung/abendkontrolle.py
- … und 34 weitere

Erzeugt vom PostToolUse-Hook .vault-tools/auto-push/auto_push.py.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-25 04:00:02 +02:00

329 lines
19 KiB
Python

"""Rot-Nachweis je Task: die neuen Tests eines Tasks gegen den Produktivcode VOR dem Task, auf Wegwerfkopien.
Koordinator-Werkzeug (entstanden 2026-09-18, Trading Plattform). plan_materialisiere.py wendet eine
Taskdatei immer ganz an; "erst der Test, dann die Umsetzung" laesst sich damit nicht beobachten. Dieses
Skript stellt den Zustand dazwischen her: Basis + Voraussetzungstasks + der Task selbst, danach die
PRODUKTIVEN Zieldateien des Tasks zurueck auf den Stand vor dem Task (neue Produktivdateien geloescht).
Dann laufen die Testdateien des Tasks. Es tut etwas und berichtet; das Urteil faellt der Koordinator:
ROT mindestens ein Fehlschlag oder Sammelfehler -- die erste Fehlerzeile steht dabei, damit man sieht,
ob der Test an der gemeinten Stelle faellt und nicht an einem Tippfehler.
GRUEN kein Fehlschlag ohne die Umsetzung -- ein Test, der so gruen ist, misst die Umsetzung nicht
(ausser der Plan fuehrt ihn ausdruecklich als Regressionsnetz).
Geltungsbereich und Luecken:
- Testdateien sind Zieldateien unter tests/ sowie *.test.ts und *.test.tsx; alles unter tests/fixtures/
gilt als Testseite und bleibt stehen. Alles andere ist produktiv.
- tests/conftest.py gilt als PRODUKTIVSEITE und wird zurueckgesetzt (Projektvorgabe seit 2026-09-22):
Liegt die Korrektur eines Tasks selbst in conftest.py (Trading Plattform, Task 369: die Ersatzfunktionen
der Netzsperre), wendete das Skript sie im Lauf "ohne Umsetzung" mit an und meldete GRUEN -- der eine
Zustand, in dem der Nachweis nichts misst, sah aus wie ein sauberer Befund. Liegt dagegen eine neue
Fixture des Tasks in conftest.py (Task 368), fehlt sie jetzt im Lauf und die neuen Tests enden als
Sammelfehler -- das ist ROT mit "fixture ... not found" in der ersten Fehlerzeile, also sichtbar.
Wer diesen Fall hat, ruft mit --conftest-als-test auf; dann gilt conftest.py wie frueher als Testdatei.
Das Skript sagt je Task, wie es conftest.py behandelt hat.
- Python-Tests laufen in WSL (Debian, Venv aus --venv), Oberflaechentests unter Windows mit npx vitest im
Ordner aus --frontend; dafuer setzt das Skript eine Junction auf das node_modules des Projekts und loest
sie vor dem Loeschen. Die Vorgaben der drei Schalter sind die der Trading Plattform (~/venvs/trading,
dashboard, trading-plattform/); jedes andere Projekt nennt seine Werte (seit 2026-09-24, Nexhorizons Hub).
Fehlt das Venv in WSL oder der Frontend-Ordner in der Kopie, steht beim Task VORAUSSETZUNG FEHLT statt
ROT -- sonst saehe ein Lauf im falschen Venv aus wie ein sauberer Rot-Nachweis. Dasselbe gilt, wenn die
erste pytest-Fehlerzeile ein Modul vermisst, das weder im Projekt liegt noch vom Task angelegt wird
(vergessenes --venv, eine Abhaengigkeit, die nur der Stack hat). Das greift nur an der ersten
Fehlerzeile; ein fremdes Modul, das erst hinter einem Projektmodul fehlt, sieht das Skript nicht.
- Vorbedingung: Das Projekt ist im Venv NICHT editierbar installiert (pip install -e). Sonst loest der
Finder von setuptools ein fehlendes Untermodul gegen das echte Projekt auf, und ein Test ohne Umsetzung
liefe gruen (Pruefung 2026-09-24, setuptools editable_wheel.py).
- Erzeugte Fixtures (der Plan laesst sie mit altem Code erzeugen) holt --fixturen aus einem Ordner nach,
wenn sie nach dem Anwenden fehlen.
- Es sieht nicht, WELCHE Tests rot sein muessten; es zaehlt und zeigt die erste Fehlerzeile.
- Die Basis ist eine Kopie, die der Aufrufer VOR der ersten Welle gezogen hat; das Skript fasst das
Projekt selbst nie an.
Aufruf: python rot_nachweis.py <basis-kopie> <planordner> <arbeitsordner> <spec.json> [--fixturen <ordner>] [--node-modules <pfad>] [--conftest-als-test] [--venv <wsl-pfad>] [--frontend <ordner>] [--projektname <ordnername>]
--node-modules nennt das node_modules des Projekts, wenn die Basis-Kopie keines traegt.
--venv nennt das Venv in WSL (Vorgabe ~/venvs/trading), --frontend den Oberflaechenordner relativ zum
Projekt (Vorgabe dashboard), --projektname den Ordnernamen, hinter dem absolute Zielpfade abgeschnitten
werden (Vorgabe trading-plattform).
--conftest-als-test laesst tests/conftest.py als Testdatei stehen (nur, wenn die neue Fixture des Tasks dort liegt).
spec.json: [{"task": "task-141-....md", "vor": ["task-...md", ...]}, ...]
"""
import json
import os
import re
import shlex
import shutil
import subprocess
import sys
from pathlib import Path
HIER = Path(__file__).resolve().parent
ZIEL = re.compile(r"^- `(.+?)`")
PROJEKTNAME = "trading-plattform/"
VENV = "~/venvs/trading"
FRONTEND = "dashboard"
CONFTEST = "tests/conftest.py"
# Das Venv steht ungequotet im bash-Aufruf, damit ~ expandiert; deshalb nur diese Zeichen.
VENV_MUSTER = re.compile(r"^~?[A-Za-z0-9._/-]+\Z")
FRONTEND_MUSTER = re.compile(r"^[A-Za-z0-9._-]+(/[A-Za-z0-9._-]+)*\Z")
FEHLENDES_MODUL = re.compile(r"No module named '([^']+)'")
def zieldateien(taskdatei: Path, projektname: str = PROJEKTNAME) -> list[str]:
"""Zieldateien relativ zum Projekt. Absolute Pfade werden hinter dem Projektordner abgeschnitten."""
drin = False
ziele: list[str] = []
for zeile in taskdatei.read_text(encoding="utf-8").replace("\r\n", "\n").split("\n"):
if zeile.startswith("## "):
drin = zeile.startswith("## Zieldateien")
continue
treffer = ZIEL.match(zeile) if drin else None
if treffer:
pfad = treffer.group(1).replace("\\", "/").removeprefix("./")
if re.match(r"^[A-Za-z]:/", pfad):
stelle = pfad.lower().find(projektname)
if stelle < 0:
raise ValueError(f"{taskdatei.name}: absoluter Pfad ausserhalb des Projekts: {pfad}")
pfad = pfad[stelle + len(projektname) :]
ziele.append(pfad)
return ziele
def ist_test(pfad: str, conftest_als_test: bool = False) -> bool:
if pfad == CONFTEST:
# Vorgabe: produktiv. Sonst wendet der Lauf "ohne Umsetzung" eine Korrektur an, die in
# conftest.py liegt, und meldet gruen (siehe Kopfkommentar).
return conftest_als_test
return pfad.startswith("tests/") or pfad.endswith((".test.ts", ".test.tsx"))
def wende_an(planordner: Path, kopie: Path, tasks: list[str]) -> str:
if not tasks:
return ""
lauf = subprocess.run(
[sys.executable, "-X", "utf8", str(HIER / "plan_materialisiere.py"), str(planordner), str(kopie), *tasks],
capture_output=True, text=True, encoding="utf-8", errors="replace",
)
return "" if lauf.returncode == 0 else (lauf.stdout + lauf.stderr)[-600:]
def wsl_pfad(pfad: Path) -> str:
text = str(pfad.resolve()).replace("\\", "/")
return f"/mnt/{text[0].lower()}{text[2:]}"
def loesche(ordner: Path, frontend: str = FRONTEND) -> None:
junction = ordner / frontend / "node_modules"
if junction.exists():
os.rmdir(junction) # loest nur die Verknuepfung, nie das Ziel
shutil.rmtree(ordner, ignore_errors=True)
def pytest_befehl(rot: Path, python_tests: list[str], venv: str = VENV) -> str:
# shlex.quote: Ein Arbeitsordner mit Leerzeichen (Benutzername im Temp-Pfad) liess das cd
# scheitern; jeder Task stand dann als "ROT pytest: keine Summenzeile" da, ohne dass ein Test
# lief (gefunden 2026-09-20).
return f"cd {shlex.quote(wsl_pfad(rot))} && {venv}/bin/python -m pytest {' '.join(python_tests)} -q -p no:cacheprovider 2>&1 | tail -60"
def venv_da(venv: str) -> bool:
lauf = subprocess.run(["wsl.exe", "-d", "Debian", "--", "bash", "-lc", f"test -x {venv}/bin/python && echo VENV_DA"],
capture_output=True, text=True, encoding="utf-8", errors="replace")
return "VENV_DA" in lauf.stdout
def fremdes_modul(fehlerzeile: str, ziele: list[str], kopie: Path) -> str | None:
"""Das fehlende Modul, wenn es weder im Projekt liegt noch vom Task angelegt wird.
Ein solcher Fehler misst das Venv, nicht die fehlende Umsetzung: ein vergessenes --venv
(No module named 'argon2' im Trading-Venv) oder eine Abhaengigkeit, die nur der Stack hat
(playwright). Gefunden 2026-09-24 in der Pruefung des Werkzeugs.
"""
treffer = FEHLENDES_MODUL.search(fehlerzeile)
if not treffer:
return None
oben = treffer.group(1).split(".")[0]
eigene = set()
for ziel in ziele:
eigene.update(Path(ziel).parts[:-1])
eigene.add(Path(ziel).stem)
if oben in eigene or any(kopie.rglob(f"{oben}.py")) or any(p.is_dir() for p in kopie.rglob(oben)):
return None
return treffer.group(1)
def nimm_wert(argumente: list[str], schalter: str) -> str | None:
if schalter not in argumente:
return None
stelle = argumente.index(schalter)
if stelle + 1 >= len(argumente) or argumente[stelle + 1].startswith("--"):
raise ValueError(f"{schalter} ohne Wert")
wert = argumente[stelle + 1]
del argumente[stelle : stelle + 2]
return wert
def erste_fehlerzeile(ausgabe: str) -> str:
for zeile in ausgabe.splitlines():
kern = zeile.strip()
if kern.startswith(("E ", "FAILED", "ERROR", "AssertionError", "Error:", "TypeError", "ReferenceError")) or " FAIL " in zeile:
return kern[:220]
return ""
def main() -> int:
argumente = sys.argv[1:]
try:
fixturen_arg = nimm_wert(argumente, "--fixturen")
node_modules_wert = nimm_wert(argumente, "--node-modules")
venv = nimm_wert(argumente, "--venv") or VENV
frontend = (nimm_wert(argumente, "--frontend") or FRONTEND).replace("\\", "/").strip("/")
projektname = (nimm_wert(argumente, "--projektname") or PROJEKTNAME).replace("\\", "/").strip("/").lower() + "/"
except ValueError as fehler:
print(f"AUFRUF FALSCH: {fehler}")
return 3
fixturen = Path(fixturen_arg) if fixturen_arg else None
node_modules_arg = Path(node_modules_wert) if node_modules_wert else None
if not VENV_MUSTER.match(venv):
print(f"AUFRUF FALSCH: --venv {venv!r} -- erlaubt sind nur ~, Buchstaben, Ziffern und ._/-")
return 3
if not FRONTEND_MUSTER.match(frontend) or ".." in frontend.split("/"):
print(f"AUFRUF FALSCH: --frontend {frontend!r} -- ein Ordner relativ zum Projekt, ohne ..")
return 3
conftest_als_test = False
if "--conftest-als-test" in argumente:
conftest_als_test = True
argumente.remove("--conftest-als-test")
if len(argumente) != 4:
print(__doc__)
return 3
print(f"Einstellungen: venv={venv} frontend={frontend} projektname={projektname}")
basis, planordner, arbeit, spec_pfad = (Path(a) for a in argumente)
if not basis.is_dir() or not planordner.is_dir() or not spec_pfad.is_file():
print("VORAUSSETZUNG FEHLT: Basis-Kopie, Planordner oder Spec")
return 3
arbeit.mkdir(parents=True, exist_ok=True)
# Den langen Namen erzwingen: Kommt der Arbeitsordner als Windows-Kurzname (PASCAL~1), startet
# vitest dort, loest die Dateien aber ueber den langen Namen auf und meldet "Cannot find module"
# fuer die eigene Testdatei -- ein ROT, das nichts misst (2026-09-24, Hub-Tasks 24, 25, 29, 37
# und Trading-Task 395).
arbeit = arbeit.resolve()
node_modules = None
for kandidat in (node_modules_arg, basis / frontend / "node_modules"):
if kandidat is not None and kandidat.exists():
node_modules = kandidat
break
venv_geprueft: bool | None = None # erst pruefen, wenn ein Task Python-Tests hat
ergebnisse = []
for eintrag in json.loads(spec_pfad.read_text(encoding="utf-8")):
task, voraus = eintrag["task"], eintrag.get("vor", [])
name = task.split("-")[1] if "-" in task else task
vor, rot = arbeit / f"{name}_vor", arbeit / f"{name}_rot"
for ordner in (vor, rot):
loesche(ordner, frontend)
shutil.copytree(basis, vor, ignore=shutil.ignore_patterns("node_modules", "__pycache__"))
fehler = wende_an(planordner, vor, voraus)
shutil.copytree(vor, rot)
fehler = fehler or wende_an(planordner, rot, [task])
if fehler:
ergebnisse.append((task, "ANWENDEN GESCHEITERT", fehler.replace("\n", " | ")))
print(f"{task}: ANWENDEN GESCHEITERT: {fehler[-300:]}")
continue
ziele = zieldateien(planordner / task, projektname)
tests = [z for z in ziele if ist_test(z, conftest_als_test) and not z.startswith("tests/fixtures/")]
if CONFTEST in ziele:
# Sichtbar machen, was mit conftest.py geschah -- der Koordinator liest die erste Fehlerzeile
# anders, wenn eine Fixture fehlt, als wenn eine Korrektur zurueckgesetzt wurde.
wie = "als Testdatei stehen gelassen (--conftest-als-test)" if conftest_als_test else "als Produktivseite auf den Stand vor dem Task zurueckgesetzt"
print(f"{task}: Hinweis: {CONFTEST} {wie}")
for ziel in ziele:
if ist_test(ziel, conftest_als_test):
if not (rot / ziel).exists() and fixturen is not None and (fixturen / Path(ziel).name).is_file():
(rot / ziel).parent.mkdir(parents=True, exist_ok=True)
shutil.copyfile(fixturen / Path(ziel).name, rot / ziel)
continue
if (vor / ziel).is_file():
shutil.copyfile(vor / ziel, rot / ziel)
elif (rot / ziel).is_file():
(rot / ziel).unlink()
# Ordner, die erst der Task angelegt hat, gehen mit, sobald sie leer sind: Ein leeres
# hub/auth/ liest Python als Namespace-Paket, und die Fehlerzeile nannte dann das
# Untermodul statt des fehlenden Pakets (2026-09-24, Nexhorizons Hub, Tasks 7, 9, 11).
ordner = (rot / ziel).parent
while ordner != rot and not (vor / ordner.relative_to(rot)).exists() and not any(ordner.iterdir()):
ordner.rmdir()
ordner = ordner.parent
python_tests = [t for t in tests if t.endswith(".py")]
ober_tests = [t for t in tests if not t.endswith(".py")]
zeilen = []
if python_tests and venv_geprueft is None:
venv_geprueft = venv_da(venv)
if python_tests and not venv_geprueft:
zeilen.append(f"VORAUSSETZUNG FEHLT: {venv}/bin/python ist in WSL nicht ausfuehrbar")
elif python_tests:
befehl = pytest_befehl(rot, python_tests, venv)
lauf = subprocess.run(["wsl.exe", "-d", "Debian", "--", "bash", "-lc", befehl],
capture_output=True, text=True, encoding="utf-8", errors="replace")
ausgabe = lauf.stdout
summe = next((z.strip() for z in reversed(ausgabe.splitlines()) if re.search(r"\d+ (passed|failed|error)", z)), "keine Summenzeile")
# Gruen ist nur ein Lauf mit Summenzeile und ohne Fehlschlag. Ohne Summenzeile hat pytest gar
# nicht gesammelt (etwa ein ImportError in tests/conftest.py) -- das ist rot, nicht gruen.
# Gefunden am 2026-09-18: Task 172 stand als GRUEN da, obwohl kein einziger Test lief.
rot_ja = bool(re.search(r"\d+ (failed|error)", summe)) or summe == "keine Summenzeile"
erste = erste_fehlerzeile(ausgabe)
fremd = fremdes_modul(erste, ziele, rot) if rot_ja else None
if fremd:
zeilen.append(f"VORAUSSETZUNG FEHLT: Modul {fremd!r} liegt weder im Projekt noch im Venv {venv} | pytest: {summe} | {erste}")
else:
zeilen.append(("ROT " if rot_ja else "GRUEN") + f" pytest: {summe} | {erste}")
if ober_tests:
if node_modules is None:
zeilen.append("VORAUSSETZUNG FEHLT: node_modules in der Basis-Kopie")
elif not (rot / frontend).is_dir():
zeilen.append(f"VORAUSSETZUNG FEHLT: Frontend-Ordner {frontend} fehlt in der Kopie")
else:
import _winapi
_winapi.CreateJunction(str(node_modules.resolve()), str(rot / frontend / "node_modules"))
relativ = [t.removeprefix(f"{frontend}/") for t in ober_tests]
lauf = subprocess.run(["npx.cmd", "vitest", "run", *relativ], cwd=rot / frontend,
capture_output=True, text=True, encoding="utf-8", errors="replace")
ausgabe = re.sub(r"\x1b\[[0-9;]*m", "", lauf.stdout + lauf.stderr)
summe = next((z.strip() for z in ausgabe.splitlines() if z.strip().startswith("Tests ")), "")
if not summe:
# Ohne Summenzeile ist vitest nicht bis zum Ende gekommen (Importfehler, Startfehler):
# das Ende der Ausgabe zeigen, statt "rot" zu behaupten, ohne zu wissen woran.
summe = "keine Summenzeile; Ende der Ausgabe: " + " / ".join(z.strip() for z in ausgabe.splitlines()[-6:] if z.strip())[:300]
rot_ja = "failed" in summe or lauf.returncode != 0
if "Denied ID" in ausgabe:
# Vite liefert keine Datei ausserhalb seines Wurzelordners, und durch die Junction
# liegt node_modules ausserhalb der Kopie: Ein Test mit "?raw" aus node_modules
# scheitert dann als ganze Datei (2026-09-24, Nexhorizons Hub, Task 42).
zeilen.append(f"VORAUSSETZUNG FEHLT: Vite verweigert eine Datei hinter der Junction auf node_modules; mit echter Kopie von node_modules fahren | vitest: {summe}")
else:
# Bei einer Datei, die schon am Import scheitert, steht vor der Fehlermeldung nur
# die FAIL-Zeile; die Meldung selbst zeigt erst, ob der Grund der gemeinte ist.
erste = erste_fehlerzeile(ausgabe)
meldung = next((z.strip()[:220] for z in ausgabe.splitlines()
if z.strip().startswith(("Error:", "AssertionError", "TypeError", "ReferenceError", "SyntaxError"))), "")
zusatz = f" | {meldung}" if meldung and meldung not in erste else ""
zeilen.append(("ROT " if rot_ja else "GRUEN") + f" vitest: {summe} | {erste}{zusatz}")
if not tests:
zeilen.append("ohne Testdatei unter den Zieldateien")
for zeile in zeilen:
print(f"{task}: {zeile}")
ergebnisse.append((task, zeile[:5].strip(), zeile))
for ordner in (vor, rot):
loesche(ordner, frontend)
gruen = [e for e in ergebnisse if e[1] == "GRUEN"]
ohne_lauf = [e for e in ergebnisse if e[1] in ("VORAU", "ANWENDEN GESCHEITERT")]
print(f"\n{len(ergebnisse)} Laeufe, davon {len(gruen)} gruen ohne Umsetzung und {len(ohne_lauf)} ohne Lauf "
"(Voraussetzung fehlt oder Anwenden gescheitert).")
return 0
if __name__ == "__main__":
try:
sys.exit(main())
except Exception as ausnahme: # noqa: BLE001 - ein Absturz des Werkzeugs ist selbst ein Befund
print(f"BEFUND: das Werkzeug ist abgebrochen: {ausnahme!r}")
sys.exit(2)