"""Fährt die Angriffsliste gegen regelwerk_check.py — je Fall auf einer Wegwerfkopie. Ersteller: Pascal Michel Erstellt: 2026-08-30 Angriffsliste und Begründung: .vault-tools/regelwerk-check/ENTWURF.md Der Treiber baut aus dem echten Vault einen Minimalvault, in dem die Prüfung sauber durchläuft — das ist die GRUNDLINIE. Läuft sie nicht grün, misst der Rest nichts und der Treiber bricht ab. Dazu normalisiert die Grundlinie jede Frontmatter-Zeile status: pending-review auf status: reference — ohne das bricht der Lauf nach jeder Selbstpflege an der Grundlinie ab, weil Prüfung 4 dann anschlägt (gemessen am 2026-09-23). Danach je Angriffsfall: frische Kopie der Grundlinie, genau eine Beschädigung, Prüfung fahren, Exit-Code und auslösende Meldung protokollieren. Ein Fall gilt als bestanden, wenn er den ERWARTETEN Textbaustein in der Ausgabe erzeugt — nicht schon, wenn der Exit-Code ungleich null ist. Sonst würde ein Absturz als Erfolg durchgehen. Aufruf: python .vault-tools/regelwerk-check/regressionslauf.py """ from __future__ import annotations import re import shutil import subprocess import sys import tempfile from pathlib import Path HIER = Path(__file__).resolve().parent VAULT = HIER.parent.parent PRUEFUNG = HIER / "regelwerk_check.py" WIKILINK = re.compile(r"\[\[([^\]]+)\]\]") # Fuer den Aufbau der Grundlinie dieselbe codeblock-bewusste Extraktion # verwenden wie das Gate. Sonst legt der Treiber Platzhalterdateien fuer # Nicht-Links an - ":space:" aus einer POSIX-Zeichenklasse ist unter # Windows kein gueltiger Dateiname und liess ihn am 2026-09-03 abstuerzen. sys.path.insert(0, str(Path(__file__).resolve().parent)) from regelwerk_check import wikilinks # noqa: E402 def fahre(vault: Path) -> tuple[int, str]: lauf = subprocess.run( [sys.executable, str(PRUEFUNG), "--vault", str(vault)], capture_output=True, text=True, encoding="utf-8", ) return lauf.returncode, (lauf.stdout or "") + (lauf.stderr or "") def baue_grundlinie(ziel: Path) -> None: """Kopiert den Geltungsbereich und legt jedes Linkziel als leere Datei an.""" (ziel / "AI" / "Regelwerk").mkdir(parents=True) (ziel / ".claude" / "agents").mkdir(parents=True) shutil.copy2(VAULT / "CLAUDE.md", ziel / "CLAUDE.md") for f in (VAULT / "AI" / "Regelwerk").glob("*.md"): shutil.copy2(f, ziel / "AI" / "Regelwerk" / f.name) for f in (VAULT / ".claude" / "agents").glob("*.md"): shutil.copy2(f, ziel / ".claude" / "agents" / f.name) for skill in (VAULT / ".claude" / "skills").glob("*/SKILL.md"): unter = ziel / ".claude" / "skills" / skill.parent.name unter.mkdir(parents=True, exist_ok=True) shutil.copy2(skill, unter / "SKILL.md") # Prüfung 7 liest die gespeicherten Workflows. copy2 kopiert Bytes, die Grundlinie # trägt also dieselben Zeilenenden wie der Vault. (ziel / ".claude" / "workflows").mkdir(parents=True) for f in (VAULT / ".claude" / "workflows").glob("*.js"): shutil.copy2(f, ziel / ".claude" / "workflows" / f.name) # jede Zeile status: pending-review auf status: reference normalisieren. # Ohne das bricht der Lauf nach jeder Selbstpflege an der Grundlinie ab, # weil Prüfung 4 (Freigabe) dann anschlägt — gemessen am 2026-09-23. for datei in (ziel / "AI" / "Regelwerk").glob("*.md"): t = datei.read_text(encoding="utf-8") neu = re.sub(r"^status: pending-review$", "status: reference", t, flags=re.M) if neu != t: datei.write_text(neu, encoding="utf-8") # Jedes Linkziel außerhalb des Geltungsbereichs als leere Datei anlegen, # damit die Grundlinie nicht an fehlendem Umfeld scheitert. for datei in list(ziel.rglob("*.md")): for roh in wikilinks(datei.read_text(encoding="utf-8-sig")): zielname = roh.split("|", 1)[0].split("#", 1)[0].strip() if not zielname: continue pfad = ziel / f"{zielname}.md" if not pfad.exists() and not (ziel / zielname).exists(): pfad.parent.mkdir(parents=True, exist_ok=True) pfad.write_text("# Platzhalter\n", encoding="utf-8") # --------------------------------------------------------------- Beschädigungen def s_pflichtfeld_fehlt(v: Path) -> None: f = v / "AI" / "Regelwerk" / "codeworkflow.md" t = f.read_text(encoding="utf-8") f.write_text(re.sub(r"^summary:.*\n", "", t, count=1, flags=re.M), encoding="utf-8") def s_status_leer(v: Path) -> None: f = v / "AI" / "Regelwerk" / "schreibstil.md" t = f.read_text(encoding="utf-8") f.write_text(re.sub(r"^status:.*$", "status:", t, count=1, flags=re.M), encoding="utf-8") def s_datei_leer(v: Path) -> None: (v / "AI" / "Regelwerk" / "projekte.md").write_text("", encoding="utf-8") def s_nicht_verlinkt(v: Path) -> None: f = v / "CLAUDE.md" t = f.read_text(encoding="utf-8") f.write_text(re.sub(r"^.*AI/Regelwerk/pruefwerkzeuge.*\n", "", t, count=1, flags=re.M), encoding="utf-8") def s_neue_unverlinkte_datei(v: Path) -> None: (v / "AI" / "Regelwerk" / "zzz-neu.md").write_text( "---\ntype: resource\nstatus: reference\ntitle: Neu\nsummary: x\n" "tags: [a]\ncreated: 2026-08-30\nupdated: 2026-08-30\n---\n\n# Neu\n", encoding="utf-8", ) def s_falsches_modell(v: Path) -> None: f = v / ".claude" / "agents" / "pruefer.md" t = f.read_text(encoding="utf-8") f.write_text(re.sub(r"^model:.*$", "model: sonnet", t, count=1, flags=re.M), encoding="utf-8") def s_toter_link_erste_position(v: Path) -> None: """Reihenfolge-Fall: Schaden an der ERSTEN Linkposition der Datei.""" f = v / "AI" / "Regelwerk" / "codeworkflow.md" t = f.read_text(encoding="utf-8") f.write_text(WIKILINK.sub(lambda m: "[[gibtsnicht-erste]]", t, count=1), encoding="utf-8") def s_toter_link_letzte_position(v: Path) -> None: """Reihenfolge-Fall: derselbe Schaden an der LETZTEN Linkposition.""" f = v / "AI" / "Regelwerk" / "codeworkflow.md" t = f.read_text(encoding="utf-8") treffer = list(WIKILINK.finditer(t)) letzter = treffer[-1] f.write_text(t[: letzter.start()] + "[[gibtsnicht-letzte]]" + t[letzter.end() :], encoding="utf-8") def s_planablage_nur_dateiform(v: Path) -> None: f = v / ".claude" / "agents" / "planer.md" t = f.read_text(encoding="utf-8") # Ordnerform entfernen, Dateiform stehen lassen — der Fehler, der am # 2026-08-30 tatsächlich in dieser Datei stand. t = re.sub(r"\*\*Ablageform hängt am Umfang\.\*\*.*?\n", "", t, count=1, flags=re.S) t = t.replace( "Erlaubt ist ausschließlich `docs/plans/` im Zielprojekt.", "Erlaubt ist ausschließlich `docs/plans/JJJJ-MM-TT-.md` im Zielprojekt.", ) f.write_text(t, encoding="utf-8") def s_kein_stamm(v: Path) -> None: (v / "CLAUDE.md").unlink() def s_kein_regelwerk(v: Path) -> None: shutil.rmtree(v / "AI" / "Regelwerk") def s_keine_rollentabelle(v: Path) -> None: f = v / "CLAUDE.md" t = f.read_text(encoding="utf-8") f.write_text(re.sub(r"^\|.*$", "", t, flags=re.M), encoding="utf-8") def s_effort_wertebereich_xhigh(v: Path) -> None: f = v / ".claude" / "agents" / "pruefer.md" t = f.read_text(encoding="utf-8") muster = r"^effort: high$" assert len(re.findall(muster, t, flags=re.M)) == 1 f.write_text(re.sub(muster, "effort: xhigh", t, count=1, flags=re.M), encoding="utf-8") def s_effort_wertebereich_ungueltig(v: Path) -> None: f = v / ".claude" / "agents" / "planer.md" t = f.read_text(encoding="utf-8") muster = r"^effort: high$" assert len(re.findall(muster, t, flags=re.M)) == 1 f.write_text(re.sub(muster, "effort: hoch", t, count=1, flags=re.M), encoding="utf-8") def s_effort_fehlt_erste(v: Path) -> None: """Reihenfolge-Fall: effort: fehlt in der ERSTEN Agentendatei (executer.md).""" f = v / ".claude" / "agents" / "executer.md" t = f.read_text(encoding="utf-8") muster = r"^effort:.*\n" assert len(re.findall(muster, t, flags=re.M)) == 1 f.write_text(re.sub(muster, "", t, count=1, flags=re.M), encoding="utf-8") def s_effort_fehlt_letzte(v: Path) -> None: """Reihenfolge-Fall: derselbe Schaden in der LETZTEN Agentendatei (recherche.md).""" f = v / ".claude" / "agents" / "recherche.md" t = f.read_text(encoding="utf-8") muster = r"^effort:.*\n" assert len(re.findall(muster, t, flags=re.M)) == 1 f.write_text(re.sub(muster, "", t, count=1, flags=re.M), encoding="utf-8") def s_effort_null(v: Path) -> None: f = v / ".claude" / "agents" / "planer.md" t = f.read_text(encoding="utf-8") muster = r"^effort:.*$" assert len(re.findall(muster, t, flags=re.M)) == 1 f.write_text(re.sub(muster, "effort:", t, count=1, flags=re.M), encoding="utf-8") def s_effort_spalte_fehlt(v: Path) -> None: """Entfernt in der Rollentabelle bei der Kopfzeile und allen direkt folgenden Tabellenzeilen die letzte Zelle — die Effort-Spalte.""" f = v / "CLAUDE.md" zeilen = f.read_text(encoding="utf-8").split("\n") kopfzeilen = [ i for i, z in enumerate(zeilen) if z.startswith("|") and "Agent" in z and "Effort" in z ] assert len(kopfzeilen) == 1 start = kopfzeilen[0] ende = start while ende < len(zeilen) and zeilen[ende].startswith("|"): ende += 1 for i in range(start, ende): teile = zeilen[i].split("|") kern = teile[1:-1] if len(kern) < 2: continue zeilen[i] = "|" + "|".join(kern[:-1]) + "|" f.write_text("\n".join(zeilen), encoding="utf-8") def s_effort_verweis_leer(v: Path) -> None: """Entfernt in der Tabellenzeile mit `pruefer` die Backticks um 'high'.""" f = v / "CLAUDE.md" zeilen = f.read_text(encoding="utf-8").split("\n") treffer = [i for i, z in enumerate(zeilen) if z.startswith("|") and "`pruefer`" in z] assert len(treffer) == 1 i = treffer[0] assert zeilen[i].count("`high`") == 1 zeilen[i] = zeilen[i].replace("`high`", "high", 1) f.write_text("\n".join(zeilen), encoding="utf-8") def s_effort_freigabe_pending(v: Path) -> None: f = v / "AI" / "Regelwerk" / "schreibstil.md" t = f.read_text(encoding="utf-8") muster = r"^status:.*$" assert len(re.findall(muster, t, flags=re.M)) == 1 f.write_text(re.sub(muster, "status: pending-review", t, count=1, flags=re.M), encoding="utf-8") def g_effort_anfuehrung(v: Path) -> None: """Kein Schaden: effort in passenden Anführungszeichen ist gültiges YAML. Muss stumm bleiben — Regressionslauf-Fall zu Punkt 4 der Effort-Prüfung (Prüfwerkzeuge.md, Abschnitt 5: neuer Code bekommt seinen eigenen Fall). """ f = v / ".claude" / "agents" / "pruefer.md" t = f.read_text(encoding="utf-8") muster = r"^effort: high$" assert len(re.findall(muster, t, flags=re.M)) == 1 f.write_text(re.sub(muster, 'effort: "high"', t, count=1, flags=re.M), encoding="utf-8") def g_zusatzspalte(v: Path) -> None: """Kein Schaden: eine Spalte hinter Effort mit eigenem Backtick-Wert. Muss stumm bleiben — die Effort-Spalte wird über ihre Überschrift gefunden, nicht über die letzte Spalte. """ f = v / "CLAUDE.md" zeilen = f.read_text(encoding="utf-8").split("\n") kopfzeilen = [ i for i, z in enumerate(zeilen) if z.startswith("|") and "Agent" in z and "Effort" in z ] assert len(kopfzeilen) == 1 start = kopfzeilen[0] zeilen[start] = zeilen[start].rstrip() + " Notiz |" zeilen[start + 1] = zeilen[start + 1].rstrip() + " --- |" i = start + 2 while i < len(zeilen) and zeilen[i].startswith("|"): zeilen[i] = zeilen[i].rstrip() + " `xhigh` |" i += 1 f.write_text("\n".join(zeilen), encoding="utf-8") def g_zweiter_backtick(v: Path) -> None: """Kein Schaden: ein zweiter Backtick-Ausdruck in der Effort-Zelle. Muss stumm bleiben — genommen wird der erste Backtick-Ausdruck. """ f = v / "CLAUDE.md" zeilen = f.read_text(encoding="utf-8").split("\n") treffer = [i for i, z in enumerate(zeilen) if z.startswith("|") and "`pruefer`" in z] assert len(treffer) == 1 i = treffer[0] assert zeilen[i].count("`high` (Frontmatter)") == 1 zeilen[i] = zeilen[i].replace( "`high` (Frontmatter)", "`high` (Frontmatter, nicht `xhigh`)", 1 ) f.write_text("\n".join(zeilen), encoding="utf-8") def g_link_im_codeblock(v: Path) -> None: """Kein Schaden, sondern eine Falle: Wikilink-artiges IN Code. Muss stumm bleiben. Vor dem 2026-09-03 hat genau das drei Fehlalarme erzeugt und den Aufbau der Grundlinie zum Absturz gebracht, weil der Treiber fuer ":space:" eine Datei anlegen wollte. """ f = v / "AI" / "Regelwerk" / "codeworkflow.md" t = f.read_text(encoding="utf-8") zeilen = [ "", "## Gegenfall (Regressionslauf)", "", "```bash", "if [[ $EUID -eq 0 ]]; then exit 1; fi", "grep -o '[[:space:]]' datei", "```", "", "Im Fliesstext: `[[bild.png]]` und `[[gibtsnicht-in-backticks]]`.", "", ] f.write_text(t + "\n".join(zeilen), encoding="utf-8") # Steuerzeichen entstehen nur ueber Codepunkte, nie als Escape-Folge im Quelltext: Die # Schreibschicht hat solche Folgen schon still in echte Zeichen verwandelt # (codeworkflow.md, "Escape-Folgen und unsichtbare Zeichen"). CR = bytes([13]) LF = bytes([10]) def _workflows(v: Path) -> list[Path]: dateien = sorted((v / ".claude" / "workflows").glob("*.js")) assert len(dateien) >= 2, "die Grundlinie braucht mindestens zwei gespeicherte Workflows" return dateien def _haenge_an(f: Path, zusatz: bytes) -> None: f.write_bytes(f.read_bytes() + zusatz) def s_workflow_crlf_erste_datei(v: Path) -> None: f = _workflows(v)[0] f.write_bytes(f.read_bytes().replace(LF, CR + LF)) def s_workflow_cr_letzte_datei_letzte_zeile(v: Path) -> None: f = _workflows(v)[-1] f.write_bytes(f.read_bytes().rstrip(LF) + CR + LF) def s_workflow_nul(v: Path) -> None: _haenge_an(_workflows(v)[0], b"// " + bytes([0]) + LF) def s_workflow_bidi(v: Path) -> None: _haenge_an(_workflows(v)[0], ("// " + chr(0x202E) + "x").encode("utf-8") + LF) def s_workflow_bom(v: Path) -> None: f = _workflows(v)[-1] f.write_bytes(chr(0xFEFF).encode("utf-8") + f.read_bytes()) def s_workflow_tab(v: Path) -> None: _haenge_an(_workflows(v)[0], b"//" + bytes([9]) + b"x" + LF) def s_workflow_kein_utf8(v: Path) -> None: _haenge_an(_workflows(v)[0], b"// " + bytes([0xFF]) + LF) def s_workflow_unterordner(v: Path) -> None: unter = v / ".claude" / "workflows" / "unter" unter.mkdir() (unter / "tief.js").write_bytes(b"a" + CR + LF) def g_workflow_typografie(v: Path) -> None: """Kein Schaden: typografische Zeichen und Umlaute sind keine Steuerzeichen.""" zeichen = "".join(chr(c) for c in (0x201E, 0x201C, 0x2013, 0x2014, 0x2026, 0xE4, 0x20AC)) _haenge_an(_workflows(v)[0], ("// " + zeichen).encode("utf-8") + LF) def g_workflow_ordner_fehlt(v: Path) -> None: """Kein Schaden: ohne Ordner gibt es nichts zu pruefen.""" shutil.rmtree(v / ".claude" / "workflows") # Gegenfaelle: duerfen NICHT anschlagen. Ohne sie waere die Codeblock- # Erkennung selbst ungeprueft — neuer Code in einer Fix-Runde bekommt # seinen eigenen Fall (pruefwerkzeuge.md, Abschnitt 5). GEGENFAELLE = [ ("codeblock | Wikilink-artiges in Code und Backticks", g_link_im_codeblock), ("anfuehrung | effort in Anführungszeichen", g_effort_anfuehrung), ("zusatzspalte | Spalte mit Backtick-Wert hinter Effort", g_zusatzspalte), ("zweiter-backtick | Effort-Zelle mit zweitem Backtick-Wert", g_zweiter_backtick), ("typografie | Anführung, Striche, Umlaut im Workflow", g_workflow_typografie), ("ordner-fehlt | kein Workflow-Ordner", g_workflow_ordner_fehlt), ] FAELLE = [ ("fehlt | Pflichtfeld summary entfernt", s_pflichtfeld_fehlt, "Pflichtfeld fehlt: summary"), ("null | status ohne Wert", s_status_leer, "Pflichtfeld ohne Wert: status"), ("leer | Regelwerk-Datei auf 0 Byte", s_datei_leer, "Datei ist leer"), ("fehlt | im Stamm nicht mehr verlinkt", s_nicht_verlinkt, "im Stamm nicht verlinkt"), ("zusaetzlich| neue Datei, die niemand verlinkt", s_neue_unverlinkte_datei, "zzz-neu.md"), ("wertebereich| model: sonnet statt opus", s_falsches_modell, "Rollentabelle im Stamm sagt opus"), ("verweis | toter Wikilink, ERSTE Position", s_toter_link_erste_position, "gibtsnicht-erste"), ("verweis | toter Wikilink, LETZTE Position", s_toter_link_letzte_position, "gibtsnicht-letzte"), ("konvention | Dateiform ohne Ordnerform", s_planablage_nur_dateiform, "ohne die Ordner-Form"), ("umgebung | CLAUDE.md fehlt", s_kein_stamm, "fehlt — nichts prüfbar"), ("umgebung | AI/Regelwerk fehlt", s_kein_regelwerk, "Verzeichnis fehlt"), ("umgebung | Rollentabelle unauffindbar", s_keine_rollentabelle, "Rollentabelle nicht auffindbar"), ("wertebereich| effort: xhigh statt high", s_effort_wertebereich_xhigh, "effort: xhigh — Rollentabelle im Stamm sagt high"), ("wertebereich| effort: hoch (ungültig)", s_effort_wertebereich_ungueltig, "ist nicht in der Vorgabe"), ("fehlt | effort fehlt, ERSTE Agentendatei", s_effort_fehlt_erste, ".claude/agents/executer.md — kein effort: im Frontmatter"), ("fehlt | effort fehlt, LETZTE Agentendatei", s_effort_fehlt_letzte, ".claude/agents/recherche.md — kein effort: im Frontmatter"), ("null | effort ohne Wert", s_effort_null, ".claude/agents/planer.md — kein effort: im Frontmatter"), ("umgebung | Effort-Spalte fehlt", s_effort_spalte_fehlt, "Effort-Spalte der Rollentabelle nicht auffindbar"), ("verweis | Effort-Zelle ohne Wert", s_effort_verweis_leer, "Rollentabelle nennt für 'pruefer' keinen Effort"), ("freigabe | status: pending-review", s_effort_freigabe_pending, "Freigabe steht aus"), ("zeichen | CRLF in der ERSTEN Workflowdatei", s_workflow_crlf_erste_datei, "U+000D"), ("zeichen | CR in der LETZTEN Zeile der LETZTEN Workflowdatei", s_workflow_cr_letzte_datei_letzte_zeile, "U+000D"), ("zeichen | NUL", s_workflow_nul, "U+0000"), ("zeichen | Bidi-Umkehr", s_workflow_bidi, "U+202E"), ("zeichen | BOM am Dateianfang", s_workflow_bom, "Zeile 1, Spalte 1: U+FEFF"), ("zeichen | Tab", s_workflow_tab, "U+0009"), ("kodierung | Workflow ist kein UTF-8", s_workflow_kein_utf8, "nicht als UTF-8 lesbar"), ("zusaetzlich| CR in einem Unterordner", s_workflow_unterordner, ".claude/workflows/unter/tief.js"), ] def main() -> int: with tempfile.TemporaryDirectory(prefix="regelwerk-regression-") as tmp: basis = Path(tmp) / "grundlinie" baue_grundlinie(basis) code, ausgabe = fahre(basis) print("=" * 78) print("GRUNDLINIE — unbeschädigte Kopie") print("=" * 78) print(ausgabe.strip()) if code != 0: print("\nABBRUCH: Die Grundlinie ist nicht grün. Der Rest misst nichts.") return 2 print("Grundlinie grün (exit 0).\n") print("=" * 78) print("ANGRIFFSLISTE") print("=" * 78) bestanden = 0 for name, schaden, erwartet in FAELLE: fall = Path(tmp) / "fall" if fall.exists(): shutil.rmtree(fall) shutil.copytree(basis, fall) schaden(fall) code, ausgabe = fahre(fall) treffer = erwartet in ausgabe zeile = next((z for z in ausgabe.splitlines() if erwartet in z), "") status = "ANGESCHLAGEN" if (treffer and code == 1) else "DURCHGELASSEN" if treffer and code == 1: bestanden += 1 print(f"{status:15} exit={code} {name}") print(f"{'':15} -> {zeile.strip() or '(erwartete Meldung fehlt)'}") print() print("=" * 78) print("GEGENFÄLLE — müssen stumm bleiben") print("=" * 78) stumm = 0 for name, harmlos in GEGENFAELLE: fall = Path(tmp) / "gegenfall" if fall.exists(): shutil.rmtree(fall) shutil.copytree(basis, fall) harmlos(fall) code, ausgabe = fahre(fall) ok = code == 0 if ok: stumm += 1 print(f"{'STUMM' if ok else 'FEHLALARM':15} exit={code} {name}") if not ok: for z in ausgabe.splitlines(): if z.strip().startswith("["): print(f"{'':15} -> {z.strip()}") print() # Beide Zahlen werden gerechnet, nicht geschrieben. print(f"{bestanden} von {len(FAELLE)} Angriffsfällen haben angeschlagen.") print(f"{stumm} von {len(GEGENFAELLE)} Gegenfällen sind stumm geblieben.") return 0 if (bestanden == len(FAELLE) and stumm == len(GEGENFAELLE)) else 1 if __name__ == "__main__": try: sys.stdout.reconfigure(encoding="utf-8") except AttributeError: pass raise SystemExit(main())