Files
Pascal MichelandClaude Opus 5.5 5cbe75c5e3 plan-kopie: Zieldatei ohne Endung über ## Zieldateien, sonst Befund statt stillem Fehlziel
Anlass: Plankritik Paket R1 im Edge-Stack, Befunde 1 und 5. Die Überschrift
"Stelle 30 — `host/logrotate/edge-traefik`" setzte kein Ziel; der Block
"Die ganze Datei ist neu" überschrieb tests/systemd/test-units.sh mit Exit 0.

- ordne() in plan_materialisiere.py, von plan_bloecke.py importiert: Ziel ist
  der letzte PFAD-Treffer, ohne ihn der letzte Token, der unter ## Zieldateien
  steht (absolut relativ zu --projekt bzw. zum Ordner vor docs/plans).
- Überschrift ohne Ziel, die nach einer Datei aussieht (relativer Pfad mit /,
  Datei in Kopie/Projekt, Dateiname eines Eintrags): jeder schreibende Block
  danach ist Befund, Exit 2, nichts geschrieben.
- plan_ziel_treiber.py: 33 Fälle; Ursprung 22 rot, jetzt 33 grün;
  21 eingebaute Fehler gefangen.
- Vorher/nachher über 2 223 Plandateien: Abweichung nur R1 task-01 und
  Trading Task 90, beide jetzt ins richtige Ziel.
- LIESMICH und planer.md nachgezogen.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-17 11:10:42 +02:00

247 lines
15 KiB
Python

"""Regressionsnachweis fuer die Zielzuordnung von plan_materialisiere.py und plan_bloecke.py.
Anlass 2026-10-17 (Plankritik von Paket R1 im Edge-Stack, Befunde 1 und 5): Die Ueberschrift
"#### Stelle 30 — `host/logrotate/edge-traefik`" nennt einen Pfad ohne Endung. PFAD erkannte ihn nicht,
das Ziel blieb beim vorigen (`tests/systemd/test-units.sh`), und der Block "Die ganze Datei ist neu"
ueberschrieb es mit Exit 0 und "0 Befund(e)". Die Faelle unten bauen diese Form nach, an erster und an
letzter Stelle, ueber eine pfadlose Unterueberschrift hinweg und fuer jede schreibende Marke, dazu die
Ueberschriften aus bestehenden Plaenen, die kein Befund sein duerfen (Ordner, absoluter Pfad, erkannte
Zieldatei neben einem Pfad ohne Endung, Token mit Leerraum).
Seit dem Nachtrag vom selben Tag ist eine Datei ohne Endung Ziel, wenn sie genau so unter "## Zieldateien"
steht, auch als absoluter Eintrag; ein Name ohne Schraegstrich, der in der Kopie liegt oder der Dateiname
eines Eintrags ist, ist ein Befund (vorher ging `Dockerfile` still ins vorige Ziel). Die Faelle dazu stehen
am Ende der Liste; "{PROJEKT}" in einer Taskdatei ist der absolute Projektordner des Falls.
Grundlinie zuerst, dann jeder Fall einzeln in einem frischen Wegwerfordner. Je Fall werden Exit-Code,
ausloesende Meldung und bei plan_materialisiere.py der Inhalt jeder Datei der Kopie geprueft.
Aufruf: python plan_ziel_treiber.py [--werkzeuge <ordner>] Vorgabe: der Ordner dieses Skripts.
Exit 0, wenn jeder Fall so ausgeht wie erwartet, sonst 2.
"""
import argparse
import shutil
import subprocess
import sys
import tempfile
from pathlib import Path
# Projekt jedes Falls: eine Datei mit Endung, eine ohne, eine Doku, zwei ohne Endung im Wurzelordner
BASIS = {
"tests/a.sh": "eins\nzwei\ndrei\n",
"host/logrotate/edge-traefik": "alt\n",
"README.md": "# Projekt\n\n- host/ Ordner\n",
"Dockerfile": "FROM alt\n",
".gitignore": "data/\n",
}
Z = "```"
def aus_wird(ueberschrift: str, alt: str, neu: str) -> str:
return f"{ueberschrift}\n\nAus:\n\n{Z}text\n{alt}\n{Z}\n\nwird:\n\n{Z}text\n{neu}\n{Z}\n\n"
def ganz(ueberschrift: str, text: str) -> str:
return f"{ueberschrift}\n\nDie ganze Datei ist neu:\n\n{Z}text\n{text}\n{Z}\n\n"
def anhaengen(ueberschrift: str, text: str) -> str:
return f"{ueberschrift}\n\nHäng diesen Block ans Ende der Datei:\n\n{Z}text\n{text}\n{Z}\n\n"
def befehl(ueberschrift: str) -> str:
return f"{ueberschrift}\n\nAufruf:\n\n{Z}bash\nbash tests/.work/r1-01/umgebung.sh\n{Z}\n\n"
def zieldateien(*pfade: str) -> str:
return "# Task 01\n\n## Zieldateien\n\n" + "".join(f"- `{p}` (geändert)\n" for p in pfade) + "\n## Prüfstand\n\n"
KOPF = "# Task 01\n\n## Prüfstand\n\n"
STELLE_A = aus_wird("#### Stelle 1 — `tests/a.sh`", "zwei", "ZWEI")
OHNE = "`host/logrotate/edge-traefik`"
MELDUNG = f"nennt {OHNE} und keine erkannte Zieldatei"
GEPLANT = zieldateien("tests/a.sh", "host/logrotate/edge-traefik")
# Name, Werkzeug, Taskdatei, erwarteter Exit, erwartete Meldung, Zahl der BEFUND-Zeilen,
# erwartete Dateien nach dem Lauf (nur "mat"; jede nicht genannte Datei bleibt wie in BASIS)
FAELLE = [
("Grundlinie", "mat", KOPF + STELLE_A, 0, "0 Befund(e).", 0,
{"tests/a.sh": "eins\nZWEI\ndrei\n"}),
# Die Form aus R1: Pfad ohne Endung nach einem erkannten Ziel (Schaden an letzter Stelle)
("R1-Form: ganze Datei, letzte Stelle", "mat",
KOPF + STELLE_A + ganz(f"#### Stelle 2 — {OHNE}", "neu"), 2, MELDUNG, 1,
{"tests/a.sh": "eins\nZWEI\ndrei\n"}),
# Dieselbe Form als erste Stelle: vorher kein Ziel, der Block ging still als "uebergangen" durch
("ganze Datei, erste Stelle", "mat",
KOPF + ganz(f"#### Stelle 1 — {OHNE}", "neu") + aus_wird("#### Stelle 2 — `tests/a.sh`", "zwei", "ZWEI"), 2, MELDUNG, 1,
{"tests/a.sh": "eins\nZWEI\ndrei\n"}),
("Aus/wird unter Pfad ohne Endung", "mat",
KOPF + aus_wird("#### Stelle 1 — `tests/a.sh`", "eins", "EINS") + aus_wird(f"#### Stelle 2 — {OHNE}", "zwei", "ZWEI"),
2, MELDUNG, 1, {"tests/a.sh": "EINS\nzwei\ndrei\n"}),
("anhaengen unter Pfad ohne Endung", "mat",
KOPF + STELLE_A + anhaengen(f"#### Stelle 2 — {OHNE}", "vier"), 2, MELDUNG, 1,
{"tests/a.sh": "eins\nZWEI\ndrei\n"}),
# Das Ziel bestimmt die letzte Ueberschrift mit Pfad; eine pfadlose Unterueberschrift aendert daran nichts
("ueber pfadlose Unterueberschrift", "mat",
KOPF + STELLE_A + f"### Stelle 2 — {OHNE}\n\n" + ganz("#### Neue Fassung", "neu"), 2, MELDUNG, 1,
{"tests/a.sh": "eins\nZWEI\ndrei\n"}),
("zwei Stellen, zwei Befunde", "mat",
KOPF + ganz(f"#### Stelle 1 — {OHNE}", "neu") + STELLE_A + ganz(f"#### Stelle 3 — {OHNE}", "neu"), 2, "2 Befund(e).", 2,
{"tests/a.sh": "eins\nZWEI\ndrei\n"}),
# Kein Befund: eine Ueberschrift mit erkannter Zieldatei hebt den Pfad ohne Endung davor auf
("Pfad ohne Endung, danach erkanntes Ziel", "mat",
KOPF + f"### Hinweis zu {OHNE}\n\nProsa.\n\n" + STELLE_A, 0, "0 Befund(e).", 0,
{"tests/a.sh": "eins\nZWEI\ndrei\n"}),
("Pfad ohne Endung ueber einem Befehl", "mat",
KOPF + befehl("#### Messordner `tests/.work/r1-01`") + STELLE_A, 0, "0 Befund(e).", 0,
{"tests/a.sh": "eins\nZWEI\ndrei\n"}),
# Kein Befund: Ueberschriften aus bestehenden Plaenen (Paket E2 Task 367, S3 Task 298, NF Task 703)
("Ordner mit Schraegstrich", "mat",
KOPF + STELLE_A + aus_wird("#### Änderung 2 — dass `host/` nicht der einzige Ort ist", "drei", "DREI"), 0, "0 Befund(e).", 0,
{"tests/a.sh": "eins\nZWEI\nDREI\n"}),
("absoluter Pfad", "mat",
KOPF + STELLE_A + aus_wird("#### Änderung 2 — ein Wegwerf-`/proc` statt zweier Dateien", "drei", "DREI"), 0, "0 Befund(e).", 0,
{"tests/a.sh": "eins\nZWEI\nDREI\n"}),
("erkannte Zieldatei neben Pfad ohne Endung", "mat",
KOPF + STELLE_A + aus_wird("#### Änderung in `README.md`: `glattstellen/beginn` ohne Schlüssel", "host/ Ordner", "host/ Ordner, neu"),
0, "0 Befund(e).", 0, {"tests/a.sh": "eins\nZWEI\ndrei\n", "README.md": "# Projekt\n\n- host/ Ordner, neu\n"}),
("Token mit Leerraum", "mat",
KOPF + STELLE_A + aus_wird("#### Endpunkt `GET /api/paper`", "drei", "DREI"), 0, "0 Befund(e).", 0,
{"tests/a.sh": "eins\nZWEI\nDREI\n"}),
# plan_bloecke.py, Modus vor: Der Aus-Block steht zufaellig auch im vorigen Ziel, deshalb vorher still.
# Je Stelle zwei Befunde (Aus und wird); keiner davon darf zusaetzlich gegen das vorige Ziel laufen.
("bloecke: Grundlinie", "vor", KOPF + STELLE_A, 0, "0 Befund(e).", 0, None),
("bloecke: letzte Stelle", "vor",
KOPF + STELLE_A + aus_wird(f"#### Stelle 2 — {OHNE}", "drei", "DREI"), 2, MELDUNG, 2, None),
("bloecke: erste Stelle", "vor",
KOPF + aus_wird(f"#### Stelle 1 — {OHNE}", "drei", "DREI") + STELLE_A, 2, MELDUNG, 2, None),
("bloecke: ueber pfadlose Unterueberschrift", "vor",
KOPF + STELLE_A + f"### Stelle 2 — {OHNE}\n\n" + aus_wird("#### Änderung", "drei", "DREI"), 2, MELDUNG, 2, None),
("bloecke: Ordner, absolut, Leerraum, erkannte Zieldatei", "vor",
KOPF + STELLE_A + aus_wird("#### Änderung 2 — dass `host/` nicht der einzige Ort ist", "drei", "DREI")
+ aus_wird("#### Änderung 3 — Wegwerf-`/proc`", "eins", "EINS")
+ aus_wird("#### Endpunkt `GET /api/paper`", "eins", "EINS")
+ aus_wird("#### Änderung in `README.md`: `glattstellen/beginn`", "host/ Ordner", "host/ Ordner, neu"),
0, "0 Befund(e).", 0, None),
# Modus nach: Der Baum traegt schon den wird-Text im vorigen Ziel
("bloecke nach: letzte Stelle", "nach",
KOPF + aus_wird("#### Stelle 1 — `tests/a.sh`", "eins", "EINS") + aus_wird(f"#### Stelle 2 — {OHNE}", "drei", "DREI"),
2, MELDUNG, 2, None),
# Nachtrag: Datei ohne Endung unter "## Zieldateien" ist Ziel (R1 Stelle 30 mit Eintrag), letzte und erste Stelle
("Zieldateien: ganze Datei ohne Endung, letzte Stelle", "mat",
GEPLANT + STELLE_A + ganz(f"#### Stelle 2 — {OHNE}", "neu"), 0, "0 Befund(e).", 0,
{"tests/a.sh": "eins\nZWEI\ndrei\n", "host/logrotate/edge-traefik": "neu\n"}),
("Zieldateien: ganze Datei ohne Endung, erste Stelle", "mat",
GEPLANT + ganz(f"#### Stelle 1 — {OHNE}", "neu") + aus_wird("#### Stelle 2 — `tests/a.sh`", "zwei", "ZWEI"),
0, "0 Befund(e).", 0, {"tests/a.sh": "eins\nZWEI\ndrei\n", "host/logrotate/edge-traefik": "neu\n"}),
("Zieldateien: Aus/wird in Datei ohne Endung", "mat",
GEPLANT + STELLE_A + aus_wird(f"#### Stelle 2 — {OHNE}", "alt", "ALT"), 0, "0 Befund(e).", 0,
{"tests/a.sh": "eins\nZWEI\ndrei\n", "host/logrotate/edge-traefik": "ALT\n"}),
# "./Dockerfile" in der Ueberschrift, "Dockerfile" unter Zieldateien
("Zieldateien: ./Dockerfile", "mat",
zieldateien("tests/a.sh", "Dockerfile") + STELLE_A + ganz("#### Stelle 2 — `./Dockerfile`", "FROM neu"),
0, "0 Befund(e).", 0, {"tests/a.sh": "eins\nZWEI\ndrei\n", "Dockerfile": "FROM neu\n"}),
# Absoluter Eintrag: relativ zum Ordner vor docs/plans (Trading Plattform, Task 90: `.gitignore`)
("Zieldateien: absoluter Eintrag", "mat",
zieldateien("tests/a.sh", "{PROJEKT}\\.gitignore") + STELLE_A + aus_wird("### Schritt 2 — `.gitignore`", "data/", "data/\nartefakte/"),
0, "0 Befund(e).", 0, {"tests/a.sh": "eins\nZWEI\ndrei\n", ".gitignore": "data/\nartefakte/\n"}),
# Vorrang: Ein PFAD-Treffer in derselben Ueberschrift bleibt Ziel, auch neben einer Zieldatei ohne Endung
("Zieldateien: PFAD-Treffer hat Vorrang", "mat",
GEPLANT + STELLE_A + aus_wird(f"#### Änderung in `README.md`, Zeile {OHNE}", "host/ Ordner", "host/ Ordner, neu"),
0, "0 Befund(e).", 0, {"tests/a.sh": "eins\nZWEI\ndrei\n", "README.md": "# Projekt\n\n- host/ Ordner, neu\n"}),
# Ein Muster unter Zieldateien ist kein Ziel; der Pfad bleibt ein Befund
("Zieldateien: Muster ist kein Ziel", "mat",
zieldateien("tests/a.sh", "host/logrotate/*") + STELLE_A + ganz("#### Stelle 2 — `host/logrotate/*`", "neu"),
2, "nennt `host/logrotate/*` und keine erkannte Zieldatei", 1, {"tests/a.sh": "eins\nZWEI\ndrei\n"}),
# Name ohne Schraegstrich: liegt in der Kopie oder ist Dateiname eines Eintrags -> Befund statt stillem Ziel
("Dockerfile ohne Eintrag, letzte Stelle", "mat",
KOPF + STELLE_A + ganz("#### Stelle 2 — `Dockerfile`", "FROM neu"), 2, "nennt `Dockerfile` und keine erkannte Zieldatei", 1,
{"tests/a.sh": "eins\nZWEI\ndrei\n"}),
("Dockerfile ohne Eintrag, erste Stelle", "mat",
KOPF + ganz("#### Stelle 1 — `Dockerfile`", "FROM neu") + aus_wird("#### Stelle 2 — `tests/a.sh`", "zwei", "ZWEI"),
2, "nennt `Dockerfile` und keine erkannte Zieldatei", 1, {"tests/a.sh": "eins\nZWEI\ndrei\n"}),
("Dateiname eines Eintrags ohne Pfad", "mat",
GEPLANT + STELLE_A + ganz("#### Stelle 2 — `edge-traefik`", "neu"), 2, "nennt `edge-traefik` und keine erkannte Zieldatei", 1,
{"tests/a.sh": "eins\nZWEI\ndrei\n"}),
# Kein Befund: ein Bezeichner ohne Datei dieses Namens (bestehende Plaene: `main`, `pytest`, `Order`)
("Bezeichner ohne Datei", "mat",
KOPF + STELLE_A + aus_wird("#### Änderung 2 — `main` ruft `pruefe` auf", "drei", "DREI"), 0, "0 Befund(e).", 0,
{"tests/a.sh": "eins\nZWEI\nDREI\n"}),
("bloecke: Zieldateien, Datei ohne Endung", "vor",
GEPLANT + STELLE_A + aus_wird(f"#### Stelle 2 — {OHNE}", "alt", "ALT"), 0, "0 Befund(e).", 0, None),
("bloecke: Zieldateien, absoluter Eintrag", "vor",
zieldateien("tests/a.sh", "{PROJEKT}\\.gitignore") + STELLE_A + aus_wird("### Schritt 2 — `.gitignore`", "data/", "data/\nartefakte/"),
0, "0 Befund(e).", 0, None),
("bloecke: Dockerfile ohne Eintrag", "vor",
KOPF + STELLE_A + aus_wird("#### Stelle 2 — `Dockerfile`", "drei", "DREI"), 2, "nennt `Dockerfile` und keine erkannte Zieldatei", 2, None),
]
def schreibe(pfad: Path, text: str) -> None:
pfad.parent.mkdir(parents=True, exist_ok=True)
pfad.write_bytes(text.encode("utf-8"))
def fahre(werkzeuge: Path, art: str, wurzel: Path, plan: Path) -> subprocess.CompletedProcess:
if art == "mat":
befehl = [str(werkzeuge / "plan_materialisiere.py"), str(plan), str(wurzel / "kopie"), "task-01.md"]
else:
befehl = [str(werkzeuge / "plan_bloecke.py"), art, str(plan / "task-01.md"), "--projekt", str(wurzel / "kopie")]
return subprocess.run([sys.executable, "-X", "utf8", "-B", *befehl], capture_output=True, text=True,
encoding="utf-8", errors="replace", stdin=subprocess.DEVNULL)
def main() -> int:
zerleger = argparse.ArgumentParser()
zerleger.add_argument("--werkzeuge", default=str(Path(__file__).resolve().parent))
werkzeuge = Path(zerleger.parse_args().werkzeuge)
fehler = 0
for name, art, task, exit_erwartet, meldung, anzahl, dateien in FAELLE:
wurzel = Path(tempfile.mkdtemp(prefix="pz_"))
try:
basis = dict(BASIS)
if art == "nach":
# Stand nach der Welle: Stelle 1 angewendet, Stelle 2 im vorigen Ziel statt in edge-traefik
basis["tests/a.sh"] = "EINS\nzwei\nDREI\n"
for rel, text in basis.items():
schreibe(wurzel / "kopie" / rel, text)
# {PROJEKT}: fuer plan_bloecke der Ordner hinter --projekt; fuer plan_materialisiere ein eigener
# Projektordner, unter dessen docs/plans die Taskdatei liegt (die Kopie liegt woanders)
plan = wurzel / "plan"
if "{PROJEKT}" in task:
projekt = wurzel / ("projekt" if art == "mat" else "kopie")
plan = projekt / "docs" / "plans" / "p" if art == "mat" else plan
task = task.replace("{PROJEKT}", str(projekt))
schreibe(plan / "task-01.md", task)
lauf = fahre(werkzeuge, art, wurzel, plan)
ausgabe = lauf.stdout + lauf.stderr
gruende = []
if lauf.returncode != exit_erwartet:
gruende.append(f"Exit {lauf.returncode}, erwartet {exit_erwartet}")
if meldung not in ausgabe:
gruende.append(f"Meldung fehlt: {meldung}")
if ausgabe.count("BEFUND ") != anzahl:
gruende.append(f"{ausgabe.count('BEFUND ')} BEFUND-Zeile(n), erwartet {anzahl}")
# Jede Datei der Kopie: erwarteter Inhalt, sonst unveraendert wie BASIS
for rel, text in basis.items() if dateien is not None else ():
soll = dateien.get(rel, text)
ist = (wurzel / "kopie" / rel).read_bytes().decode("utf-8", errors="replace")
if ist != soll:
gruende.append(f"{rel}: {ist!r}, erwartet {soll!r}")
print(f"{'ok ' if not gruende else 'FEHLER'} {name}: Exit {lauf.returncode}")
if gruende:
fehler += 1
for grund in gruende:
print(" " + grund)
print(" " + ausgabe.strip().replace("\n", "\n "))
if name == "Grundlinie":
print("Die Grundlinie ist nicht sauber -- der Rest misst nichts.")
return 2
finally:
shutil.rmtree(wurzel, ignore_errors=True)
print(f"{fehler} Fall/Faelle mit falschem Ausgang von {len(FAELLE)}.")
return 2 if fehler else 0
if __name__ == "__main__":
sys.exit(main())