#!/usr/bin/env python3 """pruefe_prosa.py — deterministische Prosa-Prüfungen (offline). Drei Prüfungen, jede aus einem realen Feldtest-Befund (Design 2026-08-11, Muster C und D; Prüfungen nur aus realen Fällen): (a) SHA-Zitate auflösen (F-012: sechs verwaiste Zitate, niemand konnte sie prüfen). Kandidaten sind Hex-Wörter (7–40 Zeichen, mindestens je ein Buchstabe und eine Ziffer) in Wurzel-*.md und docs/**, ohne docs/sources/ (die Zuordnungstabelle IST das Mapping). Auflösung: 1. Objekt existiert in diesem Repo (`git cat-file -e`), 2. SHA steht in der Zuordnungstabelle (alt→neu, ADR-0009), 3. Objekt existiert in einem Klon unter $NB_KOMPONENTEN (optional). Unauflösbar MIT gesetztem NB_KOMPONENTEN → FEHLER; ohne → als UNGEPRÜFT gelistet (sichtbar, kein stiller Skip). (b) Aufgabenmarker im Wiki (F-004: fünf Arbeitspunkte lebten nur in hosts/-Prosa, unsichtbar für das Board). Ein Absatz in docs/wiki/** mit Marker (Nächster Schritt / Offen: / TODO / offene Checkbox) muss einen Issue-Verweis tragen (#N, docs/issues/, /issues/-URL), sonst FEHLER. Bewusste Nicht-Aufgaben (z. B. Checklisten eines Verfahrens) tragen im selben Absatz das sichtbare Pragma ``. (c) Sperrliste stillgelegter Ziele (F-005: lebendes Dokument routete auf den toten Gitea-Tracker). Treffer → FEHLER. Usage: python scripts/pruefe_prosa.py [repo-root] """ from __future__ import annotations import os import re import subprocess import sys from pathlib import Path SHA_RE = re.compile(r"\b[0-9a-f]{7,40}\b") MARKER_RE = re.compile(r"Nächste[r]? Schritt|(?:^|\*\*)Offen:|TODO\b|^\s*[-*] \[ \]", re.M) ISSUE_REF_RE = re.compile(r"#\d+|docs/issues/|/issues/\d+") # F-005/ADR-0002: die Gitea-Tracker ALLER gespiegelten Repos sind seit dem # Umzug tot; verbindlich sind die git.lab-Issues (Zuordnung: Migrations- # Fußtext im jeweiligen GitLab-Issue). SPERRLISTE = [re.compile(r"rohana\.axion1337\.de/sorb/[^)\s]*/issues/")] ZUORDNUNG = "docs/sources/migration/commit-zuordnung-2026-08-07.md" # Kuratierte Ausnahmen: SHAGrund — nur für Zitate, deren Ziel # nachweislich und dokumentiert nicht mehr existiert (oder außerhalb # des Prüfbereichs liegt). Neue Einträge brauchen einen Grund. AUSNAHMEN = "scripts/sha_ausnahmen.tsv" def kandidaten(root: Path): for pattern in ("*.md", "docs/**/*.md"): for p in sorted(root.glob(pattern)): rel = p.relative_to(root).as_posix() if rel.startswith("docs/sources/") or p.name == "template.md": continue yield p, rel def obj_existiert(repo: Path, sha: str) -> bool: r = subprocess.run(["git", "-C", str(repo), "cat-file", "-e", f"{sha}^{{object}}"], capture_output=True) return r.returncode == 0 def main() -> int: root = Path(sys.argv[1]) if len(sys.argv) > 1 else Path.cwd() fehler: list[str] = [] ungeprueft: list[str] = [] zuordnung = "" zp = root / ZUORDNUNG if zp.is_file(): zuordnung = zp.read_text(encoding="utf-8") ausnahmen: set[str] = set() ap = root / AUSNAHMEN if ap.is_file(): for zeile in ap.read_text(encoding="utf-8").splitlines(): if zeile.strip() and not zeile.startswith("#"): ausnahmen.add(zeile.split("\t")[0].strip()) klone_dir = os.environ.get("NB_KOMPONENTEN", "") klone = [d for d in Path(klone_dir).iterdir() if (d / ".git").exists()] if klone_dir else [] for p, rel in kandidaten(root): text = p.read_text(encoding="utf-8") # (c) Sperrliste for muster in SPERRLISTE: for treffer in set(muster.findall(text)): fehler.append(f"{rel}: Verweis auf stillgelegtes Ziel " f"({treffer})") # (a) SHA-Zitate for sha in set(SHA_RE.findall(text)): if sha.isdigit() or not any(c.isdigit() for c in sha) \ or not any(c in "abcdef" for c in sha): continue if sha in ausnahmen: continue if obj_existiert(root, sha) or sha in zuordnung: continue if any(obj_existiert(k, sha) for k in klone): continue if klone: fehler.append(f"{rel}: SHA-Zitat unauflösbar: {sha}") else: ungeprueft.append(f"{rel}: {sha} (kein NB_KOMPONENTEN)") # (b) Aufgabenmarker, nur Wiki if rel.startswith("docs/wiki/"): for absatz in re.split(r"\n\s*\n", text): if "pruefe-prosa:ok" in absatz: continue if MARKER_RE.search(absatz) and not ISSUE_REF_RE.search(absatz): zeile = absatz.strip().splitlines()[0][:70] fehler.append(f"{rel}: Aufgabenprosa ohne " f"Issue-Verweis: „{zeile}…“") for f in fehler: print(f"FEHLER {f}") for u in ungeprueft: print(f"UNGEPRÜFT {u}") print(f"pruefe_prosa: {len(fehler)} Fehler, " f"{len(ungeprueft)} ungeprüfte SHA-Zitate") return 1 if fehler else 0 if __name__ == "__main__": sys.exit(main())