.vault-tools (ohne trading-plattform-pruefung und watermarks-remover), .claude/agents, .claude/skills, .claude/workflows, .claude/settings.json. Co-Authored-By: Claude Opus 5.5 <[email protected]>
582 lines
23 KiB
Python
582 lines
23 KiB
Python
"""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 fünf Orte:
|
|
CLAUDE.md der Stamm
|
|
AI/Regelwerk/*.md die ausgelagerten Regeln
|
|
.claude/skills/*/SKILL.md die Skills
|
|
.claude/agents/*.md die Agentendefinitionen
|
|
.claude/workflows/**/*.js die gespeicherten Workflows, nur für Prüfung 7
|
|
|
|
Sieben 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/Effort Weichen model: oder effort: einer Agentendatei von der Rollentabelle ab?
|
|
6 Planablage Nennt eine Datei die Dateiform ohne die Ordnerform?
|
|
7 Steuerzeichen Trägt ein gespeicherter Workflow ein unsichtbares Zeichen?
|
|
|
|
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 werden ausschließlich die Felder model: und effort: im
|
|
Frontmatter.
|
|
|
|
* Ein YAML-Kommentar hinter dem Wert (effort: high # …) wird nicht
|
|
abgeschnitten und ergibt einen Befund; für model: galt das schon vorher.
|
|
|
|
* 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.
|
|
|
|
* effort: bei model: haiku. Haiku 4.5 kennt keinen Effort; Claude Code
|
|
sendet ihn dann nicht (Claude Code 2.1.280, Programmdatei, nachgelesen
|
|
2026-09-23). Die Rollentabelle führt keinen Agenten auf Haiku; die
|
|
Abschrift-Stufe steht im Task-Frontmatter, nicht in einer Agentendatei.
|
|
|
|
* Doppelter Schlüssel effort:. Der flache Frontmatter-Leser nimmt den
|
|
letzten Wert, wie bei model:.
|
|
|
|
* Ob Claude Code den Wert anwendet. Das zeigt nur das Feld effort im
|
|
Transkript eines echten Laufs, und eine geänderte Agentendatei wirkt
|
|
in der laufenden Sitzung nicht sofort. Belegt am 2026-09-23 in frischen
|
|
Sitzungen über das Agent-Werkzeug und im Workflow.
|
|
|
|
* effort in gespeicherten Workflows unter .claude/workflows/. Sie setzen
|
|
den Effort mit agentType absichtlich nicht; eine Abweichung dort sieht
|
|
diese Prüfung nicht.
|
|
|
|
* Prüfung 7 meldet eine OBERMENGE dessen, was das Workflow-Werkzeug
|
|
abweist. Belegt ist nur CR: plankritik.js mit CRLF scheiterte am
|
|
2026-09-25 und 26 mit "script contains control characters that would be
|
|
hidden in the approval dialog", mit LF lief sie. Die genaue Zeichenmenge
|
|
des Werkzeugs steht im Programmabbild nicht eindeutig neben der Meldung
|
|
(Claude Code 2.1.281, am Abbild gelesen 2026-09-26). Projektvorgabe ist
|
|
deshalb: jedes Zeichen der Unicode-Kategorie Cc außer LF und jedes der
|
|
Kategorie Cf. Darunter fallen CR, Tab, NUL, die Bidi-Steuerzeichen und
|
|
die BOM. Ob das Werkzeug einen Tab abweist, ist unbelegt; gemeldet wird
|
|
er trotzdem, eingerückt wird mit Leerzeichen.
|
|
|
|
* Prüfung 7 liest nur .claude/workflows/. Die Kopie, die das Werkzeug je
|
|
Lauf im Sitzungsordner ablegt, und ein Skript, das per scriptPath von
|
|
anderswo kommt, sieht sie nicht.
|
|
|
|
===============================================================================
|
|
DIESES SKRIPT IST EIN FANGNETZ, KEIN NACHWEIS.
|
|
===============================================================================
|
|
|
|
Ein grüner Lauf belegt, dass keine der sieben 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
|
|
import unicodedata
|
|
from pathlib import Path
|
|
|
|
PFLICHTFELDER = ("type", "status", "title", "summary", "tags", "created", "updated")
|
|
REGELWERK = "AI/Regelwerk"
|
|
AGENTEN = ".claude/agents"
|
|
SKILLS = ".claude/skills"
|
|
WORKFLOWS = ".claude/workflows"
|
|
# Die Zahl der Prüfungen steht an dieser einen Stelle; main() liest sie hier.
|
|
PRUEFUNGEN = 7
|
|
|
|
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"`([^`]+)`")
|
|
|
|
# Die Stufen stammen aus Claude Code 2.1.280 (Programmdatei, nachgelesen
|
|
# 2026-09-23). Claude Code selbst nimmt zusätzlich Großschreibung, "med"
|
|
# und ganze Zahlen an; die Vorgabe hier ist bewusst enger.
|
|
EFFORT_WERTE = ("low", "medium", "high", "xhigh", "max")
|
|
|
|
|
|
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.5 (`opus`) | `high` (Frontmatter) |
|
|
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 entquotet(wert: str) -> str:
|
|
"""Nimmt passende einfache oder doppelte Anführungszeichen ab: 'high' -> high."""
|
|
if len(wert) >= 2 and wert[0] == wert[-1] and wert[0] in ("'", '"'):
|
|
return wert[1:-1]
|
|
return wert
|
|
|
|
|
|
def effort_spalte(text: str) -> int | None:
|
|
"""Findet den Index der Effort-Spalte in der Rollentabelle im Stamm.
|
|
|
|
Gesucht wird die Tabellenzeile, deren Zellen (getrimmt) sowohl 'Agent'
|
|
als auch 'Effort' enthalten — das ist die Kopfzeile. Der Index der Zelle
|
|
'Effort' ist die Effort-Spalte.
|
|
"""
|
|
for zeile in text.split("\n"):
|
|
if not zeile.startswith("|"):
|
|
continue
|
|
spalten = [s.strip() for s in zeile.strip("|").split("|")]
|
|
if "Agent" in spalten and "Effort" in spalten:
|
|
return spalten.index("Effort")
|
|
return None
|
|
|
|
|
|
def rollentabelle_effort(text: str, spalte: int) -> dict[str, str]:
|
|
"""Liest den Effort je Agent aus der Rollentabelle im Stamm.
|
|
|
|
Genommen wird der erste Backtick-Ausdruck der Effort-Spalte (`spalte`).
|
|
Fehlt ein Backtick-Ausdruck, ist der Wert für diesen Agenten leer.
|
|
"""
|
|
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) <= spalte:
|
|
continue
|
|
agenten = BACKTICK.findall(spalten[1])
|
|
if not agenten:
|
|
continue
|
|
effort_treffer = BACKTICK.findall(spalten[spalte])
|
|
wert = effort_treffer[0].strip() if effort_treffer else ""
|
|
for name in agenten:
|
|
zuordnung[name.strip()] = wert
|
|
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],
|
|
effort_index: int | None,
|
|
effort_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]}",
|
|
)
|
|
)
|
|
|
|
# Effort: nur prüfbar, wenn die Effort-Spalte der Rollentabelle
|
|
# gefunden wurde (siehe main() für den Befund am Stamm, falls nicht).
|
|
if effort_index is None:
|
|
continue
|
|
soll = effort_tabelle.get(name, "")
|
|
if not soll:
|
|
befunde.append(
|
|
Befund("wichtig", "CLAUDE.md", f"Rollentabelle nennt für '{name}' keinen Effort")
|
|
)
|
|
effort_roh = fm.get("effort")
|
|
if not effort_roh:
|
|
befunde.append(
|
|
Befund(
|
|
"wichtig",
|
|
rel,
|
|
"kein effort: im Frontmatter — der Agent erbt den Effort der Hauptsitzung",
|
|
)
|
|
)
|
|
continue
|
|
effort_wert = entquotet(effort_roh)
|
|
if not effort_wert:
|
|
befunde.append(
|
|
Befund(
|
|
"wichtig",
|
|
rel,
|
|
"kein effort: im Frontmatter — der Agent erbt den Effort der Hauptsitzung",
|
|
)
|
|
)
|
|
elif effort_wert not in EFFORT_WERTE:
|
|
befunde.append(
|
|
Befund(
|
|
"blockierend",
|
|
rel,
|
|
f"effort: {effort_wert} ist nicht in der Vorgabe (erlaubt: low, medium, high, xhigh, max)",
|
|
)
|
|
)
|
|
elif soll and effort_wert != soll:
|
|
befunde.append(
|
|
Befund("blockierend", rel, f"effort: {effort_wert} — Rollentabelle im Stamm sagt {soll}")
|
|
)
|
|
|
|
|
|
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)",
|
|
)
|
|
)
|
|
|
|
|
|
def pruefe_workflow_zeichen(vault: Path, befunde: list[Befund]) -> None:
|
|
"""Prüfung 7: unsichtbare Zeichen in den gespeicherten Workflows.
|
|
|
|
Gemeldet wird je Datei einmal, mit Anzahl und erster Fundstelle, jedes Zeichen der
|
|
Unicode-Kategorie Cc außer LF und jedes der Kategorie Cf. Gelesen wird als Bytes:
|
|
Path.read_text übersetzt CRLF still in LF und wäre genau für den Fall blind, der am
|
|
2026-09-25 plankritik.js lahmgelegt hat. Fehlt der Ordner, gibt es nichts zu prüfen,
|
|
und Schweigen ist richtig.
|
|
"""
|
|
ordner = vault / WORKFLOWS
|
|
if not ordner.is_dir():
|
|
return
|
|
for datei in sorted(ordner.rglob("*.js")):
|
|
if not datei.is_file():
|
|
continue
|
|
rel = datei.relative_to(vault).as_posix()
|
|
try:
|
|
text = datei.read_bytes().decode("utf-8")
|
|
except UnicodeDecodeError as fehler:
|
|
befunde.append(Befund("blockierend", rel, f"nicht als UTF-8 lesbar (ab Byte {fehler.start})"))
|
|
continue
|
|
funde = [
|
|
stelle
|
|
for stelle, zeichen in enumerate(text)
|
|
if (zeichen != "\n" and unicodedata.category(zeichen) == "Cc")
|
|
or unicodedata.category(zeichen) == "Cf"
|
|
]
|
|
if not funde:
|
|
continue
|
|
erstes = funde[0]
|
|
zeile = text.count("\n", 0, erstes) + 1
|
|
spalte = erstes - (text.rfind("\n", 0, erstes) + 1) + 1
|
|
befunde.append(
|
|
Befund(
|
|
"blockierend",
|
|
rel,
|
|
f"{len(funde)} unsichtbare(s) Zeichen, das erste in Zeile {zeile}, Spalte {spalte}: "
|
|
f"U+{ord(text[erstes]):04X} (Kategorie {unicodedata.category(text[erstes])}); "
|
|
"das Workflow-Werkzeug weist so eine Datei ab, also auf LF stellen und das Zeichen entfernen",
|
|
)
|
|
)
|
|
|
|
|
|
# --------------------------------------------------------------------- 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")
|
|
)
|
|
|
|
effort_index = effort_spalte(stammtext) if tabelle else None
|
|
if tabelle and effort_index is None:
|
|
befunde.append(
|
|
Befund(
|
|
"blockierend",
|
|
"CLAUDE.md",
|
|
"Effort-Spalte der Rollentabelle nicht auffindbar — Prüfung 5 kann den Effort nicht prüfen",
|
|
)
|
|
)
|
|
effort_tabelle = rollentabelle_effort(stammtext, effort_index) if effort_index is not None else {}
|
|
|
|
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, effort_index, effort_tabelle, befunde); gelaufen += 1
|
|
pruefe_planablage(vault, dateien, befunde); gelaufen += 1
|
|
pruefe_workflow_zeichen(vault, 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 {PRUEFUNGEN} Prüfungen.")
|
|
else:
|
|
print(f"Keine der {gelaufen} gelaufenen Prüfungen hat angeschlagen.")
|
|
if gelaufen < PRUEFUNGEN and not abbruch:
|
|
print(f"ACHTUNG: nur {gelaufen} von {PRUEFUNGEN} 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())
|