11 Datei(en) geändert. Erzeugt von .vault-tools/auto-push/werkzeuge_spiegel.py.
167 lines
8.5 KiB
Python
167 lines
8.5 KiB
Python
"""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.
|
|
- Zaunpruefung (seit 2026-11-02, Regel und Luecken im Kopf von plan_materialisiere.py, Nachweis: plan_zaun_treiber.py):
|
|
Klebt ein Zaun, fehlt einer oder schliesst ein Block nie, ist das ein Befund, und die Bloecke dieser Taskdatei
|
|
werden nicht geprueft: Hinter dem Zaun stimmt die Datei nicht mehr, und ein still geschluckter Aus-Block fiele
|
|
sonst nur als "steht nicht in ..." auf. Vorher lief hier eine eigene Blocksuche nur in Spalte 0; seitdem gilt
|
|
dieselbe wie in plan_materialisiere.py (zaun() und schliesst()), Aus- und wird-Bloecke zaehlen weiter nur mit
|
|
Zaun in Spalte 0.
|
|
- Exit 3 hat Vorrang: Wurde in keiner Taskdatei ein Block gegen eine Datei geprueft, endet es mit 3,
|
|
auch wenn Befunde ausgegeben sind. Ausnahme ist ein Befund der Zaunpruefung, dort bleibt es bei 2.
|
|
- 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
|
|
|
|
# Zielregel und Zaunregel stehen 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, schliesst, zaun, zaunbefunde, zieldateien # noqa: E402
|
|
|
|
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)
|
|
laenge = zaun(zeile)
|
|
if laenge:
|
|
start = i
|
|
einzug = len(zeile) - len(zeile.lstrip())
|
|
# Ein Zaun aus n Backticks schliesst erst mit mindestens n Backticks: Ein
|
|
# Vier-Backtick-Block darf einen Drei-Backtick-Block enthalten (Doku-Tasks).
|
|
i += 1
|
|
inhalt: list[str] = []
|
|
while i < len(zeilen) and not schliesst(zeilen[i], laenge, einzug):
|
|
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:") and zeile.startswith("`"):
|
|
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
|
|
zaunfehler = False
|
|
for name in argumente.taskdateien:
|
|
taskdatei = Path(name)
|
|
kaputt = zaunbefunde(taskdatei)
|
|
if kaputt:
|
|
for zeile, meldung in kaputt:
|
|
print(f"BEFUND {taskdatei.name}:{zeile}: {meldung}; Bloecke dieser Taskdatei nicht geprueft")
|
|
befunde += len(kaputt)
|
|
zaunfehler = True
|
|
continue
|
|
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 and not zaunfehler:
|
|
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())
|