init
This commit is contained in:
101
backend/artefakte.py
Normal file
101
backend/artefakte.py
Normal file
@@ -0,0 +1,101 @@
|
||||
"""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")
|
||||
Reference in New Issue
Block a user