102 lines
4.2 KiB
Python
102 lines
4.2 KiB
Python
"""Artefakt-Schicht: Scan-Fragmente je Quelldatei zusammensetzen und als
|
|
`.planer/`-Dateien ins Ziel-Repo schreiben. Merge arbeitet auf den ROHEN
|
|
Markdown-Blöcken (kein Grammatik-Roundtrip — nichts geht verloren).
|
|
"""
|
|
import re
|
|
from pathlib import Path
|
|
|
|
_ERGAENZE_RE = re.compile(r"^ergaenze-anker:\s*(\S+)\s+zu\s+(\S+)\s*$", re.M)
|
|
|
|
|
|
def datei_pfad(datei_rel: str) -> str:
|
|
"""Quelldatei → Pfad der Einheiten-Datei unter .planer/ (`/` → `__`).
|
|
Bekannte v1-Schwäche: Quellnamen mit `__` können kollidieren."""
|
|
return "einheiten/" + datei_rel.replace("/", "__") + ".md"
|
|
|
|
|
|
def _bloecke(text: str) -> dict[str, str]:
|
|
"""Fragment → {einheiten_id: roher_block}. Blöcke beginnen bei `## <id>`."""
|
|
out: dict[str, str] = {}
|
|
aktuelle_id = None
|
|
zeilen: list[str] = []
|
|
for zeile in text.splitlines():
|
|
if zeile.startswith("## ") and ("::" in zeile or re.fullmatch(r"## \S+\.\w+", zeile.strip())):
|
|
if aktuelle_id is not None:
|
|
out[aktuelle_id] = "\n".join(zeilen).rstrip()
|
|
aktuelle_id = zeile[3:].strip()
|
|
zeilen = [zeile]
|
|
elif aktuelle_id is not None and not _ERGAENZE_RE.match(zeile):
|
|
zeilen.append(zeile)
|
|
if aktuelle_id is not None:
|
|
out[aktuelle_id] = "\n".join(zeilen).rstrip()
|
|
return out
|
|
|
|
|
|
def merge_fragmente(fragmente: list[str]) -> str:
|
|
"""Fragmente EINER Quelldatei → Rumpf der Einheiten-Datei. Einheiten in Reihenfolge
|
|
des ersten Auftretens, Dubletten (gleiche ID) fallen weg; `ergaenze-anker: X zu Y`
|
|
aus Nachfass-Antworten wird als `anker: X` in den Block von Y eingefügt."""
|
|
bloecke: dict[str, str] = {}
|
|
ergaenzungen: list[tuple[str, str]] = []
|
|
for frag in fragmente:
|
|
ergaenzungen += _ERGAENZE_RE.findall(frag)
|
|
for eid, block in _bloecke(frag).items():
|
|
bloecke.setdefault(eid, block)
|
|
for anker_id, ziel_id in ergaenzungen:
|
|
block = bloecke.get(ziel_id)
|
|
if block is None:
|
|
continue # Ziel existiert nicht — Gate hat das bereits als Fehler gemeldet
|
|
kopf, _, rest = block.partition("\n")
|
|
if f"anker: {anker_id}" not in block:
|
|
bloecke[ziel_id] = f"{kopf}\nanker: {anker_id}\n{rest}"
|
|
return "\n\n".join(bloecke.values())
|
|
|
|
|
|
def einheiten_datei(datei_rel: str, rumpf: str, stand: str) -> str:
|
|
return f"# Einheiten: {datei_rel}\nstand: {stand}\n\n{rumpf}\n"
|
|
|
|
|
|
def lese_stand(text: str) -> str | None:
|
|
m = re.search(r"^stand:\s*(\S+)", text, re.M)
|
|
return m.group(1) if m else None
|
|
|
|
|
|
def alle_ids(text: str) -> set[str]:
|
|
"""Alle Einheiten-IDs + Zusatz-Anker einer Einheiten-Datei (für Referenz-Gates)."""
|
|
ids = {m.group(1) for m in re.finditer(r"^## (\S+)$", text, re.M)}
|
|
ids |= {m.group(1) for m in re.finditer(r"^anker:\s*(\S+)\s*$", text, re.M)}
|
|
return ids
|
|
|
|
|
|
def schreibe_planer(ziel_repo: Path, dateien: dict[str, str]) -> list[Path]:
|
|
"""Schreibt {relativer_pfad_unter_.planer: inhalt} ins Ziel-Repo. → geschriebene Pfade."""
|
|
geschrieben = []
|
|
for rel, inhalt in sorted(dateien.items()):
|
|
pfad = ziel_repo / ".planer" / rel
|
|
pfad.parent.mkdir(parents=True, exist_ok=True)
|
|
pfad.write_text(inhalt, encoding="utf-8")
|
|
geschrieben.append(pfad)
|
|
return geschrieben
|
|
|
|
|
|
def pruefung_geruest(wurzel: Path, projekt: str) -> str:
|
|
"""pruefung.md-Gerüst mit einfacher Erkennung (Makefile-Targets, pytest).
|
|
Fehlt etwas, steht dort explizit '(keins vorhanden)' — Gates degradieren sichtbar."""
|
|
tests = lint = start = "(keins vorhanden)"
|
|
makefile = wurzel / "Makefile"
|
|
if makefile.is_file():
|
|
targets = set(re.findall(r"^([\w-]+):", makefile.read_text(encoding="utf-8"), re.M))
|
|
if "test" in targets:
|
|
tests = "make test"
|
|
if "lint" in targets:
|
|
lint = "make lint"
|
|
for kandidat in ("dev", "run", "start"):
|
|
if kandidat in targets:
|
|
start = f"make {kandidat}"
|
|
break
|
|
elif list(wurzel.rglob("pytest.ini")) or list(wurzel.rglob("conftest.py")):
|
|
tests = "pytest"
|
|
return (f"# Prüfung: {projekt}\n"
|
|
f"tests: {tests}\nlint: {lint}\nstart: {start}\n"
|
|
f"hinweise:\n- (vom Scan erzeugtes Gerüst — bitte prüfen und ergänzen)\n")
|