Erste Fassung: Claude-Werkzeuge aus dem Vault
.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]>
This commit is contained in:
commit
fa1c26b407
137 files changed
+43926
No files matched your search
@@ -0,0 +1,115 @@
|
||||
# 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.
|
||||
|
||||
## Nachtrag 2026-09-23: Prüfung 5 prüft auch `effort:`
|
||||
|
||||
Geschrieben vor der Änderung am Skript. Anlass: Keine Agentendatei trug `effort:`, jeder Subagent erbte deshalb den Effort der Hauptsitzung, oft `xhigh` oder `max`. Seitdem steht der Effort je Rolle im Stamm und im Frontmatter (Messung und Begründung: [[AI/Regelwerk/codeworkflow|codeworkflow.md]], „Effort je Rolle"). Die Prüfung sorgt dafür, dass beide gleich bleiben.
|
||||
|
||||
| # | Frage | Herkunft | Auflösung |
|
||||
|---|---|---|---|
|
||||
| 5a | Welche Effort-Werte gibt es? | **Quelle** für die Stufen — Claude Code 2.1.280, Enum `["low","medium","high","xhigh","max"]` in der Programmdatei (nachgelesen 2026-09-23). Der Leser für Agentendateien nimmt darüber hinaus Großschreibung, `med` und ganze Zahlen an. **Projektvorgabe** ist die engere Form. | Erlaubt sind nur die fünf Wörter, klein geschrieben: eine Schreibweise, die jeder ohne Programmkenntnis liest. Die Meldung sagt deshalb „nicht in der Vorgabe", nicht „ungültig". Anführungszeichen um den Wert sind YAML und zählen nicht mit. |
|
||||
| 5b | Welcher Effort gilt je Agent? | **Projektvorgabe** — Rollentabelle im Stamm, Spalte mit der Überschrift „Effort", erster Backtick-Ausdruck der Zelle | Das `effort:` im Frontmatter muss ihm gleichen. Die Spalte wird über ihre Überschrift gefunden, nicht über die Position. |
|
||||
| 5c | Agentendatei ohne `effort:` oder mit leerem Wert | **Projektvorgabe** — Stamm: „Deshalb trägt jede Agentendatei `effort:`" | Befund: Der Agent erbt den Effort der Hauptsitzung. |
|
||||
| 5d | Rollentabelle ohne Effort-Spalte | **Projektvorgabe** | Befund am Stamm. Die Effort-Hälfte von Prüfung 5 schweigt dann nicht, sie meldet, dass sie nicht laufen kann. |
|
||||
| 5e | Tabellenzeile eines Agenten ohne Backtick-Wert in der Effort-Spalte | **Projektvorgabe** | Befund am Stamm, mit dem Namen des Agenten. |
|
||||
|
||||
Benannte Lücken, bewusst nicht geprüft:
|
||||
|
||||
- **`effort:` bei `model: haiku`.** Haiku 4.5 kennt keinen Effort; Claude Code sendet ihn dann nicht (Programmdatei). 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.
|
||||
|
||||
Dazu zwei Korrekturen am Regressionslauf, die mit diesem Nachtrag gekommen sind: Die Grundlinie setzt `status: pending-review` in ihrer Kopie auf `reference`. Vorher brach der Lauf nach jeder Selbstpflege an der Grundlinie ab, weil Prüfung 4 dann anschlägt — gemessen am 2026-09-23. Und Prüfung 4 bekommt dafür erstmals einen eigenen Angriffsfall.
|
||||
|
||||
## Nachtrag 2026-09-26: Prüfung 7, unsichtbare Zeichen in gespeicherten Workflows
|
||||
|
||||
Geschrieben vor der Änderung am Skript. Anlass: Am 2026-09-25 und 26 lehnte das Workflow-Werkzeug `plankritik.js` ab („script contains control characters that would be hidden in the approval dialog“). Die Datei trug CRLF im Arbeitsbaum. Pascal hat die Prüfung am 2026-09-26 als Punkt 4 des Pakets „Arbeitsweise“ freigegeben. Plan: `docs/plans/2026-09-26-workflow-steuerzeichen/` in diesem Ordner.
|
||||
|
||||
| # | Frage | Herkunft | Auflösung |
|
||||
|---|---|---|---|
|
||||
| 7a | Welche Zeichen weist das Werkzeug ab? | **Quelle** nur für CR (der Vorfall). Die Zeichenmenge steht im Abbild von Claude Code 2.1.281 nicht eindeutig neben der Meldung (am Abbild gelesen 2026-09-26). | Der Rest ist unbelegt und wird nicht geraten. |
|
||||
| 7b | Welche Zeichen meldet die Prüfung? | **Projektvorgabe** | Jedes Zeichen der Unicode-Kategorie Cc außer LF und jedes der Kategorie Cf. Das ist eine geschlossene Menge und eine Obermenge dessen, was belegt ist. Tab wird gemeldet, obwohl unbelegt ist, ob das Werkzeug ihn abweist. |
|
||||
| 7c | Welche Dateien? | **Projektvorgabe** | Jede `*.js` unter `.claude/workflows/`, auch in Unterordnern. |
|
||||
| 7d | Wie gelesen? | **Quelle** — Erinnerung `gespeicherter-workflow-per-name-scheitert` | Als Bytes. `read_text` übersetzt CRLF still und war beim Vorfall blind. |
|
||||
| 7e | Kein UTF-8, Ordner fehlt oder ist leer | **Projektvorgabe** — `pruefwerkzeuge.md`, Abschnitte 4 und 6 | Kein UTF-8 ist ein Befund. Fehlt der Ordner oder ist er leer, gibt es nichts zu prüfen, und Schweigen ist richtig. |
|
||||
|
||||
Der Hook `session-hooks/regelwerk_nach_edit.py` löst seitdem auch bei Änderungen unter `.claude/workflows/` aus.
|
||||
|
||||
## Positivliste, nicht Negativliste
|
||||
|
||||
Alle sieben 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, die Menge der Zeichen zweier Unicode-Kategorien. 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 sieben 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`) |
|
||||
| Wertebereich | `effort: xhigh` in `pruefer.md` (Tabelle sagt `high`) |
|
||||
| Wertebereich | `effort: hoch` in `planer.md` — kein gültiger Wert |
|
||||
| fehlt, **Reihenfolge** | `effort:` fehlt in der ersten Agentendatei (`executer.md`) und, als eigener Fall, in der letzten (`recherche.md`) |
|
||||
| null | `effort:` ohne Wert in `planer.md` |
|
||||
| Umgebung | Effort-Spalte der Rollentabelle entfernt |
|
||||
| Verweis ins Leere | Effort-Zelle eines Agenten ohne Backtick-Wert |
|
||||
| Freigabe | eine Regelwerk-Datei auf `status: pending-review` |
|
||||
| Gegenfall (muss stumm bleiben) | `effort: "high"` in Anführungszeichen |
|
||||
| Gegenfall (muss stumm bleiben) | Zusatzspalte mit Backtick-Wert hinter der Effort-Spalte — fängt einen Rückfall auf die Spaltenposition |
|
||||
| Gegenfall (muss stumm bleiben) | Effort-Zelle mit zweitem Backtick-Wert — fängt „letzter statt erster Backtick-Ausdruck" |
|
||||
| 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 |
|
||||
| Steuerzeichen, **Reihenfolge** | CRLF in der ersten Workflowdatei und, als eigener Fall, ein CR in der letzten Zeile der letzten |
|
||||
| Steuerzeichen | NUL, Bidi-Umkehr U+202E, BOM am Dateianfang, Tab, je ein Fall |
|
||||
| Kodierung | Workflowdatei ist kein UTF-8 |
|
||||
| zusätzlich | Workflowdatei mit CR in einem Unterordner |
|
||||
| Gegenfall (muss stumm bleiben) | typografische Anführungszeichen, Striche, Auslassungspunkte, Umlaut und Euro im Workflow |
|
||||
| Gegenfall (muss stumm bleiben) | Workflow-Ordner fehlt |
|
||||
|
||||
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 Fälle der Angriffsliste anschlagen, alle Gegenfälle stumm bleiben und die Grundlinie grün ist — nicht, wenn sie „durchläuft". Die Zahl der Fälle zählt der Regressionslauf selbst; hier steht keine, weil sie bei jeder Erweiterung von Hand nachgezogen und dabei falsch würde.
|
||||
|
||||
## 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.
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
---
|
||||
type: plan
|
||||
title: "Prüfung 7: unsichtbare Zeichen in gespeicherten Workflows"
|
||||
status: active
|
||||
summary: "regelwerk_check.py bekommt Prüfung 7, die in den gespeicherten Workflows jedes Zeichen der Kategorien Cc (außer LF) und Cf meldet; der Regressionslauf bekommt Angriffs- und Gegenfälle, der Hook nach dem Bearbeiten löst auch bei Workflows aus."
|
||||
tags: [plan, vault-tools]
|
||||
created: 2026-09-26
|
||||
updated: 2026-09-26
|
||||
---
|
||||
|
||||
# Prüfung 7: unsichtbare Zeichen in gespeicherten Workflows
|
||||
|
||||
## Ziel
|
||||
|
||||
Am 2026-09-25 und 26 lehnte das Workflow-Werkzeug `plankritik.js` ab („script contains control characters that would be hidden in the approval dialog“). Die Ursache war CRLF im Arbeitsbaum. Nach diesem Plan findet `regelwerk_check.py` so eine Datei, bevor jemand den Workflow startet. Der PostToolUse-Hook fährt die Prüfung auch nach einer Änderung unter `.claude/workflows/`. Freigegeben hat Pascal das am 2026-09-26 als Punkt 4 des Pakets „Arbeitsweise“.
|
||||
|
||||
## Entscheidungsliste
|
||||
|
||||
| Frage | Antwort | Herkunft |
|
||||
|---|---|---|
|
||||
| Welche Zeichen weist das Werkzeug ab? | Belegt ist nur CR. Die Zeichenmenge steht im Abbild von Claude Code 2.1.281 nicht eindeutig neben der Meldung. | Quelle (Vorfall), Rest unbelegt |
|
||||
| Welche Zeichen meldet Prüfung 7? | Jedes Zeichen der Unicode-Kategorie Cc außer LF, und jedes der Kategorie Cf. Das ist eine Obermenge: CR, Tab, NUL, Bidi-Steuerzeichen und BOM fallen darunter. | Projektvorgabe (Koordinator) |
|
||||
| Welche Dateien? | Jede `*.js` unter `.claude/workflows/`, auch in Unterordnern. | Projektvorgabe |
|
||||
| Wie gelesen? | Als Bytes, dann UTF-8 streng. `read_text` übersetzt CRLF still und wäre blind. | Quelle (Erinnerung `gespeicherter-workflow-per-name-scheitert`) |
|
||||
| Kein UTF-8? | Befund, kein Absturz. | Projektvorgabe (`pruefwerkzeuge.md`, Abschnitt 4) |
|
||||
| Ordner fehlt oder ist leer? | Kein Befund: Es gibt nichts zu prüfen. | Projektvorgabe (`pruefwerkzeuge.md`, Abschnitt 6) |
|
||||
| Schwere? | blockierend, denn der Workflow startet nicht. | Projektvorgabe |
|
||||
|
||||
## Globale Vorgaben
|
||||
|
||||
- Nur die Zieldateien der Taskdatei. `CLAUDE.md`, beide `ENTWURF.md` und alles unter `AI/` ändert der Koordinator selbst.
|
||||
- Codeblöcke wörtlich mit `python D:/Claude/.vault-tools/plan-kopie/plan_materialisiere.py <planordner> D:/Claude <taskdatei>` anwenden. Nichts von Hand nachschreiben.
|
||||
- Ein Widerspruch im Plan wird gemeldet, nicht still aufgelöst.
|
||||
- Kein Commit, kein `git add`.
|
||||
|
||||
## Tasks
|
||||
|
||||
| Task | Inhalt | Modell |
|
||||
|---|---|---|
|
||||
| `task-01-pruefung-7.md` | `regelwerk_check.py` und `regressionslauf.py` | haiku |
|
||||
| `task-02-hook.md` | `regelwerk_nach_edit.py` und `faelle_nach_edit.py` | haiku |
|
||||
|
||||
Die beiden Tasks haben disjunkte Dateien und laufen in einer Welle.
|
||||
|
||||
## Fertig heißt
|
||||
|
||||
1. `python .vault-tools/regelwerk-check/regressionslauf.py` endet mit Exit 0, alle Angriffsfälle angeschlagen, alle Gegenfälle stumm. Die neuen Fälle stehen einzeln in der Ausgabe.
|
||||
2. `python .vault-tools/regelwerk-check/regelwerk_check.py --vault .` auf dem echten Vault: Exit 0 und die Schlusszeile nennt 7 gelaufene Prüfungen.
|
||||
3. Eine Kopie von `plankritik.js` mit CRLF in einem Wegwerf-Vault ergibt einen Befund mit `U+000D`. Das ist der Vorfall in seiner exakten Form.
|
||||
4. `python .vault-tools/session-hooks/faelle_nach_edit.py` endet mit Exit 0, und der Fall „Workflowdatei“ steht als angeschlagen in der Ausgabe.
|
||||
5. Die geänderten Dateien sind byte-gleich mit der Kopie, auf die der Koordinator den Plan vorher angewendet hat.
|
||||
+411
@@ -0,0 +1,411 @@
|
||||
---
|
||||
type: plan-task
|
||||
title: "Task 01 — Prüfung 7 in regelwerk_check.py und ihre Fälle im Regressionslauf"
|
||||
status: draft
|
||||
summary: "Neue Prüfung auf unsichtbare Zeichen in gespeicherten Workflows, dazu acht Angriffs- und zwei Gegenfälle."
|
||||
tags: [plan-task, vault-tools]
|
||||
created: 2026-09-26
|
||||
updated: 2026-09-26
|
||||
modell: haiku
|
||||
modell_begruendung: "Alle Blöcke stehen wörtlich im Plan und werden per Skript angewendet; Steuerzeichen entstehen nur über Codepunkte, keine Escape-Folge muss von Hand geschrieben werden."
|
||||
---
|
||||
|
||||
# Task 01 — Prüfung 7 in `regelwerk_check.py` und ihre Fälle
|
||||
|
||||
Globale Vorgaben: `00-uebersicht.md` im selben Ordner, Abschnitt „Globale Vorgaben“.
|
||||
|
||||
## Zieldateien
|
||||
|
||||
- `D:\Claude\.vault-tools\regelwerk-check\regelwerk_check.py` (geändert)
|
||||
- `D:\Claude\.vault-tools\regelwerk-check\regressionslauf.py` (geändert)
|
||||
|
||||
## Schritt 1 — Blöcke anwenden
|
||||
|
||||
```
|
||||
python D:/Claude/.vault-tools/plan-kopie/plan_materialisiere.py D:/Claude/.vault-tools/regelwerk-check/docs/plans/2026-09-26-workflow-steuerzeichen D:/Claude task-01-pruefung-7.md
|
||||
```
|
||||
|
||||
Erwartet: `0 Befund(e).` und `'ersetzt': 13`.
|
||||
|
||||
### Geltungsbereich im Kopf von `.vault-tools/regelwerk-check/regelwerk_check.py`
|
||||
|
||||
Aus:
|
||||
|
||||
```
|
||||
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:
|
||||
```
|
||||
|
||||
wird:
|
||||
|
||||
```
|
||||
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:
|
||||
```
|
||||
|
||||
### Prüfliste im Kopf von `.vault-tools/regelwerk-check/regelwerk_check.py`
|
||||
|
||||
Aus:
|
||||
|
||||
```
|
||||
6 Planablage Nennt eine Datei die Dateiform ohne die Ordnerform?
|
||||
```
|
||||
|
||||
wird:
|
||||
|
||||
```
|
||||
6 Planablage Nennt eine Datei die Dateiform ohne die Ordnerform?
|
||||
7 Steuerzeichen Trägt ein gespeicherter Workflow ein unsichtbares Zeichen?
|
||||
```
|
||||
|
||||
### Benannte Lücken in `.vault-tools/regelwerk-check/regelwerk_check.py`
|
||||
|
||||
Aus:
|
||||
|
||||
```
|
||||
* effort in gespeicherten Workflows unter .claude/workflows/. Sie setzen
|
||||
den Effort mit agentType absichtlich nicht; eine Abweichung dort sieht
|
||||
diese Prüfung nicht.
|
||||
```
|
||||
|
||||
wird:
|
||||
|
||||
```
|
||||
* 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.
|
||||
```
|
||||
|
||||
### Achsenzahl in `.vault-tools/regelwerk-check/regelwerk_check.py`
|
||||
|
||||
Aus:
|
||||
|
||||
```
|
||||
Ein grüner Lauf belegt, dass keine der sechs formalen Achsen verletzt ist.
|
||||
```
|
||||
|
||||
wird:
|
||||
|
||||
```
|
||||
Ein grüner Lauf belegt, dass keine der sieben formalen Achsen verletzt ist.
|
||||
```
|
||||
|
||||
### Importe in `.vault-tools/regelwerk-check/regelwerk_check.py`
|
||||
|
||||
Aus:
|
||||
|
||||
```
|
||||
import argparse
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
```
|
||||
|
||||
wird:
|
||||
|
||||
```
|
||||
import argparse
|
||||
import re
|
||||
import sys
|
||||
import unicodedata
|
||||
from pathlib import Path
|
||||
```
|
||||
|
||||
### Konstanten in `.vault-tools/regelwerk-check/regelwerk_check.py`
|
||||
|
||||
Aus:
|
||||
|
||||
```
|
||||
SKILLS = ".claude/skills"
|
||||
```
|
||||
|
||||
wird:
|
||||
|
||||
```
|
||||
SKILLS = ".claude/skills"
|
||||
WORKFLOWS = ".claude/workflows"
|
||||
# Die Zahl der Prüfungen steht an dieser einen Stelle; main() liest sie hier.
|
||||
PRUEFUNGEN = 7
|
||||
```
|
||||
|
||||
### Neue Prüfung in `.vault-tools/regelwerk-check/regelwerk_check.py`
|
||||
|
||||
Aus:
|
||||
|
||||
```
|
||||
# --------------------------------------------------------------------- Lauf
|
||||
```
|
||||
|
||||
wird:
|
||||
|
||||
```
|
||||
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
|
||||
```
|
||||
|
||||
### Aufruf in `.vault-tools/regelwerk-check/regelwerk_check.py`
|
||||
|
||||
Aus:
|
||||
|
||||
```
|
||||
pruefe_planablage(vault, dateien, befunde); gelaufen += 1
|
||||
```
|
||||
|
||||
wird:
|
||||
|
||||
```
|
||||
pruefe_planablage(vault, dateien, befunde); gelaufen += 1
|
||||
pruefe_workflow_zeichen(vault, befunde); gelaufen += 1
|
||||
```
|
||||
|
||||
### Schlusszeilen in `.vault-tools/regelwerk-check/regelwerk_check.py`
|
||||
|
||||
Aus:
|
||||
|
||||
```
|
||||
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.")
|
||||
```
|
||||
|
||||
wird:
|
||||
|
||||
```
|
||||
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.")
|
||||
```
|
||||
|
||||
### Grundlinie in `.vault-tools/regelwerk-check/regressionslauf.py`
|
||||
|
||||
Aus:
|
||||
|
||||
```
|
||||
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")
|
||||
```
|
||||
|
||||
wird:
|
||||
|
||||
```
|
||||
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")
|
||||
# Prüfung 7 liest die gespeicherten Workflows. copy2 kopiert Bytes, die Grundlinie
|
||||
# trägt also dieselben Zeilenenden wie der Vault.
|
||||
(ziel / ".claude" / "workflows").mkdir(parents=True)
|
||||
for f in (VAULT / ".claude" / "workflows").glob("*.js"):
|
||||
shutil.copy2(f, ziel / ".claude" / "workflows" / f.name)
|
||||
```
|
||||
|
||||
### Beschädigungen für Prüfung 7 in `.vault-tools/regelwerk-check/regressionslauf.py`
|
||||
|
||||
Aus:
|
||||
|
||||
```
|
||||
# Gegenfaelle: duerfen NICHT anschlagen. Ohne sie waere die Codeblock-
|
||||
```
|
||||
|
||||
wird:
|
||||
|
||||
```
|
||||
# Steuerzeichen entstehen nur ueber Codepunkte, nie als Escape-Folge im Quelltext: Die
|
||||
# Schreibschicht hat solche Folgen schon still in echte Zeichen verwandelt
|
||||
# (codeworkflow.md, "Escape-Folgen und unsichtbare Zeichen").
|
||||
CR = bytes([13])
|
||||
LF = bytes([10])
|
||||
|
||||
|
||||
def _workflows(v: Path) -> list[Path]:
|
||||
dateien = sorted((v / ".claude" / "workflows").glob("*.js"))
|
||||
assert len(dateien) >= 2, "die Grundlinie braucht mindestens zwei gespeicherte Workflows"
|
||||
return dateien
|
||||
|
||||
|
||||
def _haenge_an(f: Path, zusatz: bytes) -> None:
|
||||
f.write_bytes(f.read_bytes() + zusatz)
|
||||
|
||||
|
||||
def s_workflow_crlf_erste_datei(v: Path) -> None:
|
||||
f = _workflows(v)[0]
|
||||
f.write_bytes(f.read_bytes().replace(LF, CR + LF))
|
||||
|
||||
|
||||
def s_workflow_cr_letzte_datei_letzte_zeile(v: Path) -> None:
|
||||
f = _workflows(v)[-1]
|
||||
f.write_bytes(f.read_bytes().rstrip(LF) + CR + LF)
|
||||
|
||||
|
||||
def s_workflow_nul(v: Path) -> None:
|
||||
_haenge_an(_workflows(v)[0], b"// " + bytes([0]) + LF)
|
||||
|
||||
|
||||
def s_workflow_bidi(v: Path) -> None:
|
||||
_haenge_an(_workflows(v)[0], ("// " + chr(0x202E) + "x").encode("utf-8") + LF)
|
||||
|
||||
|
||||
def s_workflow_bom(v: Path) -> None:
|
||||
f = _workflows(v)[-1]
|
||||
f.write_bytes(chr(0xFEFF).encode("utf-8") + f.read_bytes())
|
||||
|
||||
|
||||
def s_workflow_tab(v: Path) -> None:
|
||||
_haenge_an(_workflows(v)[0], b"//" + bytes([9]) + b"x" + LF)
|
||||
|
||||
|
||||
def s_workflow_kein_utf8(v: Path) -> None:
|
||||
_haenge_an(_workflows(v)[0], b"// " + bytes([0xFF]) + LF)
|
||||
|
||||
|
||||
def s_workflow_unterordner(v: Path) -> None:
|
||||
unter = v / ".claude" / "workflows" / "unter"
|
||||
unter.mkdir()
|
||||
(unter / "tief.js").write_bytes(b"a" + CR + LF)
|
||||
|
||||
|
||||
def g_workflow_typografie(v: Path) -> None:
|
||||
"""Kein Schaden: typografische Zeichen und Umlaute sind keine Steuerzeichen."""
|
||||
zeichen = "".join(chr(c) for c in (0x201E, 0x201C, 0x2013, 0x2014, 0x2026, 0xE4, 0x20AC))
|
||||
_haenge_an(_workflows(v)[0], ("// " + zeichen).encode("utf-8") + LF)
|
||||
|
||||
|
||||
def g_workflow_ordner_fehlt(v: Path) -> None:
|
||||
"""Kein Schaden: ohne Ordner gibt es nichts zu pruefen."""
|
||||
shutil.rmtree(v / ".claude" / "workflows")
|
||||
|
||||
|
||||
# Gegenfaelle: duerfen NICHT anschlagen. Ohne sie waere die Codeblock-
|
||||
```
|
||||
|
||||
### Gegenfälle in `.vault-tools/regelwerk-check/regressionslauf.py`
|
||||
|
||||
Aus:
|
||||
|
||||
```
|
||||
("zweiter-backtick | Effort-Zelle mit zweitem Backtick-Wert", g_zweiter_backtick),
|
||||
]
|
||||
```
|
||||
|
||||
wird:
|
||||
|
||||
```
|
||||
("zweiter-backtick | Effort-Zelle mit zweitem Backtick-Wert", g_zweiter_backtick),
|
||||
("typografie | Anführung, Striche, Umlaut im Workflow", g_workflow_typografie),
|
||||
("ordner-fehlt | kein Workflow-Ordner", g_workflow_ordner_fehlt),
|
||||
]
|
||||
```
|
||||
|
||||
### Angriffsfälle in `.vault-tools/regelwerk-check/regressionslauf.py`
|
||||
|
||||
Aus:
|
||||
|
||||
```
|
||||
("freigabe | status: pending-review", s_effort_freigabe_pending, "Freigabe steht aus"),
|
||||
]
|
||||
```
|
||||
|
||||
wird:
|
||||
|
||||
```
|
||||
("freigabe | status: pending-review", s_effort_freigabe_pending, "Freigabe steht aus"),
|
||||
("zeichen | CRLF in der ERSTEN Workflowdatei", s_workflow_crlf_erste_datei, "U+000D"),
|
||||
("zeichen | CR in der LETZTEN Zeile der LETZTEN Workflowdatei", s_workflow_cr_letzte_datei_letzte_zeile, "U+000D"),
|
||||
("zeichen | NUL", s_workflow_nul, "U+0000"),
|
||||
("zeichen | Bidi-Umkehr", s_workflow_bidi, "U+202E"),
|
||||
("zeichen | BOM am Dateianfang", s_workflow_bom, "Zeile 1, Spalte 1: U+FEFF"),
|
||||
("zeichen | Tab", s_workflow_tab, "U+0009"),
|
||||
("kodierung | Workflow ist kein UTF-8", s_workflow_kein_utf8, "nicht als UTF-8 lesbar"),
|
||||
("zusaetzlich| CR in einem Unterordner", s_workflow_unterordner, ".claude/workflows/unter/tief.js"),
|
||||
]
|
||||
```
|
||||
|
||||
## Schritt 2 — Verifikation
|
||||
|
||||
```
|
||||
python D:/Claude/.vault-tools/regelwerk-check/regressionslauf.py
|
||||
```
|
||||
|
||||
Erwartet: Exit 0, die Grundlinie grün, jede Zeile unter ANGRIFFSLISTE beginnt mit `ANGESCHLAGEN`, jede unter GEGENFÄLLE mit `STUMM`.
|
||||
|
||||
```
|
||||
python D:/Claude/.vault-tools/regelwerk-check/regelwerk_check.py --vault D:/Claude
|
||||
```
|
||||
|
||||
Erwartet: Exit 0 und `Keine der 7 gelaufenen Prüfungen hat angeschlagen.`
|
||||
|
||||
Melde beide Ausgaben vollständig. Läuft etwas anders: nichts anpassen, melden.
|
||||
+118
@@ -0,0 +1,118 @@
|
||||
---
|
||||
type: plan-task
|
||||
title: "Task 02 — Der Hook nach dem Bearbeiten löst auch bei Workflows aus"
|
||||
status: draft
|
||||
summary: "regelwerk_nach_edit.py nimmt den Workflow-Ordner in den Geltungsbereich; der Treiber bekommt einen Fall, der anschlägt, und einen mit ähnlichem Ordnernamen, der still bleibt."
|
||||
tags: [plan-task, vault-tools]
|
||||
created: 2026-09-26
|
||||
updated: 2026-09-26
|
||||
modell: haiku
|
||||
modell_begruendung: "Vier kurze Blöcke, wörtlich im Plan, per Skript angewendet."
|
||||
---
|
||||
|
||||
# Task 02 — Der Hook löst auch bei Workflows aus
|
||||
|
||||
Globale Vorgaben: `00-uebersicht.md` im selben Ordner, Abschnitt „Globale Vorgaben“.
|
||||
|
||||
## Zieldateien
|
||||
|
||||
- `D:\Claude\.vault-tools\session-hooks\regelwerk_nach_edit.py` (geändert)
|
||||
- `D:\Claude\.vault-tools\session-hooks\faelle_nach_edit.py` (geändert)
|
||||
|
||||
## Schritt 1 — Blöcke anwenden
|
||||
|
||||
```
|
||||
python D:/Claude/.vault-tools/plan-kopie/plan_materialisiere.py D:/Claude/.vault-tools/regelwerk-check/docs/plans/2026-09-26-workflow-steuerzeichen D:/Claude task-02-hook.md
|
||||
```
|
||||
|
||||
Erwartet: `0 Befund(e).` und `'ersetzt': 5`.
|
||||
|
||||
### Geltungsbereich im Kopf von `.vault-tools/session-hooks/regelwerk_nach_edit.py`
|
||||
|
||||
Aus:
|
||||
|
||||
```
|
||||
CLAUDE.md, AI/Regelwerk/**, .claude/agents/** oder .claude/skills/** zeigt.
|
||||
```
|
||||
|
||||
wird:
|
||||
|
||||
```
|
||||
CLAUDE.md, AI/Regelwerk/**, .claude/agents/**, .claude/skills/** oder
|
||||
.claude/workflows/** zeigt.
|
||||
```
|
||||
|
||||
### Ordnerliste in `.vault-tools/session-hooks/regelwerk_nach_edit.py`
|
||||
|
||||
Aus:
|
||||
|
||||
```
|
||||
GELTUNG_ORDNER = ("ai/regelwerk/", ".claude/agents/", ".claude/skills/")
|
||||
```
|
||||
|
||||
wird:
|
||||
|
||||
```
|
||||
GELTUNG_ORDNER = ("ai/regelwerk/", ".claude/agents/", ".claude/skills/", ".claude/workflows/")
|
||||
```
|
||||
|
||||
### Wegwerf-Vault in `.vault-tools/session-hooks/faelle_nach_edit.py`
|
||||
|
||||
Aus:
|
||||
|
||||
```
|
||||
schreibe(v, ".claude/skills/demo/SKILL.md", "---\nname: demo\n---\n")
|
||||
```
|
||||
|
||||
wird:
|
||||
|
||||
```
|
||||
schreibe(v, ".claude/skills/demo/SKILL.md", "---\nname: demo\n---\n")
|
||||
schreibe(v, ".claude/workflows/demo.js", "export const meta = {}\n")
|
||||
```
|
||||
|
||||
### Fall, der still bleibt, in `.vault-tools/session-hooks/faelle_nach_edit.py`
|
||||
|
||||
Aus:
|
||||
|
||||
```
|
||||
still("zusaetzlich | settings.json unter .claude",
|
||||
lambda v: edit(v, str(v / ".claude" / "settings.json"))),
|
||||
```
|
||||
|
||||
wird:
|
||||
|
||||
```
|
||||
still("zusaetzlich | settings.json unter .claude",
|
||||
lambda v: edit(v, str(v / ".claude" / "settings.json"))),
|
||||
still("verweis | Ordnername mit gleichem Anfang wie workflows",
|
||||
lambda v: edit(v, str(v / ".claude" / "workflowsAlt" / "x.js"))),
|
||||
```
|
||||
|
||||
### Fall, der meldet, in `.vault-tools/session-hooks/faelle_nach_edit.py`
|
||||
|
||||
Aus:
|
||||
|
||||
```
|
||||
meldet("pfad | Skilldatei",
|
||||
lambda v: edit(v, str(v / ".claude" / "skills" / "demo" / "SKILL.md"))),
|
||||
```
|
||||
|
||||
wird:
|
||||
|
||||
```
|
||||
meldet("pfad | Skilldatei",
|
||||
lambda v: edit(v, str(v / ".claude" / "skills" / "demo" / "SKILL.md"))),
|
||||
meldet("pfad | Workflowdatei",
|
||||
lambda v: edit(v, str(v / ".claude" / "workflows" / "demo.js"))),
|
||||
```
|
||||
|
||||
## Schritt 2 — Verifikation
|
||||
|
||||
```
|
||||
python D:/Claude/.vault-tools/session-hooks/faelle_nach_edit.py
|
||||
```
|
||||
|
||||
Erwartet: Exit 0; die Fälle „Workflowdatei“ und „Ordnername mit gleichem Anfang wie workflows“ stehen in der Ausgabe als bestanden.
|
||||
|
||||
Melde die Ausgabe vollständig. Läuft etwas anders: nichts anpassen, melden.
|
||||
@@ -0,0 +1,581 @@
|
||||
"""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())
|
||||
@@ -0,0 +1,517 @@
|
||||
"""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. Dazu normalisiert die Grundlinie jede
|
||||
Frontmatter-Zeile status: pending-review auf status: reference — ohne das
|
||||
bricht der Lauf nach jeder Selbstpflege an der Grundlinie ab, weil Prüfung 4
|
||||
dann anschlägt (gemessen am 2026-09-23).
|
||||
|
||||
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")
|
||||
# Prüfung 7 liest die gespeicherten Workflows. copy2 kopiert Bytes, die Grundlinie
|
||||
# trägt also dieselben Zeilenenden wie der Vault.
|
||||
(ziel / ".claude" / "workflows").mkdir(parents=True)
|
||||
for f in (VAULT / ".claude" / "workflows").glob("*.js"):
|
||||
shutil.copy2(f, ziel / ".claude" / "workflows" / f.name)
|
||||
|
||||
# jede Zeile status: pending-review auf status: reference normalisieren.
|
||||
# Ohne das bricht der Lauf nach jeder Selbstpflege an der Grundlinie ab,
|
||||
# weil Prüfung 4 (Freigabe) dann anschlägt — gemessen am 2026-09-23.
|
||||
for datei in (ziel / "AI" / "Regelwerk").glob("*.md"):
|
||||
t = datei.read_text(encoding="utf-8")
|
||||
neu = re.sub(r"^status: pending-review$", "status: reference", t, flags=re.M)
|
||||
if neu != t:
|
||||
datei.write_text(neu, encoding="utf-8")
|
||||
|
||||
# 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 s_effort_wertebereich_xhigh(v: Path) -> None:
|
||||
f = v / ".claude" / "agents" / "pruefer.md"
|
||||
t = f.read_text(encoding="utf-8")
|
||||
muster = r"^effort: high$"
|
||||
assert len(re.findall(muster, t, flags=re.M)) == 1
|
||||
f.write_text(re.sub(muster, "effort: xhigh", t, count=1, flags=re.M), encoding="utf-8")
|
||||
|
||||
|
||||
def s_effort_wertebereich_ungueltig(v: Path) -> None:
|
||||
f = v / ".claude" / "agents" / "planer.md"
|
||||
t = f.read_text(encoding="utf-8")
|
||||
muster = r"^effort: high$"
|
||||
assert len(re.findall(muster, t, flags=re.M)) == 1
|
||||
f.write_text(re.sub(muster, "effort: hoch", t, count=1, flags=re.M), encoding="utf-8")
|
||||
|
||||
|
||||
def s_effort_fehlt_erste(v: Path) -> None:
|
||||
"""Reihenfolge-Fall: effort: fehlt in der ERSTEN Agentendatei (executer.md)."""
|
||||
f = v / ".claude" / "agents" / "executer.md"
|
||||
t = f.read_text(encoding="utf-8")
|
||||
muster = r"^effort:.*\n"
|
||||
assert len(re.findall(muster, t, flags=re.M)) == 1
|
||||
f.write_text(re.sub(muster, "", t, count=1, flags=re.M), encoding="utf-8")
|
||||
|
||||
|
||||
def s_effort_fehlt_letzte(v: Path) -> None:
|
||||
"""Reihenfolge-Fall: derselbe Schaden in der LETZTEN Agentendatei (recherche.md)."""
|
||||
f = v / ".claude" / "agents" / "recherche.md"
|
||||
t = f.read_text(encoding="utf-8")
|
||||
muster = r"^effort:.*\n"
|
||||
assert len(re.findall(muster, t, flags=re.M)) == 1
|
||||
f.write_text(re.sub(muster, "", t, count=1, flags=re.M), encoding="utf-8")
|
||||
|
||||
|
||||
def s_effort_null(v: Path) -> None:
|
||||
f = v / ".claude" / "agents" / "planer.md"
|
||||
t = f.read_text(encoding="utf-8")
|
||||
muster = r"^effort:.*$"
|
||||
assert len(re.findall(muster, t, flags=re.M)) == 1
|
||||
f.write_text(re.sub(muster, "effort:", t, count=1, flags=re.M), encoding="utf-8")
|
||||
|
||||
|
||||
def s_effort_spalte_fehlt(v: Path) -> None:
|
||||
"""Entfernt in der Rollentabelle bei der Kopfzeile und allen direkt
|
||||
folgenden Tabellenzeilen die letzte Zelle — die Effort-Spalte."""
|
||||
f = v / "CLAUDE.md"
|
||||
zeilen = f.read_text(encoding="utf-8").split("\n")
|
||||
kopfzeilen = [
|
||||
i for i, z in enumerate(zeilen)
|
||||
if z.startswith("|") and "Agent" in z and "Effort" in z
|
||||
]
|
||||
assert len(kopfzeilen) == 1
|
||||
start = kopfzeilen[0]
|
||||
ende = start
|
||||
while ende < len(zeilen) and zeilen[ende].startswith("|"):
|
||||
ende += 1
|
||||
for i in range(start, ende):
|
||||
teile = zeilen[i].split("|")
|
||||
kern = teile[1:-1]
|
||||
if len(kern) < 2:
|
||||
continue
|
||||
zeilen[i] = "|" + "|".join(kern[:-1]) + "|"
|
||||
f.write_text("\n".join(zeilen), encoding="utf-8")
|
||||
|
||||
|
||||
def s_effort_verweis_leer(v: Path) -> None:
|
||||
"""Entfernt in der Tabellenzeile mit `pruefer` die Backticks um 'high'."""
|
||||
f = v / "CLAUDE.md"
|
||||
zeilen = f.read_text(encoding="utf-8").split("\n")
|
||||
treffer = [i for i, z in enumerate(zeilen) if z.startswith("|") and "`pruefer`" in z]
|
||||
assert len(treffer) == 1
|
||||
i = treffer[0]
|
||||
assert zeilen[i].count("`high`") == 1
|
||||
zeilen[i] = zeilen[i].replace("`high`", "high", 1)
|
||||
f.write_text("\n".join(zeilen), encoding="utf-8")
|
||||
|
||||
|
||||
def s_effort_freigabe_pending(v: Path) -> None:
|
||||
f = v / "AI" / "Regelwerk" / "schreibstil.md"
|
||||
t = f.read_text(encoding="utf-8")
|
||||
muster = r"^status:.*$"
|
||||
assert len(re.findall(muster, t, flags=re.M)) == 1
|
||||
f.write_text(re.sub(muster, "status: pending-review", t, count=1, flags=re.M), encoding="utf-8")
|
||||
|
||||
|
||||
def g_effort_anfuehrung(v: Path) -> None:
|
||||
"""Kein Schaden: effort in passenden Anführungszeichen ist gültiges YAML.
|
||||
|
||||
Muss stumm bleiben — Regressionslauf-Fall zu Punkt 4 der Effort-Prüfung
|
||||
(Prüfwerkzeuge.md, Abschnitt 5: neuer Code bekommt seinen eigenen Fall).
|
||||
"""
|
||||
f = v / ".claude" / "agents" / "pruefer.md"
|
||||
t = f.read_text(encoding="utf-8")
|
||||
muster = r"^effort: high$"
|
||||
assert len(re.findall(muster, t, flags=re.M)) == 1
|
||||
f.write_text(re.sub(muster, 'effort: "high"', t, count=1, flags=re.M), encoding="utf-8")
|
||||
|
||||
|
||||
def g_zusatzspalte(v: Path) -> None:
|
||||
"""Kein Schaden: eine Spalte hinter Effort mit eigenem Backtick-Wert.
|
||||
|
||||
Muss stumm bleiben — die Effort-Spalte wird über ihre Überschrift
|
||||
gefunden, nicht über die letzte Spalte.
|
||||
"""
|
||||
f = v / "CLAUDE.md"
|
||||
zeilen = f.read_text(encoding="utf-8").split("\n")
|
||||
kopfzeilen = [
|
||||
i for i, z in enumerate(zeilen)
|
||||
if z.startswith("|") and "Agent" in z and "Effort" in z
|
||||
]
|
||||
assert len(kopfzeilen) == 1
|
||||
start = kopfzeilen[0]
|
||||
zeilen[start] = zeilen[start].rstrip() + " Notiz |"
|
||||
zeilen[start + 1] = zeilen[start + 1].rstrip() + " --- |"
|
||||
i = start + 2
|
||||
while i < len(zeilen) and zeilen[i].startswith("|"):
|
||||
zeilen[i] = zeilen[i].rstrip() + " `xhigh` |"
|
||||
i += 1
|
||||
f.write_text("\n".join(zeilen), encoding="utf-8")
|
||||
|
||||
|
||||
def g_zweiter_backtick(v: Path) -> None:
|
||||
"""Kein Schaden: ein zweiter Backtick-Ausdruck in der Effort-Zelle.
|
||||
|
||||
Muss stumm bleiben — genommen wird der erste Backtick-Ausdruck.
|
||||
"""
|
||||
f = v / "CLAUDE.md"
|
||||
zeilen = f.read_text(encoding="utf-8").split("\n")
|
||||
treffer = [i for i, z in enumerate(zeilen) if z.startswith("|") and "`pruefer`" in z]
|
||||
assert len(treffer) == 1
|
||||
i = treffer[0]
|
||||
assert zeilen[i].count("`high` (Frontmatter)") == 1
|
||||
zeilen[i] = zeilen[i].replace(
|
||||
"`high` (Frontmatter)", "`high` (Frontmatter, nicht `xhigh`)", 1
|
||||
)
|
||||
f.write_text("\n".join(zeilen), 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")
|
||||
|
||||
|
||||
# Steuerzeichen entstehen nur ueber Codepunkte, nie als Escape-Folge im Quelltext: Die
|
||||
# Schreibschicht hat solche Folgen schon still in echte Zeichen verwandelt
|
||||
# (codeworkflow.md, "Escape-Folgen und unsichtbare Zeichen").
|
||||
CR = bytes([13])
|
||||
LF = bytes([10])
|
||||
|
||||
|
||||
def _workflows(v: Path) -> list[Path]:
|
||||
dateien = sorted((v / ".claude" / "workflows").glob("*.js"))
|
||||
assert len(dateien) >= 2, "die Grundlinie braucht mindestens zwei gespeicherte Workflows"
|
||||
return dateien
|
||||
|
||||
|
||||
def _haenge_an(f: Path, zusatz: bytes) -> None:
|
||||
f.write_bytes(f.read_bytes() + zusatz)
|
||||
|
||||
|
||||
def s_workflow_crlf_erste_datei(v: Path) -> None:
|
||||
f = _workflows(v)[0]
|
||||
f.write_bytes(f.read_bytes().replace(LF, CR + LF))
|
||||
|
||||
|
||||
def s_workflow_cr_letzte_datei_letzte_zeile(v: Path) -> None:
|
||||
f = _workflows(v)[-1]
|
||||
f.write_bytes(f.read_bytes().rstrip(LF) + CR + LF)
|
||||
|
||||
|
||||
def s_workflow_nul(v: Path) -> None:
|
||||
_haenge_an(_workflows(v)[0], b"// " + bytes([0]) + LF)
|
||||
|
||||
|
||||
def s_workflow_bidi(v: Path) -> None:
|
||||
_haenge_an(_workflows(v)[0], ("// " + chr(0x202E) + "x").encode("utf-8") + LF)
|
||||
|
||||
|
||||
def s_workflow_bom(v: Path) -> None:
|
||||
f = _workflows(v)[-1]
|
||||
f.write_bytes(chr(0xFEFF).encode("utf-8") + f.read_bytes())
|
||||
|
||||
|
||||
def s_workflow_tab(v: Path) -> None:
|
||||
_haenge_an(_workflows(v)[0], b"//" + bytes([9]) + b"x" + LF)
|
||||
|
||||
|
||||
def s_workflow_kein_utf8(v: Path) -> None:
|
||||
_haenge_an(_workflows(v)[0], b"// " + bytes([0xFF]) + LF)
|
||||
|
||||
|
||||
def s_workflow_unterordner(v: Path) -> None:
|
||||
unter = v / ".claude" / "workflows" / "unter"
|
||||
unter.mkdir()
|
||||
(unter / "tief.js").write_bytes(b"a" + CR + LF)
|
||||
|
||||
|
||||
def g_workflow_typografie(v: Path) -> None:
|
||||
"""Kein Schaden: typografische Zeichen und Umlaute sind keine Steuerzeichen."""
|
||||
zeichen = "".join(chr(c) for c in (0x201E, 0x201C, 0x2013, 0x2014, 0x2026, 0xE4, 0x20AC))
|
||||
_haenge_an(_workflows(v)[0], ("// " + zeichen).encode("utf-8") + LF)
|
||||
|
||||
|
||||
def g_workflow_ordner_fehlt(v: Path) -> None:
|
||||
"""Kein Schaden: ohne Ordner gibt es nichts zu pruefen."""
|
||||
shutil.rmtree(v / ".claude" / "workflows")
|
||||
|
||||
|
||||
# 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),
|
||||
("anfuehrung | effort in Anführungszeichen", g_effort_anfuehrung),
|
||||
("zusatzspalte | Spalte mit Backtick-Wert hinter Effort", g_zusatzspalte),
|
||||
("zweiter-backtick | Effort-Zelle mit zweitem Backtick-Wert", g_zweiter_backtick),
|
||||
("typografie | Anführung, Striche, Umlaut im Workflow", g_workflow_typografie),
|
||||
("ordner-fehlt | kein Workflow-Ordner", g_workflow_ordner_fehlt),
|
||||
]
|
||||
|
||||
|
||||
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"),
|
||||
("wertebereich| effort: xhigh statt high", s_effort_wertebereich_xhigh, "effort: xhigh — Rollentabelle im Stamm sagt high"),
|
||||
("wertebereich| effort: hoch (ungültig)", s_effort_wertebereich_ungueltig, "ist nicht in der Vorgabe"),
|
||||
("fehlt | effort fehlt, ERSTE Agentendatei", s_effort_fehlt_erste, ".claude/agents/executer.md — kein effort: im Frontmatter"),
|
||||
("fehlt | effort fehlt, LETZTE Agentendatei", s_effort_fehlt_letzte, ".claude/agents/recherche.md — kein effort: im Frontmatter"),
|
||||
("null | effort ohne Wert", s_effort_null, ".claude/agents/planer.md — kein effort: im Frontmatter"),
|
||||
("umgebung | Effort-Spalte fehlt", s_effort_spalte_fehlt, "Effort-Spalte der Rollentabelle nicht auffindbar"),
|
||||
("verweis | Effort-Zelle ohne Wert", s_effort_verweis_leer, "Rollentabelle nennt für 'pruefer' keinen Effort"),
|
||||
("freigabe | status: pending-review", s_effort_freigabe_pending, "Freigabe steht aus"),
|
||||
("zeichen | CRLF in der ERSTEN Workflowdatei", s_workflow_crlf_erste_datei, "U+000D"),
|
||||
("zeichen | CR in der LETZTEN Zeile der LETZTEN Workflowdatei", s_workflow_cr_letzte_datei_letzte_zeile, "U+000D"),
|
||||
("zeichen | NUL", s_workflow_nul, "U+0000"),
|
||||
("zeichen | Bidi-Umkehr", s_workflow_bidi, "U+202E"),
|
||||
("zeichen | BOM am Dateianfang", s_workflow_bom, "Zeile 1, Spalte 1: U+FEFF"),
|
||||
("zeichen | Tab", s_workflow_tab, "U+0009"),
|
||||
("kodierung | Workflow ist kein UTF-8", s_workflow_kein_utf8, "nicht als UTF-8 lesbar"),
|
||||
("zusaetzlich| CR in einem Unterordner", s_workflow_unterordner, ".claude/workflows/unter/tief.js"),
|
||||
]
|
||||
|
||||
|
||||
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