This commit is contained in:
Team3
2026-07-22 16:12:23 +02:00
commit f07ef9653d
851 changed files with 501480 additions and 0 deletions

156
phase0/pruefe.py Normal file
View File

@@ -0,0 +1,156 @@
#!/usr/bin/env python3
"""Mechanische Prüfung einer Einheiten-Datei (Phase 0).
Aufruf: pruefe.py <einheiten.md> <projekt-wurzel>
Prüft tokenfrei:
1. Anker-Existenz — jede Einheiten-ID und jedes Kanten-Ziel zeigt auf existierende
Datei + darin definiertes Symbol (def/class/Zuweisung).
2. Zitat-Treue — jeder beleg:-String kommt wörtlich in der Anker-Datei vor.
3. Struktur — Schlüssel-Grammatik (beschreibung/fakten/kanten, Kanten-Typen).
4. Abdeckung — Report: welche Top-Level-Symbole der Quelldatei keiner Einheit
zugeordnet sind (v1 kein Fehler).
"""
import re
import sys
from pathlib import Path
KANTEN_TYPEN = {"ruft-auf", "nutzt", "wird-genutzt-von", "löst-aus", "wird-ausgelöst-von"}
def entschaerfe(s: str) -> str:
"""Escaping-Rauschen der Modelle normalisieren: \\"" und \\\\\\."""
return s.replace('\\"', '"').replace("\\\\", "\\")
def symbole_in(quelle: str) -> set[str]:
"""Top-Level-Symbole: def/class/Konstanten-Zuweisung auf Spalte 0 (+ async def)."""
out = set()
for m in re.finditer(r"^(?:async\s+)?(?:def|class)\s+(\w+)", quelle, re.M):
out.add(m.group(1))
for m in re.finditer(r"^(\w+)\s*[:=]", quelle, re.M):
out.add(m.group(1))
return out
def main(pfad: str, wurzel: str) -> int:
text = Path(pfad).read_text(encoding="utf-8")
wurzel_p = Path(wurzel)
fehler, warnungen = [], []
quell_cache: dict[str, str | None] = {}
def quelle(datei: str) -> str | None:
if datei not in quell_cache:
p = wurzel_p / datei
quell_cache[datei] = p.read_text(encoding="utf-8") if p.is_file() else None
return quell_cache[datei]
def pruefe_id(eid: str, kontext: str):
if "::" not in eid:
if quelle(eid) is None:
fehler.append(f"{kontext}: Datei fehlt: {eid}")
return
datei, symbol = eid.split("::", 1)
q = quelle(datei)
if q is None:
fehler.append(f"{kontext}: Datei fehlt: {datei}")
elif symbol not in symbole_in(q):
fehler.append(f"{kontext}: Symbol '{symbol}' nicht top-level in {datei}")
# --- parsen ---
einheiten: list[dict] = []
zusatz_anker: set[str] = set()
aktuelle = None
modus = None # None | fakten | kanten
for nr, zeile in enumerate(text.splitlines(), 1):
if zeile.startswith("## "):
aktuelle = {"id": zeile[3:].strip(), "zeile": nr, "belege": [], "kanten": [],
"beschreibung": False}
einheiten.append(aktuelle)
modus = None
elif zeile.startswith(("anker:", "ergaenze-anker:")):
# Zusatz-Anker dürfen auch verschachtelte Symbole sein (Innenfunktionen);
# ergaenze-anker kommt auch außerhalb von Einheiten vor (Nachfass-Ausgabe)
aid = zeile.split(":", 1)[1].strip().split(" zu ")[0].strip()
zusatz_anker.add(aid)
if "::" in aid:
datei, symbol = aid.split("::", 1)
q = quelle(datei)
if q is None:
fehler.append(f"Z{nr} anker: Datei fehlt: {datei}")
elif not re.search(rf"(?:def|class)\s+{re.escape(symbol)}\b|^{re.escape(symbol)}\s*[:=]",
q, re.M):
fehler.append(f"Z{nr} anker: Symbol '{symbol}' nicht in {datei}")
else:
pruefe_id(aid, f"Z{nr} anker")
elif aktuelle is None:
continue
elif zeile.startswith("beschreibung:"):
aktuelle["beschreibung"] = True
modus = None
elif zeile.strip() in ("fakten:", "entscheidungen:"):
modus = "fakten"
elif zeile.startswith("input:"):
aktuelle["input"] = True
elif zeile.startswith("output:"):
aktuelle["output"] = True
elif zeile.strip() == "kanten:":
modus = "kanten"
elif m := re.match(r'\s+beleg:\s*"(.*)"\s*$', zeile):
aktuelle["belege"].append((nr, entschaerfe(m.group(1))))
elif modus == "fakten" and (m := re.search(r'\bbeleg:\s*"(.*)"\s*$', zeile)):
# tolerant: beleg inline am Ende der Fakt-Zeile (streng schreiben, tolerant parsen)
aktuelle["belege"].append((nr, entschaerfe(m.group(1))))
elif modus == "kanten" and (m := re.match(r"- ([\wäöü-]+):\s*(\S+)\s*$", zeile)):
aktuelle["kanten"].append((nr, m.group(1), m.group(2)))
elif modus == "kanten" and zeile.startswith("- "):
fehler.append(f"Z{nr}: Kante unlesbar (Zusatztext?): {zeile.strip()!r}")
# --- prüfen ---
for e in einheiten:
kontext = e["id"]
pruefe_id(e["id"], f"Z{e['zeile']} einheit")
if not e["beschreibung"]:
fehler.append(f"{kontext}: beschreibung: fehlt")
datei = e["id"].split("::")[0]
q = quelle(datei) or ""
for nr, beleg in e["belege"]:
if beleg not in q:
fehler.append(f"{kontext} Z{nr}: Zitat nicht wörtlich in {datei}: \"{beleg[:50]}\"")
if not e["belege"]:
warnungen.append(f"{kontext}: keine Belege")
if not e.get("input") or not e.get("output"):
warnungen.append(f"{kontext}: input:/output: fehlt")
for nr, typ, ziel in e["kanten"]:
if typ not in KANTEN_TYPEN:
fehler.append(f"{kontext} Z{nr}: unbekannter Kanten-Typ '{typ}'")
pruefe_id(ziel, f"{kontext} Z{nr} kante")
# --- Abdeckung der gescannten Datei ---
dateien = {e["id"].split("::")[0] for e in einheiten if "::" in e["id"]}
for datei in sorted(dateien):
q = quelle(datei)
if q is None:
continue
zugeordnet = {e["id"].split("::", 1)[1] for e in einheiten
if e["id"].startswith(datei + "::")}
zugeordnet |= {a.split("::", 1)[1] for a in zusatz_anker
if a.startswith(datei + "::")}
oeffentlich = {s for s in symbole_in(q)
if re.search(rf"^(?:async\s+)?(?:def|class)\s+{re.escape(s)}\b", q, re.M)}
fehlt = oeffentlich - zugeordnet
if fehlt:
warnungen.append(f"Abdeckung {datei}: nicht zugeordnet: {', '.join(sorted(fehlt))}")
print(f"Einheiten: {len(einheiten)}, Belege: {sum(len(e['belege']) for e in einheiten)}, "
f"Kanten: {sum(len(e['kanten']) for e in einheiten)}")
for f in fehler:
print(f"FEHLER {f}")
for w in warnungen:
print(f"WARNUNG {w}")
print(f"\n{len(fehler)} Fehler, {len(warnungen)} Warnungen")
return 1 if fehler else 0
if __name__ == "__main__":
sys.exit(main(sys.argv[1], sys.argv[2]))