Regelwerk, Agenten, Skills und Werkzeuge veröffentlichen

Erster Stand des öffentlichen Ausschnitts aus dem Vault. Die .gitignore
arbeitet als Positivliste: ausgeschlossen ist alles, eingeschlossen nur,
was ausdrücklich wieder aufgenommen wird. Notizen, Projekte, Finanzen,
das Sitzungsgedächtnis und settings.local.json bleiben damit außen vor.

Der Kundenname in AI/Regelwerk/seo.md ist durch eine neutrale
Bezeichnung ersetzt; die Regeln selbst sind unverändert.

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
Pascal MichelandClaude Opus 5 committed 2026-09-17 22:38:15 +02:00
commit 2c9a4475fc
97 files changed
+14627

No files matched your search

+64
View File
@@ -0,0 +1,64 @@
# Entwurf: Konsistenzprüfung der Anweisungsschicht
Geschrieben am 2026-08-30 **vor** der ersten Zeile Code, wie es [[AI/Regelwerk/pruefwerkzeuge|pruefwerkzeuge.md]] §1 verlangt.
## Was die Prüfung beantworten soll — und woher die Antwort kommt
| # | Frage | Herkunft | Auflösung |
|---|---|---|---|
| 1 | Zeigt jeder Wikilink auf eine existierende Datei? | **Quelle** — Dateisystem | Ziel ist `<link>.md` oder `<link>` relativ zum Vault-Root. Alias nach `\|` wird abgeschnitten, Anker nach `#` ebenso. |
| 2 | Trägt jede Regelwerk-Datei die Pflichtfelder im Frontmatter? | **Projektvorgabe** — abgelesen an den sieben bestehenden Dateien | `type, status, title, summary, tags, created, updated` |
| 3 | Ist jede Regelwerk-Datei vom Stamm aus verlinkt? | **Projektvorgabe** — CLAUDE.md, Abschnitt „Regelwerk" | Eine Datei, die niemand verlinkt, wird nie geladen. |
| 4 | Steht eine Regelwerk-Datei auf `status: pending-review`? | **Projektvorgabe** — CLAUDE.md, Abschnitt „Selbstpflege" | Hinweis, kein Fehler: die Freigabe steht aus. |
| 5 | Weicht das `model:` einer Agentendefinition von der Rollentabelle im Stamm ab? | **Projektvorgabe** — CLAUDE.md, Tabelle „Agenten & Modelle" | Die Tabelle wird **gelesen**, nicht im Skript nachgebaut. |
| 6 | Nennen mehrere Dateien die Planablage-Konvention widersprüchlich? | **Projektvorgabe** — `codeworkflow.md` ist maßgeblich | Siehe Auflösung unten. |
## Die drei Annahmen, die vor dem Code aufgelöst wurden
**Annahme A: „Ein Pfad in Backticks ist ein Vault-Pfad."** Falsch. Backticks tragen auch Befehle (`python …`), Frontmatter-Schlüssel (`modell:`), Dateinamen ohne Ort (`CHANGELOG.md`) und Beispielwerte. Eine allgemeine Pfadprüfung wäre eine Heuristik im Positivlisten-Gewand.
→ **Aufgelöst durch Streichung.** Frage 2 der ersten Fassung („existiert jeder Pfad in Backticks") ist nicht Teil der Prüfung. Wikilinks werden geprüft, Backtick-Pfade nicht. Das steht als benannte Lücke im Kopfkommentar.
**Annahme B: „Aus Fließtext lässt sich ablesen, welches Modell ein Agent fährt."** Falsch. „Executer-Subagent (`executer`, Sonnet — bei reiner Abschrift Haiku…)" ist für einen Menschen eindeutig und für ein Skript nicht.
→ **Aufgelöst durch Einschränkung des Geltungsbereichs.** Geprüft wird ausschließlich das `model:`-Feld im Frontmatter der Agentendateien gegen die Rollentabelle im Stamm. Fließtextaussagen über Modelle prüft das Skript **nicht** — benannte Lücke.
**Annahme C: „Eine widersprüchliche Konvention ist maschinell erkennbar."** Nur eingeschränkt.
→ **Aufgelöst durch eine erzwungene Form.** Das Skript sucht das Literal `docs/plans/JJJJ-MM-TT` und klassifiziert das unmittelbar folgende Zeichen: `/` = Ordnerform, `.` = Dateiform. Regel: **Wer die Dateiform nennt, ohne die Ordnerform zu nennen, ist ein Befund** — denn `codeworkflow.md` schreibt beide vor und die Ordnerform ist die, die vergessen wird. Genau dieser Fall lag am 2026-08-30 in `planer.md` vor.
## Positivliste, nicht Negativliste
Alle sechs Prüfungen schränken **geschlossene** Mengen ein: die Menge der Wikilinks in einer Datei, die Menge der Pflichtfelder, die Menge der Agentendateien, die Menge der Regelwerk-Dateien. Es gibt keine Prüfung, die eine offene Menge verbotener Muster abzählt — und damit auch keine handgepflegte Negativliste, die gegen sie verlieren könnte.
Die Begründung gilt **nur** für diese sechs Achsen. Sie gilt ausdrücklich nicht für die Frage, ob zwei Regeln inhaltlich dasselbe sagen. Das sieht die Prüfung nicht und kann es nicht sehen.
## Angriffsliste — einmal vollständig
Je Fall eine Wegwerfkopie des Vaults, Rücksetzen dazwischen, Exit-Code und auslösende Meldung protokolliert. Vor allem läuft die **Grundlinie**: die unbeschädigte Kopie muss sauber durchlaufen, sonst misst der Rest nichts.
| Klasse | Fall |
|---|---|
| Grundlinie | unveränderter Vault → Exit 0 |
| fehlt | Pflichtfeld `summary` aus einer Regelwerk-Datei entfernt |
| fehlt | Regelwerk-Datei nicht mehr im Stamm verlinkt |
| null | `status:` auf leeren Wert gesetzt |
| leer | Regelwerk-Datei auf 0 Byte gekürzt |
| zusätzlich | neue Regelwerk-Datei, die niemand verlinkt |
| Wertebereich | `model: sonnet` in `pruefer.md` (Tabelle sagt `opus`) |
| Verweis ins Leere | Wikilink auf nicht existierende Datei |
| Konvention | Dateiform der Planablage ohne Ordnerform |
| **Reihenfolge** | toter Wikilink an **erster** und an **letzter** Position derselben Datei |
| Umgebung | `--vault` zeigt auf ein Verzeichnis ohne CLAUDE.md |
| Umgebung | `AI/Regelwerk/` fehlt vollständig |
| Umgebung | Rollentabelle im Stamm nicht auffindbar |
Die letzten drei sind die wichtigsten: Sie prüfen, dass eine fehlende Voraussetzung ein **Befund** ist und kein Grund zu schweigen (§6).
## Abbruchbedingung, vor der ersten Runde benannt
Schluss ist, wenn eine Runde nur noch Umformulierungen findet oder wenn alle verbleibenden Befunde dieselbe Antwort haben. Die Prüfung ist fertig, wenn alle dreizehn Fälle der Angriffsliste anschlagen und die Grundlinie grün ist — nicht, wenn sie „durchläuft".
## Was diese Prüfung ausdrücklich nicht ist
Ein Fangnetz gegen formale Widersprüche. **Kein Nachweis, dass die Anweisungsschicht widerspruchsfrei ist.** Zwei Dateien können dieselbe Regel in verschiedenen Worten gegensätzlich formulieren, und die Prüfung meldet null Befunde. Der belastbare Nachweis ist das Lesen — die Prüfung nimmt nur die Formalien aus dem Weg, damit beim Lesen Kontext für den Inhalt bleibt.
@@ -0,0 +1,386 @@
"""Prüft die Anweisungsschicht des Vaults auf formale Widersprüche.
Ersteller: Pascal Michel
Erstellt: 2026-08-30
Entwurf und Angriffsliste: .vault-tools/regelwerk-check/ENTWURF.md
===============================================================================
GELTUNGSBEREICH — was diese Prüfung sieht
===============================================================================
Geprüft werden genau vier Orte:
CLAUDE.md der Stamm
AI/Regelwerk/*.md die ausgelagerten Regeln
.claude/skills/*/SKILL.md die Skills
.claude/agents/*.md die Agentendefinitionen
Sechs Prüfungen, jede über eine GESCHLOSSENE Menge:
1 Wikilinks Zeigt jeder [[Verweis]] auf eine existierende Datei?
2 Frontmatter Trägt jede Regelwerk-Datei die sieben Pflichtfelder?
3 Erreichbarkeit Ist jede Regelwerk-Datei vom Stamm aus verlinkt?
4 Freigabe Steht eine Regelwerk-Datei auf status: pending-review?
5 Modell Weicht model: einer Agentendatei von der Rollentabelle ab?
6 Planablage Nennt eine Datei die Dateiform ohne die Ordnerform?
Die Rollentabelle und die Liste der Regelwerk-Verweise werden AUS CLAUDE.md
GELESEN, nicht hier nachgebaut. Damit kann die Prüfung nicht gegen eine
veraltete Kopie im eigenen Quelltext laufen.
===============================================================================
WAS DIESE PRÜFUNG NICHT SIEHT — benannte Lücken
===============================================================================
* Pfade in Backticks. `python …`, `modell:` und `CHANGELOG.md` stehen alle
in Backticks und sind keine Vault-Pfade. Eine allgemeine Pfadprüfung wäre
eine Heuristik im Positivlisten-Gewand. Bewusst weggelassen.
* Modellaussagen im Fließtext. "(`executer`, Sonnet — bei reiner Abschrift
Haiku)" ist für einen Menschen eindeutig und für ein Skript nicht. Geprüft
wird ausschließlich das model:-Feld im Frontmatter.
* INHALTLICHE Widersprüche. Zwei Dateien können dieselbe Regel in
verschiedenen Worten gegensätzlich formulieren, und diese Prüfung meldet
null Befunde. Genau der Fehler, der am 2026-08-30 gefunden wurde — Skill
und Agent sagten das Gegenteil des Regelwerks — wäre nur zur Hälfte
aufgefallen: die Planablage (Prüfung 6) ja, "fortsetzen statt frisch
starten" nein.
===============================================================================
DIESES SKRIPT IST EIN FANGNETZ, KEIN NACHWEIS.
===============================================================================
Ein grüner Lauf belegt, dass keine der sechs formalen Achsen verletzt ist.
Er belegt NICHT, dass die Anweisungsschicht widerspruchsfrei ist. Der
belastbare Nachweis ist das Lesen. Diese Prüfung nimmt nur die Formalien aus
dem Weg, damit beim Lesen Kontext für den Inhalt bleibt.
Das Skript liegt unter .vault-tools/ und damit AUSSERHALB des eigenen
Geltungsbereichs — es misst seinen eigenen Erklärtext nicht mit.
Exit: 0 = kein Befund, 1 = Befunde, 2 = Abbruch (Befunde werden trotzdem
ausgegeben).
"""
from __future__ import annotations
import argparse
import re
import sys
from pathlib import Path
PFLICHTFELDER = ("type", "status", "title", "summary", "tags", "created", "updated")
REGELWERK = "AI/Regelwerk"
AGENTEN = ".claude/agents"
SKILLS = ".claude/skills"
WIKILINK = re.compile(r"\[\[([^\]]+)\]\]")
# Codebloecke ausblenden, bevor Wikilinks gelesen werden. Ohne das zaehlen
# `[[ $EUID -eq 0 ]]` (Bash), `[[:space:]]` (POSIX-Zeichenklasse) und
# Doku-Platzhalter wie `[[bild.png]]` als Verweise. Am 2026-09-03 hat genau
# das drei Fehlalarme in navigation.md erzeugt und den Regressionslauf zum
# Absturz gebracht: er legt fuer jedes Linkziel eine Datei an, und ":space:"
# ist unter Windows kein gueltiger Dateiname.
#
# Die Funktion liegt an einer Stelle: .vault-tools/linkcheck/linkcheck.py.
# Dort stehen auch ihr Entwurf und die 17 Angriffsfaelle (faelle.py).
_LINKCHECK = Path(__file__).resolve().parent.parent / "linkcheck"
if str(_LINKCHECK) not in sys.path:
sys.path.insert(0, str(_LINKCHECK))
try:
from linkcheck import strip_code as _strip_code
except ImportError as _exc: # fehlende Voraussetzung ist ein Befund
raise SystemExit(
"ABBRUCH: %s/linkcheck.py nicht importierbar (%s). Ohne die "
"Codeblock-Erkennung wuerde diese Pruefung Fehlalarme melden."
% (_LINKCHECK, _exc))
def wikilinks(text: str) -> list[str]:
"""Alle Wikilink-Ziele ausserhalb von Codebloecken und Code-Spans."""
return WIKILINK.findall(_strip_code(text))
PLANPFAD = re.compile(r"docs/plans/JJJJ-MM-TT[^\s`]*")
BACKTICK = re.compile(r"`([^`]+)`")
class Befund:
def __init__(self, schwere: str, ort: str, text: str) -> None:
self.schwere = schwere
self.ort = ort
self.text = text
def __str__(self) -> str:
return f"[{self.schwere}] {self.ort} — {self.text}"
def lies(pfad: Path) -> str:
"""Liest eine Datei als UTF-8 und verschluckt kein Problem still."""
return pfad.read_text(encoding="utf-8-sig")
def frontmatter(text: str) -> dict[str, str] | None:
"""Gibt das YAML-Frontmatter als flaches Wörterbuch zurück, oder None."""
zeilen = text.split("\n")
if not zeilen or zeilen[0].strip() != "---":
return None
for i in range(1, len(zeilen)):
if zeilen[i].strip() == "---":
werte: dict[str, str] = {}
for zeile in zeilen[1:i]:
schluessel, trenner, wert = zeile.partition(":")
if trenner and not zeile.startswith((" ", "\t", "-")):
werte[schluessel.strip()] = wert.strip()
return werte
return None
def linkziel(roh: str) -> str:
"""Schneidet Alias und Anker ab: 'AI/x|Anzeige' und 'AI/x#Kapitel' -> 'AI/x'."""
ziel = roh.split("|", 1)[0]
ziel = ziel.split("#", 1)[0]
return ziel.strip()
def dateien_im_geltungsbereich(vault: Path) -> list[Path]:
treffer: list[Path] = []
stamm = vault / "CLAUDE.md"
if stamm.is_file():
treffer.append(stamm)
regelwerk = vault / REGELWERK
if regelwerk.is_dir():
treffer.extend(sorted(regelwerk.glob("*.md")))
agenten = vault / AGENTEN
if agenten.is_dir():
treffer.extend(sorted(agenten.glob("*.md")))
skills = vault / SKILLS
if skills.is_dir():
treffer.extend(sorted(skills.glob("*/SKILL.md")))
return treffer
def rollentabelle(text: str) -> dict[str, str]:
"""Liest die Zuordnung Agent -> Modellalias aus der Tabelle im Stamm.
Eine Zeile sieht so aus:
| Planen, Spezifizieren | `planer` | Opus 5 (`opus`) |
Genommen wird der erste Backtick-Ausdruck der Agenten- und der
Modellspalte. Zeilen ohne Backticks in der Agentenspalte (etwa
'Hauptsitzung') werden übersprungen.
"""
zuordnung: dict[str, str] = {}
for zeile in text.split("\n"):
if not zeile.startswith("|"):
continue
spalten = [s.strip() for s in zeile.strip("|").split("|")]
if len(spalten) < 3:
continue
agenten = BACKTICK.findall(spalten[1])
modelle = BACKTICK.findall(spalten[2])
if not agenten or not modelle:
continue
for name in agenten:
zuordnung[name.strip()] = modelle[0].strip()
return zuordnung
def stamm_regelwerkverweise(text: str) -> set[str]:
"""Alle im Stamm verlinkten Regelwerk-Dateien, als Dateiname ohne Endung."""
namen: set[str] = set()
for roh in wikilinks(text):
ziel = linkziel(roh)
if ziel.startswith(REGELWERK + "/"):
namen.add(ziel[len(REGELWERK) + 1 :])
return namen
# ---------------------------------------------------------------- Prüfungen
def pruefe_wikilinks(vault: Path, dateien: list[Path], befunde: list[Befund]) -> None:
for datei in dateien:
rel = datei.relative_to(vault).as_posix()
for roh in wikilinks(lies(datei)):
ziel = linkziel(roh)
if not ziel:
befunde.append(Befund("wichtig", rel, f"leerer Wikilink: [[{roh}]]"))
continue
if (vault / f"{ziel}.md").is_file() or (vault / ziel).exists():
continue
befunde.append(Befund("wichtig", rel, f"toter Wikilink: [[{ziel}]]"))
def pruefe_frontmatter(vault: Path, befunde: list[Befund]) -> None:
for datei in sorted((vault / REGELWERK).glob("*.md")):
rel = datei.relative_to(vault).as_posix()
text = lies(datei)
if not text.strip():
befunde.append(Befund("blockierend", rel, "Datei ist leer"))
continue
fm = frontmatter(text)
if fm is None:
befunde.append(Befund("blockierend", rel, "kein YAML-Frontmatter"))
continue
for feld in PFLICHTFELDER:
if feld not in fm:
befunde.append(Befund("wichtig", rel, f"Pflichtfeld fehlt: {feld}"))
elif not fm[feld]:
befunde.append(Befund("wichtig", rel, f"Pflichtfeld ohne Wert: {feld}"))
def pruefe_erreichbarkeit(vault: Path, verlinkt: set[str], befunde: list[Befund]) -> None:
for datei in sorted((vault / REGELWERK).glob("*.md")):
if datei.stem not in verlinkt:
befunde.append(
Befund(
"wichtig",
datei.relative_to(vault).as_posix(),
"im Stamm nicht verlinkt — wird nie geladen",
)
)
def pruefe_freigabe(vault: Path, befunde: list[Befund]) -> None:
for datei in sorted((vault / REGELWERK).glob("*.md")):
fm = frontmatter(lies(datei)) or {}
if fm.get("status") == "pending-review":
befunde.append(
Befund(
"klein",
datei.relative_to(vault).as_posix(),
"status: pending-review — Freigabe steht aus",
)
)
def pruefe_modelle(vault: Path, tabelle: dict[str, str], befunde: list[Befund]) -> None:
for datei in sorted((vault / AGENTEN).glob("*.md")):
rel = datei.relative_to(vault).as_posix()
fm = frontmatter(lies(datei))
if fm is None:
befunde.append(Befund("blockierend", rel, "kein YAML-Frontmatter"))
continue
name = fm.get("name") or datei.stem
gesetzt = fm.get("model")
if not gesetzt:
befunde.append(Befund("wichtig", rel, "kein model: im Frontmatter"))
continue
if name not in tabelle:
befunde.append(
Befund("wichtig", rel, f"Agent '{name}' steht nicht in der Rollentabelle des Stamms")
)
continue
if gesetzt != tabelle[name]:
befunde.append(
Befund(
"blockierend",
rel,
f"model: {gesetzt} — Rollentabelle im Stamm sagt {tabelle[name]}",
)
)
def pruefe_planablage(vault: Path, dateien: list[Path], befunde: list[Befund]) -> None:
for datei in dateien:
formen = set()
for treffer in PLANPFAD.findall(lies(datei)):
rest = treffer[len("docs/plans/JJJJ-MM-TT") :]
if rest.startswith("-"):
rest = rest.lstrip("-")
# <thema>/ oder <thema>.md — das Zeichen nach dem Platzhalter zählt
if rest.endswith("/") or "/" in rest:
formen.add("ordner")
elif rest.endswith(".md") or rest.endswith(".md`"):
formen.add("datei")
elif rest.startswith("<thema>/"):
formen.add("ordner")
if "datei" in formen and "ordner" not in formen:
befunde.append(
Befund(
"blockierend",
datei.relative_to(vault).as_posix(),
"nennt die Datei-Form der Planablage ohne die Ordner-Form "
"(ab drei Tasks gilt der Ordner)",
)
)
# --------------------------------------------------------------------- Lauf
def main() -> int:
p = argparse.ArgumentParser(description="Konsistenzprüfung der Anweisungsschicht")
p.add_argument("--vault", default=".", help="Vault-Wurzel (Vorgabe: .)")
args = p.parse_args()
vault = Path(args.vault).resolve()
befunde: list[Befund] = []
gelaufen = 0
abbruch: str | None = None
try:
# Voraussetzungen. Eine fehlende Voraussetzung ist ein BEFUND,
# kein Grund zu schweigen.
stamm = vault / "CLAUDE.md"
if not stamm.is_file():
befunde.append(Befund("blockierend", "CLAUDE.md", "fehlt — nichts prüfbar"))
raise SystemExit
if not (vault / REGELWERK).is_dir():
befunde.append(Befund("blockierend", REGELWERK, "Verzeichnis fehlt"))
raise SystemExit
stammtext = lies(stamm)
tabelle = rollentabelle(stammtext)
verlinkt = stamm_regelwerkverweise(stammtext)
if not tabelle:
befunde.append(
Befund("blockierend", "CLAUDE.md", "Rollentabelle nicht auffindbar — Prüfung 5 kann nicht laufen")
)
if not verlinkt:
befunde.append(
Befund("blockierend", "CLAUDE.md", "keine Regelwerk-Verweise gefunden — Prüfung 3 kann nicht laufen")
)
dateien = dateien_im_geltungsbereich(vault)
pruefe_wikilinks(vault, dateien, befunde); gelaufen += 1
pruefe_frontmatter(vault, befunde); gelaufen += 1
if verlinkt:
pruefe_erreichbarkeit(vault, verlinkt, befunde); gelaufen += 1
pruefe_freigabe(vault, befunde); gelaufen += 1
if tabelle and (vault / AGENTEN).is_dir():
pruefe_modelle(vault, tabelle, befunde); gelaufen += 1
pruefe_planablage(vault, dateien, befunde); gelaufen += 1
except SystemExit:
pass
except Exception as fehler: # ein Traceback wäre ein Befund ohne Meldung
abbruch = f"{type(fehler).__name__}: {fehler}"
befunde.append(Befund("blockierend", "regelwerk_check.py", f"Abbruch: {abbruch}"))
# Ausgabe — auch bei Abbruch, damit die Ursache nicht hinter dem Absturz
# verschwindet.
rang = {"blockierend": 0, "wichtig": 1, "klein": 2}
for b in sorted(befunde, key=lambda b: (rang.get(b.schwere, 9), b.ort)):
print(b)
print()
if befunde:
# Die Zahlen werden gerechnet, nicht geschrieben.
print(f"{len(befunde)} Befund(e) aus {gelaufen} von 6 Prüfungen.")
else:
print(f"Keine der {gelaufen} gelaufenen Prüfungen hat angeschlagen.")
if gelaufen < 6 and not abbruch:
print(f"ACHTUNG: nur {gelaufen} von 6 Prüfungen sind gelaufen — der Rest hatte keine Voraussetzung.")
if abbruch:
return 2
return 1 if befunde else 0
if __name__ == "__main__":
# Ohne das bricht die Ausgabe auf einer cp1252-Konsole ab — genau der
# Fehler, an dem vault_doctor.py auf diesem Rechner scheitert.
try:
sys.stdout.reconfigure(encoding="utf-8")
except AttributeError:
pass
raise SystemExit(main())
@@ -0,0 +1,276 @@
"""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.
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")
# 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-<thema>.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 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")
# 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),
]
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"),
]
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())