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
c064c729ed
82 files changed
+13428
No files matched your search
@@ -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