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

129
phase0/baue-vorschau.py Normal file
View File

@@ -0,0 +1,129 @@
#!/usr/bin/env python3
"""Baut die Phase-0-Vorschau: Feature-Sicht + Einheiten → aufklappbares HTML.
Aufruf: baue-vorschau.py <features.md> <projekt-wurzel> <ausgabe.html> <einheiten.md ...>
Der konkrete Code je Einheit wird live aus der Quelle extrahiert (Anker: Pfad::Symbol) —
Artefakte enthalten nie Code-Kopien.
"""
import html
import json
import re
import sys
from pathlib import Path
def einheiten_parsen(pfade) -> dict:
out = {}
aktuelle = None
modus = None
for pfad in pfade:
for zeile in Path(pfad).read_text(encoding="utf-8").splitlines():
if zeile.startswith("## ") and "::" in zeile:
aktuelle = {"id": zeile[3:].strip(), "beschreibung": "", "input": "",
"output": "", "entscheidungen": [], "kanten": [], "anker": []}
out[aktuelle["id"]] = aktuelle
modus = None
elif aktuelle is None:
continue
elif zeile.startswith("beschreibung:"):
aktuelle["beschreibung"] = zeile.split(":", 1)[1].strip(); modus = "beschreibung"
elif zeile.startswith("input:"):
aktuelle["input"] = zeile.split(":", 1)[1].strip(); modus = "input"
elif zeile.startswith("output:"):
aktuelle["output"] = zeile.split(":", 1)[1].strip(); modus = "output"
elif zeile.startswith("anker:"):
aktuelle["anker"].append(zeile.split(":", 1)[1].strip())
elif zeile.strip() in ("entscheidungen:", "fakten:"):
modus = "punkte"
elif zeile.strip() == "kanten:":
modus = "kanten"
elif modus == "punkte" and (m := re.match(r'\s+beleg:\s*"(.*)"', zeile)):
pass # Belege prüft pruefe.py; die Vorschau zeigt echten Code statt Zitaten
elif modus == "punkte" and zeile.startswith("- "):
aktuelle["entscheidungen"].append(zeile[2:].strip())
elif modus == "punkte" and zeile.startswith(" ") and aktuelle["entscheidungen"]:
aktuelle["entscheidungen"][-1] += " " + zeile.strip()
elif modus == "kanten" and (m := re.match(r"- ([\wäöü-]+):\s*(\S+)", zeile)):
aktuelle["kanten"].append({"typ": m.group(1), "ziel": m.group(2)})
elif modus in ("beschreibung", "input", "output") and zeile.startswith(" "):
aktuelle[modus] += " " + zeile.strip()
return out
def features_parsen(pfad) -> list:
bereiche = []
bereich = feature = None
for zeile in Path(pfad).read_text(encoding="utf-8").splitlines():
if zeile.startswith("# Bereich:"):
bereich = {"name": zeile.split(":", 1)[1].strip(), "beschreibung": "", "features": []}
bereiche.append(bereich); feature = None
elif zeile.startswith("## Feature:"):
titel = zeile[11:].strip()
flag = "kern" if "[kern]" in titel else "rand"
feature = {"titel": re.sub(r"\s*\[(kern|rand)\]", "", titel).strip(),
"flag": flag, "beschreibung": "", "teilfeatures": []}
bereich["features"].append(feature)
elif zeile.startswith("### ") and feature is not None:
feature["teilfeatures"].append({"titel": zeile[4:].strip(), "einheiten": []})
elif zeile.startswith("einheiten:") and feature and feature["teilfeatures"]:
feature["teilfeatures"][-1]["einheiten"] = \
[t.strip() for t in zeile.split(":", 1)[1].split(",") if t.strip()]
elif zeile.startswith("beschreibung:"):
ziel = feature if feature is not None else bereich
if ziel is not None:
ziel["beschreibung"] = zeile.split(":", 1)[1].strip()
return bereiche
def code_extrahieren(wurzel: Path, eid: str) -> str:
"""Quellcode eines Ankers (pfad::symbol): def/class-Block per Einrückung."""
if "::" not in eid:
return ""
datei, symbol = eid.split("::", 1)
p = wurzel / datei
if not p.is_file():
return ""
zeilen = p.read_text(encoding="utf-8").splitlines()
start = None
for i, z in enumerate(zeilen):
if re.match(rf"(\s*)(?:async\s+)?(?:def|class)\s+{re.escape(symbol)}\b", z):
start = i
break
if re.match(rf"{re.escape(symbol)}\s*[:=]", z): # Modul-Konstante
start = i
ende = i + 1
while ende < len(zeilen) and (zeilen[ende].startswith((" ", "\t", ")", "]", "}"))
or not zeilen[ende].strip()):
ende += 1
return "\n".join(zeilen[start:ende]).rstrip()
if start is None:
return ""
einzug = len(zeilen[start]) - len(zeilen[start].lstrip())
ende = start + 1
while ende < len(zeilen):
z = zeilen[ende]
if z.strip() and (len(z) - len(z.lstrip())) <= einzug:
break
ende += 1
return "\n".join(zeilen[start:ende]).rstrip()
def main():
features_pfad, wurzel, ausgabe = sys.argv[1], Path(sys.argv[2]), sys.argv[3]
einheiten = einheiten_parsen(sys.argv[4:])
bereiche = features_parsen(features_pfad)
code = {}
for e in einheiten.values():
for eid in [e["id"], *e["anker"]]:
code[e["id"]] = code.get(e["id"]) or code_extrahieren(wurzel, eid)
daten = {"bereiche": bereiche, "einheiten": einheiten, "code": code}
tpl = Path(__file__).with_name("vorschau-template.html").read_text(encoding="utf-8")
Path(ausgabe).write_text(tpl.replace("/*DATEN*/", json.dumps(daten, ensure_ascii=False)),
encoding="utf-8")
n_feat = sum(len(b["features"]) for b in bereiche)
print(f"{ausgabe}: {len(bereiche)} Bereiche, {n_feat} Features, {len(einheiten)} Einheiten")
if __name__ == "__main__":
main()