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:
commit
2c9a4475fc
97 files changed
+14627
No files matched your search
@@ -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())
|
||||
Reference in new issue
Block a user