Automatischer Push: 19 Datei(en) geändert

- .vault-tools/plan-kopie/LIESMICH.md
- .vault-tools/plan-kopie/rot_nachweis.py
- .vault-tools/plan-kopie/welle_vergleich.py
- .vault-tools/plan-kopie/welle_vergleich_treiber.py
- .vault-tools/trading-plattform-pruefung/LIESMICH.md
- .vault-tools/trading-plattform-pruefung/angriffe.py
- .vault-tools/trading-plattform-pruefung/browserlauf.py
- .vault-tools/trading-plattform-pruefung/browserlauf_zugang.py
- .vault-tools/trading-plattform-pruefung/c_eigenlauf.py
- .vault-tools/trading-plattform-pruefung/d_eigenlauf.py
- .vault-tools/trading-plattform-pruefung/handelstag_auswertung.py
- .vault-tools/trading-plattform-pruefung/ntfy_probe_server.py
- … und 7 weitere

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

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
Pascal MichelandClaude Opus 5 committed 2026-09-19 04:00:01 +02:00
1 parent 371a1f5d2a
commit 395642c480
19 files changed
+1636 -32

No files matched your search

+4
View File
@@ -21,8 +21,12 @@ Die Regel dazu steht in [[AI/Regelwerk/codeworkflow|codeworkflow.md]] („Die Ko
5. **Nach jeder Welle:** `welle_vergleich.py --projekt <projekt> --mat <kopie> [--prosa <rel> …] -- <taskdateien der Welle>`. Prüft, ob genau die Zieldateien geändert sind und ob jede byte-gleich mit der Kopie ist. Es zeigt je Datei nur die erste Abweichung — danach `diff` auf die ganze Datei. Eigene Plankorrekturen im Baum meldet es zu Recht als „außerhalb der Zieldateien".
6. **Escape-Folgen setzen:** `plan_escape.py <datei> <zeile> <codepunkt-hex> …` ersetzt echte Zeichen in einer Zeile durch ihre Escape-Folge, ohne dass eine Schreibschicht mitredet.
7. **Rot-Nachweis je Task (seit 2026-09-18):** `rot_nachweis.py <basis-kopie> <planordner> <arbeitsordner> <spec.json> [--fixturen <ordner>] [--node-modules <pfad>]`. `plan_materialisiere.py` wendet eine Taskdatei immer ganz an; „erst der Test, dann die Umsetzung" lässt sich damit nicht beobachten. Das Skript stellt den Zustand dazwischen auf Wegwerfkopien her — Basis plus Voraussetzungstasks plus der Task, danach die produktiven Zieldateien zurück auf den Stand vor dem Task — und fährt die Testdateien des Tasks (pytest in WSL, vitest unter Windows über eine Junction, die es vor dem Löschen löst). Es meldet je Task `ROT` mit der ersten Fehlerzeile oder `GRUEN`; ein Test, der ohne Umsetzung grün ist, misst sie nicht. Die Basis-Kopie zieht man **vor** der ersten Welle. `spec.json` nennt je Task die Voraussetzungstasks. Erster Einsatz am 2026-09-18 an vier Plänen der Trading Plattform: 35 Tasks, alle rot an der gemeinten Stelle, keiner grün ohne Umsetzung. Die Executer brauchen den Tanz mit dem Beiseitelegen der Produktivdatei dann nicht mehr; ihr Auftrag sagt das ausdrücklich.
## Was belegt ist
Seit 2026-09-18 nimmt `welle_vergleich.py` unter `## Zieldateien` auch Pfade relativ zum Projekt (ohne Laufwerk und ohne Wurzel), weil Pläne für einen Git-Worktree keine absoluten Pfade nennen; ein `..` darin ist ein Befund wie ein fremder absoluter Pfad. Jede Zeile braucht weiter die Klammer hinter dem Pfad (`(neu)`, `(geändert)`, `(gelöscht)`). `plan_materialisiere.py` erkennt eine Zieldatei ohne Schrägstrich nur mit den Endungen py, ts, tsx, toml, md, css und json — für `compose.yaml` in der Projektwurzel schreibt die Überschrift `./compose.yaml`. Der Treiber fährt dazu drei weitere Fälle (relative Grundlinie, relative Abweichung, `..` nach außen), zusammen 17.
`welle_vergleich_treiber.py` fährt 14 Angriffsfälle gegen `welle_vergleich.py` (Grundlinie zuerst; fremde Datei, unveränderte Zieldatei, Abweichung in erster und letzter Zeile und in der letzten Zieldatei, CRLF, Datei fehlt in der Kopie, Task ohne Zieldateien, Kopie fehlt, Löschung nicht ausgeführt, Prosa-Datei unverändert, Zieldatei außerhalb des Projekts, Datei länger als der Plan). `plan_zeichen.py` hat am 2026-09-17 an einem echten Fund rot gezeigt (U+2028 im Plan). `plan_materialisiere.py` reproduziert Task 108 des Teil-3b-Plans aus dem Stand vor den Wellen byte-genau. `kopie_bauen.py` ist am 2026-09-17 an drei kaputten Eingaben rot gesehen: Ziel existiert schon (Exit 2, nichts kopiert), Ausschluss `node_modules` ohne Gegenstück im Wurzelordner (Befund), und ein aus der Kopie entferntes Paket `trading/data` (die Paketprüfung nennt es; nach dem Zurücklegen leer). Einen Treiber dazu gibt es noch nicht — die drei Fälle sind von Hand gefahren.
## Was die Skripte nicht sehen
+195
View File
@@ -0,0 +1,195 @@
"""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.
- Python-Tests laufen in WSL (Debian, ~/venvs/trading), Oberflaechentests unter Windows mit npx vitest;
dafuer setzt das Skript eine Junction auf das node_modules des Projekts und loest sie vor dem Loeschen.
- 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>]
--node-modules nennt das node_modules des Projekts, wenn die Basis-Kopie keines traegt.
spec.json: [{"task": "task-141-....md", "vor": ["task-...md", ...]}, ...]
"""
import json
import os
import re
import shutil
import subprocess
import sys
from pathlib import Path
HIER = Path(__file__).resolve().parent
ZIEL = re.compile(r"^- `(.+?)`")
PROJEKTNAME = "trading-plattform/"
def zieldateien(taskdatei: Path) -> 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) -> bool:
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) -> None:
junction = ordner / "dashboard" / "node_modules"
if junction.exists():
os.rmdir(junction) # loest nur die Verknuepfung, nie das Ziel
shutil.rmtree(ordner, ignore_errors=True)
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:]
fixturen = None
if "--fixturen" in argumente:
stelle = argumente.index("--fixturen")
fixturen = Path(argumente[stelle + 1])
del argumente[stelle : stelle + 2]
node_modules_arg = None
if "--node-modules" in argumente:
stelle = argumente.index("--node-modules")
node_modules_arg = Path(argumente[stelle + 1])
del argumente[stelle : stelle + 2]
if len(argumente) != 4:
print(__doc__)
return 3
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)
node_modules = None
for kandidat in (node_modules_arg, basis / "dashboard" / "node_modules"):
if kandidat is not None and kandidat.exists():
node_modules = kandidat
break
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)
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)
tests = [z for z in ziele if ist_test(z) and not z.startswith("tests/fixtures/")]
for ziel in ziele:
if ist_test(ziel):
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()
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:
befehl = f"cd {wsl_pfad(rot)} && ~/venvs/trading/bin/python -m pytest {' '.join(python_tests)} -q -p no:cacheprovider 2>&1 | tail -60"
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"
zeilen.append(("ROT " if rot_ja else "GRUEN") + f" pytest: {summe} | {erste_fehlerzeile(ausgabe)}")
if ober_tests:
if node_modules is None:
zeilen.append("VORAUSSETZUNG FEHLT: node_modules in der Basis-Kopie")
else:
import _winapi
_winapi.CreateJunction(str(node_modules.resolve()), str(rot / "dashboard" / "node_modules"))
relativ = [t.removeprefix("dashboard/") for t in ober_tests]
lauf = subprocess.run(["npx.cmd", "vitest", "run", *relativ], cwd=rot / "dashboard",
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
zeilen.append(("ROT " if rot_ja else "GRUEN") + f" vitest: {summe} | {erste_fehlerzeile(ausgabe)}")
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)
gruen = [e for e in ergebnisse if e[1] == "GRUEN"]
print(f"\n{len(ergebnisse)} Laeufe, davon {len(gruen)} gruen ohne Umsetzung.")
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)
+19 -7
View File
@@ -23,7 +23,9 @@ import sys
import traceback
from pathlib import Path, PureWindowsPath
ZIEL = re.compile(r"^- `(.+?)`\s*\((.*)\)\s*$")
# Seit 2026-09-18 ist die Klammer hinter dem Pfad wahlfrei, und dahinter darf Text stehen
# ("- `pfad` -- nur die Konstante X"): Plaene nennen Zieldateien nicht immer mit "(neu)".
ZIEL = re.compile(r"^- `(.+?)`(?:\s*\((.*?)\))?")
def zieldateien(taskdatei: Path, projekt: Path, befunde: list[str]) -> dict[str, bool]:
@@ -45,12 +47,22 @@ def zieldateien(taskdatei: Path, projekt: Path, befunde: list[str]) -> dict[str,
if not treffer:
continue
absolut = PureWindowsPath(treffer.group(1))
try:
rel = absolut.relative_to(PureWindowsPath(str(projekt)))
except ValueError:
befunde.append(f"{taskdatei.name}: Zieldatei {absolut} liegt nicht unter dem Projekt {projekt}")
continue
ziele[rel.as_posix()] = "gelöscht" in treffer.group(2) or "geloescht" in treffer.group(2)
if absolut.drive == "" and absolut.root == "":
# Seit 2026-09-18: Ein Pfad ohne Laufwerk und ohne Wurzel gilt relativ zum Projekt
# (Plaene fuer einen Git-Worktree nennen keine absoluten Pfade). Ein ".." fuehrte
# aus dem Projekt hinaus und ist ein Befund wie ein fremder absoluter Pfad.
if ".." in absolut.parts:
befunde.append(f"{taskdatei.name}: Zieldatei {absolut} liegt nicht unter dem Projekt {projekt}")
continue
rel = absolut
else:
try:
rel = absolut.relative_to(PureWindowsPath(str(projekt)))
except ValueError:
befunde.append(f"{taskdatei.name}: Zieldatei {absolut} liegt nicht unter dem Projekt {projekt}")
continue
klammer = treffer.group(2) or ""
ziele[rel.as_posix()] = "gelöscht" in klammer or "geloescht" in klammer
if not ziele:
befunde.append(f"{taskdatei.name}: keine Zieldateien gefunden -- ohne sie misst der Lauf nichts")
return ziele
@@ -114,6 +114,43 @@ def fall_laengere_datei(projekt, mat, task, wurzel):
schreibe(projekt / "a.py", "EINS\nzwei\nDREI\nvier\n")
def _relativ(task, projekt):
"""Schreibt die Zieldateien der Taskdatei relativ zum Projekt, eine davon mit Schraegstrich."""
text = task.read_text(encoding="utf-8")
schreibe(task, text.replace(f"{projekt}\\", "").replace("sub\\neu.py", "sub/neu.py"))
def fall_relativ_grundlinie(projekt, mat, task, wurzel):
_relativ(task, projekt)
def fall_relativ_abweichung(projekt, mat, task, wurzel):
_relativ(task, projekt)
schreibe(projekt / "sub" / "neu.py", "neu eins\nneu ZWEI\n")
def _ohne_klammer(task):
"""Nimmt den Zieldateien die Klammer (ausser der Loeschmarke) und haengt einer Zeile Text an."""
text = task.read_text(encoding="utf-8")
text = text.replace("a.py` (geändert)", "a.py` — **nur die erste und die letzte Zeile**")
schreibe(task, text.replace(" (neu)", "").replace("doku.md` (geändert)", "doku.md`"))
def fall_ohne_klammer_grundlinie(projekt, mat, task, wurzel):
_ohne_klammer(task)
def fall_ohne_klammer_abweichung(projekt, mat, task, wurzel):
_ohne_klammer(task)
schreibe(projekt / "a.py", "Eins\nzwei\nDREI\n")
def fall_relativ_nach_aussen(projekt, mat, task, wurzel):
_relativ(task, projekt)
text = task.read_text(encoding="utf-8")
schreibe(task, text.replace("Keine andere Datei", "- `../anderswo/x.py` (neu)\n\nKeine andere Datei"))
FAELLE = [
("Grundlinie", fall_grundlinie, 0, "0 Befund(e)."),
("fremde Datei im Baum", fall_fremde_datei, 2, "fremd.py: geaendert"),
@@ -130,6 +167,13 @@ FAELLE = [
("Zieldatei ausserhalb des Projekts", fall_ziel_ausserhalb, 2, "liegt nicht unter dem Projekt"),
# Die angehaengte Zeile steht der leeren Restzeile der Kopie gegenueber, deshalb "Zeile 4" und nicht "Laenge".
("Datei laenger als der Plan", fall_laengere_datei, 2, "a.py: weicht vom Plan ab -- Zeile 4: Baum b'vier'"),
# Seit 2026-09-18: Zieldateien relativ zum Projekt (Plaene fuer einen Git-Worktree).
("Zieldateien relativ zum Projekt", fall_relativ_grundlinie, 0, "0 Befund(e)."),
("relative Zieldatei weicht ab", fall_relativ_abweichung, 2, "sub/neu.py: weicht vom Plan ab -- Zeile 2"),
("relative Zieldatei fuehrt nach aussen", fall_relativ_nach_aussen, 2, "liegt nicht unter dem Projekt"),
# Seit 2026-09-18: Die Klammer hinter dem Pfad ist wahlfrei, Text dahinter erlaubt.
("Zieldateien ohne Klammer", fall_ohne_klammer_grundlinie, 0, "0 Befund(e)."),
("Zieldatei ohne Klammer weicht ab", fall_ohne_klammer_abweichung, 2, "a.py: weicht vom Plan ab -- Zeile 1"),
]