"""Wendet die Codebloecke der Taskdateien eines Plans in Wellenreihenfolge auf eine Kopie des Projekts an. Koordinator-Werkzeug (entstanden 2026-09-17, Trading Plattform Teil 3b). Es tut etwas und beurteilt nichts; das Urteil faellt danach mit Suite, Mutanten und welle_vergleich.py. Geltungsbereich: - Zieldatei ist der letzte Pfad in Backticks in einer Ueberschrift (## bis ####) oberhalb des Blocks. - Dateien, die "## Zieldateien" als "(gelöscht)" fuehrt, werden in der Kopie geloescht. - Bloecke mit Einrueckung (Befehle in Listen) und Prosa-Vorgaben ("wird: ein neuer Abschnitt ...") werden uebergangen: Ein Doku-Task mit frei zu formulierendem Text laesst sich nicht anwenden. - Marke ist die letzte nicht leere Zeile vor dem Zaun: "Aus:" -> merkt den Block als alten Text "wird:" -> ersetzt den gemerkten Text (muss genau einmal vorkommen) enthaelt "Die ganze Datei ist neu" -> schreibt die Datei beginnt mit "Häng diesen Block ans Ende" -> haengt an Alles andere (Befehle, erwartete Ausgaben, Prosa-"wird: ...") wird uebergangen und gezaehlt. - Zaeune aus n Backticks schliessen erst mit n Backticks. - Geschrieben wird immer mit LF. Aufruf: python plan_materialisiere.py [ ...] Exit 0 ohne Befund, 2 mit Befund. """ import re import sys from pathlib import Path UEBERSCHRIFT = re.compile(r"^#{2,4} ") PFAD = re.compile(r"`([^`\s]+/[^`\s]+\.[A-Za-z0-9]+|[^`\s/]+\.(?:py|ts|tsx|toml|md|css|json))`") def normal(text: str) -> str: return text.replace("\r\n", "\n").replace("\r", "\n") def bloecke(taskdatei: Path): zeilen = normal(taskdatei.read_text(encoding="utf-8")).split("\n") ziel = None i = 0 while i < len(zeilen): zeile = zeilen[i] if UEBERSCHRIFT.match(zeile): treffer = PFAD.findall(zeile) if treffer: ziel = treffer[-1] if zeile.lstrip().startswith("```"): einzug = len(zeile) - len(zeile.lstrip()) kern = zeile.lstrip() laenge = len(kern) - len(kern.lstrip("`")) start = i i += 1 inhalt = [] while i < len(zeilen): kandidat = zeilen[i].lstrip() if kandidat.startswith("`" * laenge) and not kandidat.strip("`").strip(): break inhalt.append(zeilen[i][einzug:] if zeilen[i][:einzug].strip() == "" else zeilen[i]) i += 1 k = start - 1 while k >= 0 and not zeilen[k].strip(): k -= 1 marke = zeilen[k].strip() if k >= 0 else "" yield {"ziel": ziel, "marke": marke, "zeile": start + 1, "text": "\n".join(inhalt), "einzug": einzug} i += 1 def in_kopie(kopie: Path, ziel: str) -> Path | None: """Der Zielpfad in der Kopie, oder None, wenn er aus ihr hinausfuehrt. Nachbesserung 2026-10-11 (Paket E9 der Trading Plattform): Ein absoluter Pfad in einer Ueberschrift schlug unter Windows die Kopie (kopie / "D:/..." ist der absolute Pfad), und das Skript schrieb mit Exit 0 in die echte Datei; gemessen im Scratchpad an einer Probe-Datei. Seitdem ist ein Ziel ausserhalb der Kopie (absolut oder mit "..") ein Befund, und nichts wird geschrieben. """ pfad = (kopie / ziel).resolve() try: pfad.relative_to(kopie.resolve()) except ValueError: return None return pfad LOESCHZIEL = re.compile(r"^- `(.+?)`\s*\((.*)\)\s*$") def loesche(taskdatei: Path, kopie: Path) -> int: """Loescht, was der Abschnitt "## Zieldateien" als "(gelöscht)" fuehrt. Der Pfad dort ist absolut und zeigt ins echte Projekt. Geloescht wird in der Kopie die Datei am laengsten Pfadende, das dort existiert -- nie etwas ausserhalb der Kopie. """ drin = False anzahl = 0 for zeile in normal(taskdatei.read_text(encoding="utf-8")).split("\n"): if zeile.startswith("## "): drin = zeile.startswith("## Zieldateien") continue treffer = LOESCHZIEL.match(zeile) if drin else None if not treffer or "gelöscht" not in treffer.group(2): continue teile = treffer.group(1).replace("\\", "/").split("/") for anfang in range(len(teile)): kandidat = kopie.joinpath(*teile[anfang:]) if ":" not in teile[anfang] and kandidat.is_file(): kandidat.unlink() anzahl += 1 break return anzahl def main() -> int: plan, kopie = Path(sys.argv[1]), Path(sys.argv[2]) befunde = 0 for name in sys.argv[3:]: taskdatei = plan / name alt = None zaehler = {"neu": 0, "ersetzt": 0, "angehaengt": 0, "uebergangen": 0} for block in bloecke(taskdatei): marke, ziel = block["marke"], block["ziel"] if block["einzug"] > 0: zaehler["uebergangen"] += 1 continue if marke == "Aus:" or marke.startswith("Aus, Zeile"): alt = block continue if marke == "wird:": if alt is None or ziel is None: print(f"BEFUND {name}:{block['zeile']}: wird-Block ohne Aus-Block oder ohne Zieldatei") befunde += 1 continue pfad = in_kopie(kopie, ziel) if pfad is None: print(f"BEFUND {name}:{block['zeile']}: Zieldatei {ziel} liegt ausserhalb der Kopie, nichts geschrieben") befunde += 1 alt = None continue if not pfad.exists(): print(f"BEFUND {name}:{block['zeile']}: Zieldatei {ziel} fehlt in der Kopie") befunde += 1 alt = None continue text = normal(pfad.read_text(encoding="utf-8")) anzahl = text.count(alt["text"]) if anzahl != 1: print(f"BEFUND {name}:{alt['zeile']}: Aus-Block steht {anzahl}-mal in {ziel}") befunde += 1 else: with open(pfad, "w", encoding="utf-8", newline="\n") as strom: strom.write(text.replace(alt["text"], block["text"])) zaehler["ersetzt"] += 1 alt = None continue alt = None if ("Die ganze Datei ist neu" in marke or "vollständig ersetzt" in marke) and ziel is not None: pfad = in_kopie(kopie, ziel) if pfad is None: print(f"BEFUND {name}:{block['zeile']}: Zieldatei {ziel} liegt ausserhalb der Kopie, nichts geschrieben") befunde += 1 continue pfad.parent.mkdir(parents=True, exist_ok=True) with open(pfad, "w", encoding="utf-8", newline="\n") as strom: strom.write(block["text"].rstrip("\n") + "\n") zaehler["neu"] += 1 continue if marke.startswith("Häng diesen Block ans Ende") and ziel is not None: pfad = in_kopie(kopie, ziel) if pfad is None: print(f"BEFUND {name}:{block['zeile']}: Zieldatei {ziel} liegt ausserhalb der Kopie, nichts geschrieben") befunde += 1 continue text = normal(pfad.read_text(encoding="utf-8")) with open(pfad, "w", encoding="utf-8", newline="\n") as strom: strom.write(text.rstrip("\n") + "\n" + block["text"].rstrip("\n") + "\n") zaehler["angehaengt"] += 1 continue zaehler["uebergangen"] += 1 zaehler["geloescht"] = loesche(taskdatei, kopie) print(f"{name}: {zaehler}") print(f"{befunde} Befund(e).") return 2 if befunde else 0 if __name__ == "__main__": sys.exit(main())