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,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())
|
||||
Reference in new issue
Block a user