"""Prueft die Aus-/wird-Bloecke einer Plan-Taskdatei gegen den Projektbaum. Koordinator-Werkzeug im Scratchpad. Geltungsbereich (Fangnetz, kein Nachweis): - Sieht nur Codebloecke, deren letzte nicht leere Zeile vor dem Zaun genau "Aus:" oder "wird:" ist (Projektkonvention der Planer-Taskdateien seit Teil 5). Welche Datei Ziel ist, regelt ordne() in plan_materialisiere.py (importiert, die Regel steht dort im Kopf): der letzte PFAD-Treffer einer Ueberschrift (##, ###, ####) oberhalb des Blocks, ohne ihn ein Token, das genau so unter "## Zieldateien" steht (absolute Eintraege relativ zu --projekt). Ob die Zieldatei im Projekt liegt, prueft erst der Modus. Bis 2026-10-17 stand hier, das Ziel muesse unter Zieldateien stehen; der Code tat das nie. - Befund statt stillem Ziel (seit 2026-10-17, Plankritik Paket R1 im Edge-Stack): Nennt eine Ueberschrift kein Ziel, aber einen Token, der nach einer Datei aussieht (relativer Pfad mit Schraegstrich; Name, der direkt in --projekt als Datei liegt oder Dateiname eines Eintrags ist), ist jeder Aus- und wird-Block danach ein Befund, bis eine Ueberschrift mit Ziel kommt; vorher wurde still gegen das vorige Ziel geprueft. Luecken wie in plan_materialisiere.py; Nachweis: plan_ziel_treiber.py. - Exit 3 hat Vorrang: Wurde in keiner Taskdatei ein Block gegen eine Datei geprueft, endet es mit 3, auch wenn Befunde ausgegeben sind. - Modus vor: jeder Aus-Block steht genau einmal in seiner Zieldatei (0 = Befund, >1 = Befund "mehrdeutig"). Belegt, dass der Plan den heutigen Stand byte-genau zitiert. - Modus nach: jeder wird-Block steht in der Zieldatei; jeder Aus-Block steht nicht mehr darin, ausser er ist Teil seines eigenen wird-Blocks (Einfuegung). - Ganzdatei-Bloecke ("neu anlegen") sieht es nicht; dafuer ist .vault-tools/plan-diff/plan_diff.py da. - Vergleich nach Normalisierung CRLF -> LF. Eine fehlende Zieldatei ist im Modus vor fuer Aus-Bloecke ein Befund, im Modus nach fuer beide. Exit: 0 kein Befund, 2 Befunde, 3 kein einziger Aus-/wird-Block gefunden (nichts geprueft). """ import argparse import re import sys from pathlib import Path, PureWindowsPath # Die Zielregel (PFAD, Zieldateien, offene Token) steht einmal, in plan_materialisiere.py im selben Ordner; # der Ordner kommt ausdruecklich in den Suchpfad, sonst scheitert der Import unter "python -I" sys.path.insert(0, str(Path(__file__).resolve().parent)) from plan_materialisiere import ordne, zieldateien # noqa: E402 ZAUN = re.compile(r"^```") UEBERSCHRIFT = re.compile(r"^#{2,4} ") def normal(text: str) -> str: return text.replace("\r\n", "\n").replace("\r", "\n") def lies_bloecke(taskdatei: Path, projekt: Path) -> list[dict]: zeilen = normal(taskdatei.read_text(encoding="utf-8")).split("\n") bloecke: list[dict] = [] # Absolute Eintraege unter "## Zieldateien" zaehlen relativ zu --projekt, wie in welle_vergleich.py geplant = zieldateien(zeilen, [PureWindowsPath(projekt.absolute()), PureWindowsPath(projekt.resolve())]) ziel: str | None = None # (Zeile, Token) der letzten Ueberschrift ohne Ziel, die nach einer Datei aussieht; None, sobald ein Ziel kommt ungeklaert: tuple[int, list[str]] | None = None i = 0 while i < len(zeilen): zeile = zeilen[i] if UEBERSCHRIFT.match(zeile): neu, offen = ordne(zeile, geplant, projekt) if neu: ziel, ungeklaert = neu, None elif offen: ungeklaert = (i + 1, offen) if ZAUN.match(zeile): start = i # Ein Zaun aus n Backticks schliesst erst mit mindestens n Backticks: Ein # Vier-Backtick-Block darf einen Drei-Backtick-Block enthalten (Doku-Tasks). laenge = len(zeile) - len(zeile.lstrip("`")) i += 1 inhalt: list[str] = [] while i < len(zeilen) and not ( zeilen[i].startswith("`" * laenge) and not zeilen[i].strip("`").strip() ): inhalt.append(zeilen[i]) i += 1 marke = "" k = start - 1 while k >= 0 and not zeilen[k].strip(): k -= 1 if k >= 0: marke = zeilen[k].strip() if marke in ("Aus:", "wird:"): bloecke.append({"art": marke[:-1], "ziel": ziel, "zeile": start + 1, "text": "\n".join(inhalt), "ungeklaert": ungeklaert}) i += 1 return bloecke def main() -> int: zerleger = argparse.ArgumentParser() zerleger.add_argument("modus", choices=["vor", "nach"]) zerleger.add_argument("taskdateien", nargs="+") zerleger.add_argument("--projekt", required=True) argumente = zerleger.parse_args() projekt = Path(argumente.projekt) befunde = 0 geprueft = 0 for name in argumente.taskdateien: taskdatei = Path(name) bloecke = lies_bloecke(taskdatei, projekt) paare = 0 for nummer, block in enumerate(bloecke): if block["ungeklaert"]: zeile_u, pfade = block["ungeklaert"] print(f"BEFUND {taskdatei.name}:{block['zeile']}: {block['art']}-Block unter Ueberschrift in Zeile {zeile_u}, " f"die nennt {', '.join(f'`{p}`' for p in pfade)} und keine erkannte Zieldatei (fehlt sie unter ## Zieldateien?); " f"geprueft wuerde gegen {block['ziel'] or 'nichts'}") befunde += 1 continue if block["ziel"] is None: print(f"BEFUND {taskdatei.name}:{block['zeile']}: {block['art']}-Block ohne Zieldatei in einer Ueberschrift") befunde += 1 continue datei = projekt / block["ziel"] if not datei.exists(): if argumente.modus == "nach" or block["art"] == "Aus": print(f"BEFUND {taskdatei.name}:{block['zeile']}: Zieldatei {block['ziel']} fehlt") befunde += 1 continue inhalt = normal(datei.read_text(encoding="utf-8")) anzahl = inhalt.count(block["text"]) geprueft += 1 if argumente.modus == "vor": if block["art"] != "Aus": continue paare += 1 if anzahl == 0: print(f"BEFUND {taskdatei.name}:{block['zeile']}: Aus-Block steht nicht in {block['ziel']}") befunde += 1 elif anzahl > 1: print(f"BEFUND {taskdatei.name}:{block['zeile']}: Aus-Block steht {anzahl}-mal in {block['ziel']} (mehrdeutig)") befunde += 1 else: if block["art"] == "wird": paare += 1 if anzahl == 0: print(f"BEFUND {taskdatei.name}:{block['zeile']}: wird-Block steht nicht in {block['ziel']}") befunde += 1 else: folge = bloecke[nummer + 1] if nummer + 1 < len(bloecke) else None einfuegung = folge is not None and folge["art"] == "wird" and block["text"] in folge["text"] if anzahl > 0 and not einfuegung: print(f"BEFUND {taskdatei.name}:{block['zeile']}: Aus-Block steht noch in {block['ziel']}") befunde += 1 print(f"{taskdatei.name}: {len(bloecke)} Aus-/wird-Bloecke, im Modus {argumente.modus} geprueft: {paare}") if geprueft == 0: print("Nichts geprueft: kein Aus-/wird-Block gefunden.") return 3 print(f"{befunde} Befund(e).") return 2 if befunde else 0 if __name__ == "__main__": sys.exit(main())