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,229 @@
|
||||
---
|
||||
type: plan
|
||||
title: "Sitzungs-Hooks und Schließzaun im Linkcheck"
|
||||
status: active
|
||||
summary: "Nach dem Plan gibt ein SessionStart-Hook den Stand des Sitzungsprotokolls als Kontext aus, ein PostToolUse-Hook startet den Regelwerk-Check nach Änderungen an der Anweisungsschicht, und der Linkcheck schließt Codeblöcke nach CommonMark."
|
||||
tags: [plan, vault-tools]
|
||||
created: 2026-09-13
|
||||
updated: 2026-09-13
|
||||
---
|
||||
|
||||
# Sitzungs-Hooks und Schließzaun im Linkcheck
|
||||
|
||||
## Ziel
|
||||
|
||||
Nach diesem Plan liegen zwei Hook-Skripte unter `.vault-tools/session-hooks/`, jedes mit Regressionstreiber. `sitzungsstart.py` zählt zu Sitzungsbeginn, was das Sitzungsprotokoll in `CLAUDE.md` verlangt, und gibt es als Kontext aus. `regelwerk_nach_edit.py` startet `regelwerk_check.py`, sobald eine Datei von Stamm, Regelwerk, Agenten oder Skills geändert wurde, und meldet Befunde, eine fehlende, abstürzende oder hängende Prüfung. Außerdem schließt `strip_code()` im Linkcheck einen Codeblock nur noch mit einem passenden Zaun. Damit verschwindet der Fehlalarm `[[:space:]]` in `Code Projekte/active - Logging/docs/plans/2026-09-08-a-betriebsmodelle/task-09-gate.md`. Wirksam in Claude Code werden die Hooks erst mit dem Eintrag in `settings.json`, und der ist nicht Teil dieses Plans.
|
||||
|
||||
## Nicht im Umfang
|
||||
|
||||
- Einträge in `.claude/settings.json` oder `.claude/settings.local.json`. Die macht der Koordinator nach der Prüfung.
|
||||
- Jede Änderung außerhalb von `.vault-tools/session-hooks/` und `.vault-tools/linkcheck/`. Auch der Kommentar in `regelwerk_check.py`, der die Zahl der Linkcheck-Fälle nennt, bleibt stehen.
|
||||
- Die Öffnerregel des Linkchecks (Backtick im Info-String). Sie ist im Entwurf als Lücke benannt.
|
||||
- `CHANGELOG.md`: `.vault-tools/` führt keins.
|
||||
|
||||
## Architektur
|
||||
|
||||
```text
|
||||
Claude Code
|
||||
SessionStart ──stdin JSON──> sitzungsstart.py ──stdout JSON──> additionalContext
|
||||
│
|
||||
└─ importiert strip_code() aus linkcheck/linkcheck.py
|
||||
|
||||
PostToolUse (Edit|Write|MultiEdit)
|
||||
──stdin JSON──> regelwerk_nach_edit.py
|
||||
│ Pfad im Geltungsbereich? nein -> still
|
||||
└─ startet regelwerk-check/regelwerk_check.py --vault <wurzel>
|
||||
ohne Befund -> still
|
||||
sonst -> stdout JSON (additionalContext, systemMessage)
|
||||
|
||||
linkcheck/linkcheck.py strip_code() <── auch von regelwerk_check.py importiert
|
||||
```
|
||||
|
||||
Beide Hooks enden immer mit Exit 0. Entscheidungen und Herkunft stehen in `.vault-tools/session-hooks/ENTWURF.md` (entsteht in Task 1), die Regel zum Schließzaun in `.vault-tools/linkcheck/ENTWURF-codeblock-erkennung.md` (Task 3).
|
||||
|
||||
## Globale Vorgaben
|
||||
|
||||
Diese Vorgaben gelten für jeden Task. Die Taskdateien verweisen hierher und wiederholen sie nicht.
|
||||
|
||||
1. **Abschrift, keine Autorschaft.** Code und Texte aus dem Plan werden Zeichen für Zeichen übernommen. Wer einen Widerspruch im Plan findet, meldet ihn und löst ihn nicht still auf. Kommentare und Docstrings werden nicht umformuliert.
|
||||
2. **Test zuerst.** Erst den Treiber oder die Fälle anlegen, den Fehlschlag im Wortlaut beobachten und melden, dann die Umsetzung. Weicht der Fehlschlag vom Plan ab, anhalten und melden.
|
||||
3. **Nur die Zieldateien des eigenen Tasks.** Keine weiteren Dateien, keine Hilfsskripte im Vault. Temporäres gehört ins Temp-Verzeichnis, das der Treiber selbst anlegt und löscht.
|
||||
4. **Shell.** PowerShell, Arbeitsverzeichnis `D:\Claude`. Vor jedem Python-Aufruf `$env:PYTHONIOENCODING = "utf-8"`. Interpreter ist `python`, nie `python3`. Kein `bash`, kein Heredoc, kein `python -c`, kein `python -`.
|
||||
5. **Dateien.** UTF-8 ohne BOM, Zeilenende LF. Nach dem Anlegen oder Ändern einer Python-Datei `python -m py_compile <datei>`.
|
||||
6. **Nur Standardbibliothek**, Python 3.13.
|
||||
7. **Kein Commit.** Der Vault-Root ist kein Git-Repository, und Subagenten committen nie.
|
||||
8. **Keine Geheimnisse lesen.** `.claude/settings.json`, `.claude/settings.local.json` und Transkripte werden nicht geöffnet.
|
||||
9. **Schlägt ein Fall fehl, wird nicht am Code gedreht, bis er grün ist.** Gemeldet werden Fall, vollständige Ausgabe und die vermutete Ursache. Die Korrektur passiert im Plan.
|
||||
|
||||
## Dateiübersicht
|
||||
|
||||
| Datei | Task | Art |
|
||||
|---|---|---|
|
||||
| `.vault-tools/session-hooks/sitzungsstart.py` | 1 | neu |
|
||||
| `.vault-tools/session-hooks/faelle_sitzungsstart.py` | 1 | neu |
|
||||
| `.vault-tools/session-hooks/ENTWURF.md` | 1 | neu |
|
||||
| `.vault-tools/session-hooks/regelwerk_nach_edit.py` | 2 | neu |
|
||||
| `.vault-tools/session-hooks/faelle_nach_edit.py` | 2 | neu |
|
||||
| `.vault-tools/linkcheck/linkcheck.py` | 3 | ändern |
|
||||
| `.vault-tools/linkcheck/faelle.py` | 3 | ändern |
|
||||
| `.vault-tools/linkcheck/ENTWURF-codeblock-erkennung.md` | 3 | ändern |
|
||||
|
||||
## Tasks
|
||||
|
||||
| Nr | Datei | Zieldateien | Modell | parallel mit |
|
||||
|---|---|---|---|---|
|
||||
| 1 | `task-01-sitzungsstart.md` | `sitzungsstart.py`, `faelle_sitzungsstart.py`, `ENTWURF.md` in `session-hooks/` | sonnet | 2, 3 |
|
||||
| 2 | `task-02-regelwerk-nach-edit.md` | `regelwerk_nach_edit.py`, `faelle_nach_edit.py` in `session-hooks/` | sonnet | 1, 3 |
|
||||
| 3 | `task-03-schliesszaun.md` | `linkcheck.py`, `faelle.py`, `ENTWURF-codeblock-erkennung.md` in `linkcheck/` | haiku | 1, 2 |
|
||||
|
||||
Die Zieldateien sind disjunkt, alle drei Tasks können gleichzeitig laufen. Eine Abhängigkeit gibt es zur Laufzeit: `sitzungsstart.py` importiert `strip_code()` aus `linkcheck.py`, das Task 3 ändert. Die Fälle von Task 1 hängen nicht von der Schließzaun-Regel ab, ein Lauf während einer halb geschriebenen Datei kann aber scheitern. Deshalb wiederholt die Abnahme den Treiber von Task 1 nach dem Ende von Task 3. Wer Nebenläufigkeit ganz vermeiden will, startet Task 3 zuerst, er ist der kleinste.
|
||||
|
||||
## Vor dem Start (Koordinator)
|
||||
|
||||
Den Linkcheck einmal vor Task 3 laufen lassen und die Ausgabe sichern, sonst fehlt der Vergleich:
|
||||
|
||||
```powershell
|
||||
Set-Location D:\Claude
|
||||
$env:PYTHONIOENCODING = "utf-8"
|
||||
python .vault-tools\linkcheck\linkcheck.py --vault . --alle > "$env:TEMP\linkcheck-vorher.txt"; "exit=$LASTEXITCODE"
|
||||
```
|
||||
|
||||
Laut Sitzungsprotokoll `AI/sessions/2026-09-13-18.md` meldete er 3 tote Links bei 1006 Notizen, einer davon der Fehlalarm in `task-09-gate.md`.
|
||||
|
||||
## Abnahme
|
||||
|
||||
1. Die Verifikationsbefehle aller drei Taskdateien selbst wiederholen, nicht aus dem Bericht übernehmen.
|
||||
2. Den Treiber von Task 1 nach dem Ende von Task 3 noch einmal fahren.
|
||||
3. Jede neu angelegte Datei byteweise gegen ihren Codeblock im Plan vergleichen, bei Task 3 jede ersetzte Stelle gegen den Neutext. `.vault-tools/plan-diff/plan_diff.py` erwartet eine andere Form der Pfadzeile, als diese Taskdateien sie haben. Ob es hier trägt, ist nicht geprüft, im Zweifel von Hand vergleichen.
|
||||
4. Je Treiber mindestens einen Fall absichtlich kaputt machen und rot sehen. In `sitzungsstart.py` `(?<!\S)` aus `_NEEDS_REVIEW` entfernen: Der Fall „aehnliche Tags zaehlen nicht" muss fallen. In `regelwerk_nach_edit.py` das `+ "/"` im Präfixvergleich streichen: Der Fall „Datei neben dem Vault mit gleichem Namensanfang" muss fallen. Der Fall „Nachbarordner mit gleichem Namensanfang" bleibt bei diesem Eingriff grün, weil der Rest des Pfads nicht passt; das ist kein blinder Fleck. In `faelle.py` von Task 3 genügt der Blick auf die drei Fälle, die vor der Umsetzung rot waren. Danach alles zurücksetzen.
|
||||
5. Lauf mit echten Daten wie unten.
|
||||
|
||||
## Lauf mit echten Daten
|
||||
|
||||
**Task 1.** `python .vault-tools\session-hooks\faelle_sitzungsstart.py --echt` zweimal. Stand beim Planen, zum Vergleich:
|
||||
|
||||
- Tagesnotiz `Inbox/2026-09-13.md` vorhanden.
|
||||
- Inbox ohne `type: daily`: `2026-09-13-vernetzung-vault-claude.md`. Die übrigen 28 Dateien in `Inbox/` tragen `type: daily`.
|
||||
- `needs-review`: `Inbox/2026-09-13-vernetzung-vault-claude.md` über den Frontmatter-Tag. Den Text `#needs-review` gab es beim Planen in 17 anderen Dateien. In den gelesenen Zeilen steht er in Backticks, auch in `CLAUDE.md` Zeile 118, oder in Zeilen, die zu Codebeispielen gehören; ob diese Zeilen in einem Fenced-Block liegen, wurde nicht geprüft. Nicht gelesen wurden zwei lange Zeilen: `Code Projekte/active - Agentic Cockpit/docs/plans/2026-09-05-agentic-cockpit/task-04-vault-reader-teil-1.md` Zeile 53 und `Code Projekte/active - Agentic Cockpit/docs/code/vault_reader.md` Zeile 26. Jeden Treffer des Hooks an seiner Zeile nachlesen. Steht das Tag dort ohne Backticks im Fließtext, stimmt der Treffer.
|
||||
- `review` ohne `review-done`: Zahl gegen `Review-Queue.base` in Obsidian halten. Eine Abweichung muss sich aus den Ausschlüssen (`Import/`, Codeblöcke, Inline-Code) erklären lassen, sonst ist sie ein Befund.
|
||||
- `pending-review`: `pruefwerkzeuge.md`.
|
||||
- Sitzungsdateien vom 2026-09-13: beim Planen `2026-09-13-16.md`, `2026-09-13-16b.md`, `2026-09-13-18.md`, dazu alles, was seitdem entstanden ist.
|
||||
- Laufzeit beider Läufe unter 5 Sekunden.
|
||||
|
||||
Die Gegenprüfung für `needs-review` läuft mit eigener Suche, nicht mit dem Treiber: Grep über `*.md` nach `needs-review`, dann Ausschlüsse, Backticks und Codeblöcke von Hand abziehen.
|
||||
|
||||
**Task 2.** `python .vault-tools\session-hooks\faelle_nach_edit.py --echt`. Solange `pruefwerkzeuge.md` auf `pending-review` steht: `systemMessage` mit `Exit 1, 1 Befundzeile(n)` und im Kontext `[klein] AI/Regelwerk/pruefwerkzeuge.md — status: pending-review — Freigabe steht aus`. Danach einen Hook-Lauf mit einem Pfad außerhalb des Geltungsbereichs von Hand prüfen, er muss still bleiben:
|
||||
|
||||
```powershell
|
||||
Set-Location D:\Claude
|
||||
$env:PYTHONIOENCODING = "utf-8"
|
||||
'{"cwd": "D:\\Claude", "tool_name": "Edit", "tool_input": {"file_path": "D:\\Claude\\Inbox\\2026-09-13.md"}}' | python .vault-tools\session-hooks\regelwerk_nach_edit.py; "exit=$LASTEXITCODE"
|
||||
```
|
||||
|
||||
Erwartet: nur `exit=0`, keine Zeile davor.
|
||||
|
||||
**Task 3.** Den Linkcheck nach Task 3 erneut fahren und gegen `linkcheck-vorher.txt` halten. Erwartet: Die Zeile mit `task-09-gate.md -> [[:space:]]` fehlt, alle anderen Befunde sind gleich. Dazu der Vergleich alte gegen neue Zaunerkennung über alle Notizen, weil der Linkcheck nur tote Links zeigt und ein jetzt verschluckter gültiger Link dort nicht auffällt. Das Skript gehört ins Scratchpad, nicht in den Vault:
|
||||
|
||||
```python
|
||||
"""Vergleicht alte und neue Zaunerkennung über den Vault: Welche Notizen liefern andere Wikilinks?
|
||||
|
||||
Nur für die Abnahme von Task 3, gehört nicht in den Vault.
|
||||
"""
|
||||
import re
|
||||
import sys
|
||||
|
||||
VAULT = r"D:\Claude"
|
||||
sys.path.insert(0, VAULT + r"\.vault-tools\linkcheck")
|
||||
sys.path.insert(0, VAULT + r"\.vault-tools\obsidian-vault-navigator\scripts")
|
||||
import vaultlib as V # noqa: E402
|
||||
from linkcheck import strip_code as neu # noqa: E402
|
||||
|
||||
_FENCE_RE = re.compile(r"^\s{0,3}(`{3,}|~{3,})")
|
||||
_INLINE_CODE_RE = re.compile(r"(`+)(?:(?!\1).)*?\1")
|
||||
|
||||
|
||||
def alt(body):
|
||||
"""strip_code() in der Fassung vor Task 3."""
|
||||
if not body:
|
||||
return ""
|
||||
zaun = None
|
||||
aus = []
|
||||
for zeile in body.split("\n"):
|
||||
m = _FENCE_RE.match(zeile)
|
||||
if zaun is None:
|
||||
if m:
|
||||
zaun = m.group(1)[0] * 3
|
||||
aus.append(" " * len(zeile))
|
||||
continue
|
||||
aus.append(_INLINE_CODE_RE.sub(lambda t: " " * len(t.group(0)), zeile))
|
||||
else:
|
||||
if m and m.group(1)[0] * 3 == zaun:
|
||||
zaun = None
|
||||
aus.append(" " * len(zeile))
|
||||
return "\n".join(aus)
|
||||
|
||||
|
||||
def links(text):
|
||||
return {m.group(1).strip() for m in V._WIKILINK_RE.finditer(text)}
|
||||
|
||||
|
||||
def main():
|
||||
sys.stdout.reconfigure(encoding="utf-8")
|
||||
geprueft = 0
|
||||
anders = 0
|
||||
for ap, rp in V.walk_vault(VAULT):
|
||||
if not rp.endswith(".md"):
|
||||
continue
|
||||
with open(ap, "r", encoding="utf-8", errors="replace") as fh:
|
||||
_, body = V.parse_frontmatter(fh.read())
|
||||
geprueft += 1
|
||||
a, n = links(alt(body)), links(neu(body))
|
||||
if a != n:
|
||||
anders += 1
|
||||
print(rp)
|
||||
print(" nur alt:", sorted(a - n))
|
||||
print(" nur neu:", sorted(n - a))
|
||||
print(f"{anders} von {geprueft} Notizen liefern andere Wikilinks.")
|
||||
|
||||
|
||||
main()
|
||||
```
|
||||
|
||||
Erwartet ist mindestens `task-09-gate.md` mit `nur alt: [':space:']`. Jede weitere Notiz mit `nur alt:` ist ein Link, den der Linkcheck nicht mehr sieht, und wird an der Stelle nachgelesen. Ist es ein echter Link hinter einem Block, den bisher eine Zeile mit Info-String versehentlich geschlossen hat, stimmt die neue Regel und die Notiz hat einen Zaunfehler. Ist es kein Zaunfehler, ist das ein Befund gegen Task 3.
|
||||
|
||||
## Risiken
|
||||
|
||||
- **Rauschen nach jedem Edit.** Solange `pruefwerkzeuge.md` auf `pending-review` steht, meldet `regelwerk_nach_edit.py` bei jeder Änderung an Stamm, Regelwerk, Agenten oder Skills denselben Befund. Das sieht man an der `systemMessage`. Ein Hook, der immer anschlägt, wird überlesen.
|
||||
- **SessionStart feuert öfter als gedacht.** Ohne Matcher läuft der Hook auch bei `resume`, `clear`, `compact` und `fork` und legt denselben Kontext erneut ab. Die Laufzeit fällt dann jedes Mal an.
|
||||
- **Verschluckte Links.** Ein Block, den bisher eine Zeile mit Info-String versehentlich geschlossen hat, läuft nach Task 3 weiter, notfalls bis Dateiende. Der Linkcheck prüft die Links dahinter dann nicht mehr. Sichtbar macht das nur der Vergleichslauf oben.
|
||||
- **Timeout-Reihenfolge.** `regelwerk_nach_edit.py` bricht den Check nach 20 Sekunden selbst ab. Setzt der `settings.json`-Eintrag ein `timeout` unter etwa 25 Sekunden, beendet Claude Code den Hook, bevor er melden kann, und die Meldung fehlt.
|
||||
- **Falsche Wurzel.** Beide Hooks nehmen `cwd` aus der Eingabe. Läuft eine Sitzung mit einem anderen `cwd` als `D:\Claude` und greift der Eintrag trotzdem, zeigt `sitzungsstart.py` das in der Kopfzeile. `regelwerk_nach_edit.py` bleibt dann bei Vault-Dateien still, und das sieht niemand. Ob Einträge aus `D:\Claude\.claude\settings.json` bei einem anderen Startordner überhaupt geladen werden, ist nicht geprüft.
|
||||
- **Laufzeit.** Die 5 Sekunden sind ein Ziel, gemessen wird am echten Vault. Bei kaltem Dateicache oder wachsendem Vault kann `sitzungsstart.py` länger brauchen. Der erste `--echt`-Lauf zeigt eher den kalten Wert, der zweite den warmen.
|
||||
- **Mitternacht.** Die Treiber rechnen das Datum einmal beim Start. Läuft ein Treiber über Mitternacht, schlagen datumsabhängige Fälle fehl, ohne dass am Hook etwas falsch ist.
|
||||
- **Regressionslauf des Regelwerk-Checks.** `.vault-tools/regelwerk-check/regressionslauf.py` bricht nach dem Code schon heute an der Grundlinie ab, weil die Kopie des Regelwerks `pruefwerkzeuge.md` mit `pending-review` enthält. Das ist aus dem Code gelesen, nicht gefahren, und hat mit Task 3 nichts zu tun. Ein Abbruch dort ist kein Beleg gegen Task 3.
|
||||
- **Python auf dem PATH.** Die Exec-Form startet `python` über den PATH. Ändert sich der PATH, scheitert der Hook mit einem Exit ungleich 0. Claude Code meldet das als nicht blockierenden Fehler, die Hook-Ausgabe fehlt dann.
|
||||
|
||||
## Getroffene Annahmen
|
||||
|
||||
- Inbox-Notizen werden auch in Unterordnern von `Inbox/` gezählt, nur `.md`-Dateien, Punktordner ausgenommen.
|
||||
- Die Zählung `review` ohne `review-done` hat dieselben Ausschlüsse wie `needs-review` (`Import/`, `_index/`, Punktordner, `node_modules`).
|
||||
- `Import/` und `_index/` fallen nur direkt unter der Wurzel heraus, Punktordner und `node_modules` in jeder Tiefe.
|
||||
- Tags werden ohne Rücksicht auf Groß- und Kleinschreibung verglichen. Ein Inline-Tag steht am Zeilenanfang oder nach Leerraum. `#needs-review/x` und `#needs-review-done` zählen nicht als `needs-review`.
|
||||
- Kaputtes YAML wird tolerant gelesen: Ein Tag in einer nicht geschlossenen Liste zählt. Ein Frontmatter ohne Schlusszeile gilt nicht als Frontmatter.
|
||||
- `regelwerk_nach_edit.py` bestimmt die Wurzel wie `sitzungsstart.py` (`cwd`, sonst zwei Ebenen über dem Skript).
|
||||
- Ein relativer `file_path` gilt relativ zur Wurzel. Der Pfadvergleich ignoriert Groß- und Kleinschreibung im ganzen Pfad, nicht nur beim Laufwerk.
|
||||
- Still bleibt der Regelwerk-Hook nach dem Check nur bei Exit 0 mit der Schlusszeile „Keine der N gelaufenen Prüfungen hat angeschlagen." und ohne `ACHTUNG`-Zeile. Exit 0 mit `ACHTUNG` ist ein Befund.
|
||||
- Interner Timeout 20 Sekunden, Kontext höchstens 40 Zeilen und 4000 Zeichen.
|
||||
- `tool_name` wird nicht geprüft, das übernimmt der Matcher.
|
||||
- `ENTWURF.md` für beide Hooks gehört zu Task 1, damit die Zieldateien disjunkt bleiben.
|
||||
- Jeder Punkt des Sitzungsstart-Hooks läuft in einem eigenen `try`. Das geht über den Auftrag hinaus, dort war nur ein Fehler insgesamt verlangt.
|
||||
- Der Wortlaut der CommonMark-Spezifikation wurde nicht gezogen. Die Schließzaun-Regel ist an markdown-it-py 4.2.0 belegt, das lokal im Projekt Agentic Cockpit liegt.
|
||||
- In `ENTWURF-codeblock-erkennung.md` wird die gepflegte Zahl „vierzehn Angriffsfälle" durch „alle Angriffsfälle aus `faelle.py`" ersetzt.
|
||||
|
||||
## Offene Punkte für den Koordinator
|
||||
|
||||
- `settings.json`-Einträge: Matcher für SessionStart (alle Quellen oder nur `startup` und `clear`) und ein `timeout` von mindestens 30 Sekunden für den PostToolUse-Eintrag.
|
||||
- Das Rauschen durch `pruefwerkzeuge.md` auf `pending-review`: freigeben lassen oder für die Zeit bis zur Freigabe hinnehmen.
|
||||
- Den Abschnitt 4.5 der CommonMark-Spezifikation 0.31.2 gegen die Regel in `ENTWURF-codeblock-erkennung.md` halten und den Vermerk „Abgleich offen" danach streichen oder korrigieren.
|
||||
- `regelwerk_check.py` nennt im Kommentar 17 Linkcheck-Fälle, nach Task 3 sind es mehr. Die Datei liegt außerhalb dieses Plans.
|
||||
- `regressionslauf.py` des Regelwerk-Checks bricht wegen `pending-review` an der Grundlinie ab. Das ist unabhängig von diesem Plan, verdeckt aber jeden Regressionsnachweis dort.
|
||||
+1040
File diff suppressed because it is too large.
Load diff
+792
@@ -0,0 +1,792 @@
|
||||
---
|
||||
type: plan-task
|
||||
title: "Task 02 — PostToolUse-Hook regelwerk_nach_edit.py"
|
||||
status: draft
|
||||
summary: "Hook startet regelwerk_check.py nach jeder Änderung an Stamm, Regelwerk, Agenten oder Skills und meldet Befunde, fehlende, abstürzende oder hängende Prüfung; Regressionstreiber mit Attrappe."
|
||||
tags: [plan-task, vault-tools]
|
||||
created: 2026-09-13
|
||||
updated: 2026-09-13
|
||||
modell: sonnet
|
||||
modell_begruendung: "Der Code steht wörtlich im Plan, aber der Treiber läuft zum ersten Mal gegen Windows-Pfad- und Prozessverhalten (Laufwerksschreibweise, Timeout mit Kill); schlägt ein Fall fehl, muss die Ursache eingegrenzt und als Planwiderspruch gemeldet werden, das ist Fehlersuche und keine Abschrift."
|
||||
---
|
||||
|
||||
# Task 02 — PostToolUse-Hook `regelwerk_nach_edit.py`
|
||||
|
||||
Globale Vorgaben: `00-uebersicht.md` im selben Ordner, Abschnitt „Globale Vorgaben". Sie gelten hier vollständig und werden nicht wiederholt.
|
||||
|
||||
## Zieldateien
|
||||
|
||||
- `D:\Claude\.vault-tools\session-hooks\faelle_nach_edit.py` (neu)
|
||||
- `D:\Claude\.vault-tools\session-hooks\regelwerk_nach_edit.py` (neu)
|
||||
|
||||
## Verbote
|
||||
|
||||
- Keine andere Datei anlegen oder ändern. `ENTWURF.md` in diesem Ordner gehört Task 1, `regelwerk_check.py` bleibt unberührt.
|
||||
- Kein Eintrag in `.claude/settings.json` oder `.claude/settings.local.json`, und diese Dateien auch nicht lesen.
|
||||
- Den echten `regelwerk_check.py` im Treiber nicht verändern oder ersetzen. Die Attrappe entsteht ausschließlich im Temp-Verzeichnis.
|
||||
- Schlägt ein Fall fehl: Code nicht anpassen, bis er grün ist. Fall, Ausgabe und vermutete Ursache melden.
|
||||
|
||||
## Schritt 1 — Test zuerst: `faelle_nach_edit.py`
|
||||
|
||||
Datei `D:\Claude\.vault-tools\session-hooks\faelle_nach_edit.py` mit genau diesem Inhalt anlegen:
|
||||
|
||||
```python
|
||||
"""Fährt die Angriffsliste gegen regelwerk_nach_edit.py, je Fall auf einem Wegwerf-Vault.
|
||||
|
||||
Ersteller: Pascal Michel
|
||||
Erstellt: 2026-09-13
|
||||
Entwurf: ENTWURF.md im selben Ordner
|
||||
|
||||
Statt des echten regelwerk_check.py liegt im Wegwerf-Vault eine Attrappe. Sie
|
||||
schreibt ihren Aufruf nach aufruf.json und zeigt dann ein festgelegtes
|
||||
Verhalten: grün, Befund, Abbruch, ACHTUNG, stumm, Absturz, hängt, sehr viel
|
||||
Ausgabe. Ihre Zeilen haben die Form der echten Ausgabe (regelwerk_check.py
|
||||
Zeilen 114, 368, 370 und 372). So lässt sich je Fall unterscheiden, ob der
|
||||
Hook still blieb, ohne die Prüfung zu starten, oder still blieb, nachdem sie
|
||||
grün war.
|
||||
|
||||
Grundlinie zuerst: Ein grüner Check muss still bleiben und nachweislich
|
||||
gelaufen sein, ein Befund muss gemeldet werden. Sonst misst der Rest nichts.
|
||||
|
||||
Aufruf:
|
||||
python .vault-tools/session-hooks/faelle_nach_edit.py
|
||||
python .vault-tools/session-hooks/faelle_nach_edit.py --echt
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import contextlib
|
||||
import importlib.util
|
||||
import io
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import time
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
from typing import Callable
|
||||
|
||||
HIER = Path(__file__).resolve().parent
|
||||
HOOK = HIER / "regelwerk_nach_edit.py"
|
||||
MAX_KONTEXT = 4100
|
||||
|
||||
FAKE_KOPF = '''import json
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
Path(__file__).resolve().parents[2].joinpath("aufruf.json").write_text(
|
||||
json.dumps({"argv": sys.argv[1:], "exe": sys.executable}), encoding="utf-8")
|
||||
sys.stdout.reconfigure(encoding="utf-8")
|
||||
'''
|
||||
|
||||
FAKES = {
|
||||
"gruen": FAKE_KOPF + '''print()
|
||||
print("Keine der 6 gelaufenen Prüfungen hat angeschlagen.")
|
||||
sys.exit(0)
|
||||
''',
|
||||
"befund": FAKE_KOPF + '''print("[klein] AI/Regelwerk/regel.md — status: pending-review — Freigabe steht aus")
|
||||
print()
|
||||
print("1 Befund(e) aus 6 von 6 Prüfungen.")
|
||||
sys.exit(1)
|
||||
''',
|
||||
"abbruch": FAKE_KOPF + '''print("[blockierend] regelwerk_check.py — Abbruch: KeyError: 'x'")
|
||||
print()
|
||||
print("1 Befund(e) aus 2 von 6 Prüfungen.")
|
||||
sys.exit(2)
|
||||
''',
|
||||
"achtung": FAKE_KOPF + '''print()
|
||||
print("Keine der 5 gelaufenen Prüfungen hat angeschlagen.")
|
||||
print("ACHTUNG: nur 5 von 6 Prüfungen sind gelaufen — der Rest hatte keine Voraussetzung.")
|
||||
sys.exit(0)
|
||||
''',
|
||||
"stumm": FAKE_KOPF + '''sys.exit(0)
|
||||
''',
|
||||
"absturz": FAKE_KOPF + '''raise RuntimeError("Absturz im Check")
|
||||
''',
|
||||
"haengt": FAKE_KOPF + '''time.sleep(120)
|
||||
''',
|
||||
"viel": FAKE_KOPF + '''for i in range(200):
|
||||
print("[wichtig] AI/Regelwerk/regel.md — toter Wikilink: [[ziel%d]]" % i)
|
||||
print()
|
||||
print("200 Befund(e) aus 6 von 6 Prüfungen.")
|
||||
sys.exit(1)
|
||||
''',
|
||||
}
|
||||
|
||||
|
||||
def schreibe(vault: Path, rel: str, inhalt: str | bytes) -> None:
|
||||
# Immer Bytes: write_text würde unter Windows jedes LF zu CRLF machen.
|
||||
pfad = vault / rel
|
||||
pfad.parent.mkdir(parents=True, exist_ok=True)
|
||||
pfad.write_bytes(inhalt if isinstance(inhalt, bytes) else inhalt.encode("utf-8"))
|
||||
|
||||
|
||||
def baue_vault(v: Path, fake: str | None) -> None:
|
||||
schreibe(v, "CLAUDE.md", "# Stamm\n")
|
||||
schreibe(v, "AI/Regelwerk/regel.md", "---\nstatus: reference\n---\n")
|
||||
schreibe(v, "AI/Regelwerk/unter/tief.md", "x\n")
|
||||
schreibe(v, ".claude/agents/planer.md", "---\nname: planer\n---\n")
|
||||
schreibe(v, ".claude/skills/demo/SKILL.md", "---\nname: demo\n---\n")
|
||||
schreibe(v, "Inbox/notiz.md", "x\n")
|
||||
schreibe(v, "Code/CLAUDE.md", "x\n")
|
||||
if fake is not None:
|
||||
schreibe(v, ".vault-tools/regelwerk-check/regelwerk_check.py", FAKES[fake])
|
||||
|
||||
|
||||
def vorwaerts(pfad: Path) -> str:
|
||||
return str(pfad).replace("\\", "/")
|
||||
|
||||
|
||||
def laufwerk_getauscht(pfad: Path) -> str:
|
||||
s = str(pfad)
|
||||
if re.match(r"^[A-Za-z]:", s):
|
||||
return s[0].swapcase() + s[1:]
|
||||
return s
|
||||
|
||||
|
||||
def j(daten: object) -> bytes:
|
||||
return json.dumps(daten).encode("utf-8")
|
||||
|
||||
|
||||
def edit(v: Path, datei: object, **extra: object) -> bytes:
|
||||
daten: dict[str, object] = {
|
||||
"session_id": "faelle",
|
||||
"transcript_path": "",
|
||||
"cwd": str(v),
|
||||
"permission_mode": "default",
|
||||
"hook_event_name": "PostToolUse",
|
||||
"tool_name": "Edit",
|
||||
"tool_input": {"file_path": datei},
|
||||
"tool_response": {},
|
||||
}
|
||||
daten.update(extra)
|
||||
return j(daten)
|
||||
|
||||
|
||||
def ohne_cwd(v: Path, datei: object) -> bytes:
|
||||
daten = json.loads(edit(v, datei))
|
||||
del daten["cwd"]
|
||||
return j(daten)
|
||||
|
||||
|
||||
@dataclass
|
||||
class Fall:
|
||||
name: str
|
||||
fake: str | None
|
||||
stdin: Callable[[Path], bytes]
|
||||
erwartung: str
|
||||
enthaelt: list[str] = field(default_factory=list)
|
||||
system_enthaelt: str = ""
|
||||
kopie: bool = False
|
||||
max_dauer: float = 10.0
|
||||
|
||||
|
||||
BEFUND = ["[klein] AI/Regelwerk/regel.md"]
|
||||
BEFUND_SYSTEM = "Exit 1, 1 Befundzeile(n)"
|
||||
|
||||
|
||||
def still(name: str, stdin: Callable[[Path], bytes]) -> Fall:
|
||||
return Fall(name, "befund", stdin, "still-ohne-lauf")
|
||||
|
||||
|
||||
def meldet(name: str, stdin: Callable[[Path], bytes], kopie: bool = False) -> Fall:
|
||||
return Fall(name, "befund", stdin, "meldung", BEFUND, BEFUND_SYSTEM, kopie)
|
||||
|
||||
|
||||
GRUNDLINIE = [
|
||||
Fall("GRUNDLINIE | gruener Check bleibt still und lief", "gruen",
|
||||
lambda v: edit(v, str(v / "CLAUDE.md")), "still-nach-lauf"),
|
||||
Fall("GRUNDLINIE | Befund wird gemeldet", "befund",
|
||||
lambda v: edit(v, str(v / "CLAUDE.md")), "meldung", BEFUND, BEFUND_SYSTEM),
|
||||
]
|
||||
|
||||
FAELLE = [
|
||||
still("leer | stdin leer", lambda v: b""),
|
||||
still("leer | stdin nur Leerraum", lambda v: b" \r\n"),
|
||||
still("falscher Typ| stdin kein JSON", lambda v: b"kein json"),
|
||||
still("kodierung | stdin ungueltiges UTF-8", lambda v: b"\xff\xfe\x00"),
|
||||
still("falscher Typ| stdin ist eine Liste",
|
||||
lambda v: j([json.loads(edit(v, str(v / "CLAUDE.md")))])),
|
||||
still("falscher Typ| stdin ist eine Zahl", lambda v: b"5"),
|
||||
still("fehlt | tool_input fehlt", lambda v: j({"cwd": str(v), "tool_name": "Edit"})),
|
||||
still("null | tool_input ist null", lambda v: edit(v, "x", tool_input=None)),
|
||||
still("falscher Typ| tool_input ist eine Liste",
|
||||
lambda v: edit(v, "x", tool_input=[str(v / "CLAUDE.md")])),
|
||||
still("fehlt | file_path fehlt", lambda v: edit(v, "x", tool_input={"content": "a"})),
|
||||
still("null | file_path ist null", lambda v: edit(v, None)),
|
||||
still("falscher Typ| file_path ist eine Zahl", lambda v: edit(v, 5)),
|
||||
still("falscher Typ| file_path ist eine Liste", lambda v: edit(v, [str(v / "CLAUDE.md")])),
|
||||
still("leer | file_path ist leer", lambda v: edit(v, "")),
|
||||
still("wertebereich| file_path mit Nullzeichen",
|
||||
lambda v: edit(v, str(v / "CLAUDE.md") + "\u0000")),
|
||||
still("zusaetzlich | Pfad ausserhalb des Vaults",
|
||||
lambda v: edit(v, str(v.parent / "anderswo" / "CLAUDE.md"))),
|
||||
still("verweis | Nachbarordner mit gleichem Namensanfang",
|
||||
lambda v: edit(v, str(v) + "-kopie\\CLAUDE.md")),
|
||||
still("verweis | Datei neben dem Vault mit gleichem Namensanfang",
|
||||
lambda v: edit(v, str(v) + "-CLAUDE.md")),
|
||||
still("zusaetzlich | im Vault, ausserhalb des Geltungsbereichs",
|
||||
lambda v: edit(v, str(v / "Inbox" / "notiz.md"))),
|
||||
still("zusaetzlich | CLAUDE.md in einem Unterordner",
|
||||
lambda v: edit(v, str(v / "Code" / "CLAUDE.md"))),
|
||||
still("verweis | Ordnername mit gleichem Anfang",
|
||||
lambda v: edit(v, str(v / "AI" / "RegelwerkAlt" / "x.md"))),
|
||||
still("zusaetzlich | settings.json unter .claude",
|
||||
lambda v: edit(v, str(v / ".claude" / "settings.json"))),
|
||||
still("wertebereich| der Regelwerk-Ordner selbst",
|
||||
lambda v: edit(v, str(v / "AI" / "Regelwerk"))),
|
||||
still("verweis | .. fuehrt aus dem Regelwerk hinaus",
|
||||
lambda v: edit(v, vorwaerts(v) + "/AI/Regelwerk/../../Inbox/notiz.md")),
|
||||
still("verweis | .. fuehrt aus dem Vault hinaus",
|
||||
lambda v: edit(v, vorwaerts(v) + "/../fremd/AI/Regelwerk/x.md")),
|
||||
Fall("gruen | CLAUDE.md, Check ohne Befund", "gruen",
|
||||
lambda v: edit(v, str(v / "CLAUDE.md")), "still-nach-lauf"),
|
||||
Fall("gruen | Skill, Check ohne Befund", "gruen",
|
||||
lambda v: edit(v, str(v / ".claude" / "skills" / "demo" / "SKILL.md")), "still-nach-lauf"),
|
||||
meldet("pfad | Schraegstriche vorwaerts",
|
||||
lambda v: edit(v, vorwaerts(v) + "/CLAUDE.md")),
|
||||
meldet("pfad | Backslashes",
|
||||
lambda v: edit(v, str(v) + "\\AI\\Regelwerk\\regel.md")),
|
||||
meldet("pfad | Laufwerksbuchstabe in anderer Schreibung",
|
||||
lambda v: edit(v, laufwerk_getauscht(v / "AI" / "Regelwerk" / "regel.md"))),
|
||||
meldet("pfad | ganzer Pfad in Grossbuchstaben",
|
||||
lambda v: edit(v, str(v / "AI" / "Regelwerk" / "regel.md").upper())),
|
||||
meldet("pfad | Unterordner im Regelwerk",
|
||||
lambda v: edit(v, str(v / "AI" / "Regelwerk" / "unter" / "tief.md"))),
|
||||
meldet("pfad | Agentendatei",
|
||||
lambda v: edit(v, str(v / ".claude" / "agents" / "planer.md"))),
|
||||
meldet("pfad | Skilldatei",
|
||||
lambda v: edit(v, str(v / ".claude" / "skills" / "demo" / "SKILL.md"))),
|
||||
meldet("verweis | neue Datei, die es noch nicht gibt",
|
||||
lambda v: edit(v, str(v / "AI" / "Regelwerk" / "neu.md"))),
|
||||
meldet("pfad | relativer Pfad", lambda v: edit(v, "AI/Regelwerk/regel.md")),
|
||||
meldet("verweis | .. fuehrt ins Regelwerk hinein",
|
||||
lambda v: edit(v, vorwaerts(v) + "/Inbox/../AI/Regelwerk/regel.md")),
|
||||
meldet("zusaetzlich | Write statt Edit",
|
||||
lambda v: edit(v, str(v / "CLAUDE.md"), tool_name="Write")),
|
||||
meldet("zusaetzlich | MultiEdit",
|
||||
lambda v: edit(v, str(v / "CLAUDE.md"), tool_name="MultiEdit")),
|
||||
meldet("kodierung | ungueltiges UTF-8 im Pfad",
|
||||
lambda v: edit(v, "PLATZHALTER").replace(
|
||||
b"PLATZHALTER", vorwaerts(v).encode("utf-8") + b"/AI/Regelwerk/regel\xff.md")),
|
||||
meldet("pfad | cwd mit abschliessendem Backslash",
|
||||
lambda v: edit(v, str(v / "CLAUDE.md"), cwd=str(v) + "\\")),
|
||||
meldet("fehlt | cwd fehlt, Rueckfall auf den Skriptpfad",
|
||||
lambda v: ohne_cwd(v, str(v / "CLAUDE.md")), kopie=True),
|
||||
meldet("null | cwd ist null, Rueckfall",
|
||||
lambda v: edit(v, str(v / "CLAUDE.md"), cwd=None), kopie=True),
|
||||
meldet("falscher Typ| cwd ist eine Zahl, Rueckfall",
|
||||
lambda v: edit(v, str(v / "CLAUDE.md"), cwd=5), kopie=True),
|
||||
meldet("verweis | cwd existiert nicht, Rueckfall",
|
||||
lambda v: edit(v, str(v / "CLAUDE.md"), cwd=str(v / "gibtsnicht")), kopie=True),
|
||||
Fall("check | Exit 2, Abbruch im Check", "abbruch",
|
||||
lambda v: edit(v, str(v / "CLAUDE.md")), "meldung",
|
||||
["Abbruch: KeyError"], "Exit 2, 1 Befundzeile(n)"),
|
||||
Fall("check | Exit 0 mit ACHTUNG", "achtung",
|
||||
lambda v: edit(v, str(v / "CLAUDE.md")), "meldung",
|
||||
["ACHTUNG: nur 5 von 6"], "Exit 0, 0 Befundzeile(n)"),
|
||||
Fall("check | Exit 0 ohne Schlusszeile", "stumm",
|
||||
lambda v: edit(v, str(v / "CLAUDE.md")), "meldung",
|
||||
["(keine Ausgabe)"], "Exit 0, 0 Befundzeile(n)"),
|
||||
Fall("check | Check stuerzt ab", "absturz",
|
||||
lambda v: edit(v, str(v / "CLAUDE.md")), "meldung",
|
||||
["--- stderr ---", "RuntimeError: Absturz im Check"], "Exit 1, 0 Befundzeile(n)"),
|
||||
Fall("umgebung | regelwerk_check.py fehlt", None,
|
||||
lambda v: edit(v, str(v / "CLAUDE.md")), "meldung",
|
||||
["regelwerk_check.py fehlt"], "Regelwerk-Check fehlt"),
|
||||
Fall("umgebung | regelwerk_check.py haengt", "haengt",
|
||||
lambda v: edit(v, str(v / "CLAUDE.md")), "meldung",
|
||||
["länger als 20 s"], "nach 20 s abgebrochen", max_dauer=28.0),
|
||||
Fall("wertebereich| sehr lange Ausgabe wird gekuerzt", "viel",
|
||||
lambda v: edit(v, str(v / "CLAUDE.md")), "meldung",
|
||||
["weitere Zeilen gekürzt"], "Exit 1, 200 Befundzeile(n)"),
|
||||
]
|
||||
|
||||
|
||||
def fahre(skript: Path, eingabe: bytes, arbeitsordner: Path) -> tuple[int, str, str, float]:
|
||||
start = time.perf_counter()
|
||||
lauf = subprocess.run(
|
||||
[sys.executable, str(skript)],
|
||||
input=eingabe,
|
||||
capture_output=True,
|
||||
cwd=str(arbeitsordner),
|
||||
timeout=90,
|
||||
)
|
||||
dauer = time.perf_counter() - start
|
||||
return (
|
||||
lauf.returncode,
|
||||
lauf.stdout.decode("utf-8", errors="replace"),
|
||||
lauf.stderr.decode("utf-8", errors="replace"),
|
||||
dauer,
|
||||
)
|
||||
|
||||
|
||||
def pruefe_aufruf(aufruf: Path, v: Path) -> list[str]:
|
||||
if not aufruf.exists():
|
||||
return ["Check wurde nicht gestartet"]
|
||||
daten = json.loads(aufruf.read_text(encoding="utf-8"))
|
||||
probleme: list[str] = []
|
||||
if daten.get("argv") != ["--vault", str(v)]:
|
||||
probleme.append(f"Aufruf-Argumente {daten.get('argv')!r} statt {['--vault', str(v)]!r}")
|
||||
if os.path.normcase(str(daten.get("exe"))) != os.path.normcase(sys.executable):
|
||||
probleme.append(f"Interpreter {daten.get('exe')!r} statt {sys.executable!r}")
|
||||
return probleme
|
||||
|
||||
|
||||
def pruefe_meldung(fall: Fall, out: str) -> list[str]:
|
||||
roh = out.strip()
|
||||
if not (roh.startswith("{") and roh.endswith("}")):
|
||||
return [f"stdout ist kein JSON-Objekt: {out[:300]!r}"]
|
||||
try:
|
||||
daten = json.loads(roh)
|
||||
except ValueError as exc:
|
||||
return [f"stdout nicht parsebar: {exc}"]
|
||||
if not isinstance(daten, dict) or set(daten) != {"hookSpecificOutput", "systemMessage"}:
|
||||
art = sorted(daten) if isinstance(daten, dict) else type(daten).__name__
|
||||
return [f"falsche Schluessel: {art}"]
|
||||
innen = daten["hookSpecificOutput"]
|
||||
system = daten["systemMessage"]
|
||||
if not isinstance(innen, dict) or innen.get("hookEventName") != "PostToolUse":
|
||||
return ["hookSpecificOutput.hookEventName ist nicht PostToolUse"]
|
||||
kontext = innen.get("additionalContext")
|
||||
if not isinstance(kontext, str) or not kontext:
|
||||
return ["additionalContext fehlt oder ist leer"]
|
||||
probleme: list[str] = []
|
||||
if not isinstance(system, str) or not system or "\n" in system:
|
||||
probleme.append(f"systemMessage ist keine einzelne Zeile: {system!r}")
|
||||
elif fall.system_enthaelt not in system:
|
||||
probleme.append(f"systemMessage ohne {fall.system_enthaelt!r}: {system!r}")
|
||||
probleme += [f"Kontext ohne {t!r}" for t in fall.enthaelt if t not in kontext]
|
||||
if len(kontext) > MAX_KONTEXT:
|
||||
probleme.append(f"Kontext hat {len(kontext)} Zeichen, Grenze {MAX_KONTEXT}")
|
||||
if probleme:
|
||||
probleme.append("Kontext war:\n" + kontext)
|
||||
return probleme
|
||||
|
||||
|
||||
def pruefe(fall: Fall, v: Path, code: int, out: str, dauer: float) -> list[str]:
|
||||
probleme: list[str] = []
|
||||
if code != 0:
|
||||
probleme.append(f"Exit {code} statt 0")
|
||||
if dauer > fall.max_dauer:
|
||||
probleme.append(f"Laufzeit {dauer:.1f} s ueber {fall.max_dauer:.0f} s")
|
||||
aufruf = v / "aufruf.json"
|
||||
if fall.erwartung == "still-ohne-lauf":
|
||||
if out != "":
|
||||
probleme.append(f"stdout nicht leer: {out[:300]!r}")
|
||||
if aufruf.exists():
|
||||
probleme.append("Check wurde gestartet, obwohl er nicht haette laufen duerfen")
|
||||
elif fall.erwartung == "still-nach-lauf":
|
||||
if out != "":
|
||||
probleme.append(f"stdout nicht leer: {out[:300]!r}")
|
||||
probleme += pruefe_aufruf(aufruf, v)
|
||||
else:
|
||||
probleme += pruefe_meldung(fall, out)
|
||||
if fall.fake is not None:
|
||||
probleme += pruefe_aufruf(aufruf, v)
|
||||
return probleme
|
||||
|
||||
|
||||
def lauf_fall(fall: Fall, arbeit: Path) -> tuple[bool, str]:
|
||||
v = arbeit / "fall"
|
||||
if v.exists():
|
||||
shutil.rmtree(v)
|
||||
baue_vault(v, fall.fake)
|
||||
skript = HOOK
|
||||
if fall.kopie:
|
||||
ziel = v / ".vault-tools" / "session-hooks"
|
||||
ziel.mkdir(parents=True, exist_ok=True)
|
||||
skript = ziel / HOOK.name
|
||||
shutil.copy2(HOOK, skript)
|
||||
code, out, err, dauer = fahre(skript, fall.stdin(v), arbeit)
|
||||
kopf = f"exit={code} {dauer:.2f}s"
|
||||
probleme = pruefe(fall, v, code, out, dauer)
|
||||
if not probleme:
|
||||
return True, kopf
|
||||
text = kopf + "\n" + "\n".join(" " + p for p in probleme)
|
||||
if err.strip():
|
||||
text += "\n stderr: " + err.strip()[:800]
|
||||
return False, text
|
||||
|
||||
|
||||
def ip_hook_wirft(arbeit: Path) -> tuple[bool, str]:
|
||||
"""Fehlerpfad im Hook selbst, von aussen nicht ausloesbar: im Prozess."""
|
||||
v = arbeit / "fall"
|
||||
if v.exists():
|
||||
shutil.rmtree(v)
|
||||
baue_vault(v, "befund")
|
||||
spec = importlib.util.spec_from_file_location("hook_wirft", HOOK)
|
||||
modul = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(modul)
|
||||
|
||||
def wirft(wurzel: Path) -> None:
|
||||
raise RuntimeError("Testfehler")
|
||||
|
||||
modul.fuehre_check = wirft
|
||||
puffer = io.StringIO()
|
||||
with contextlib.redirect_stdout(puffer):
|
||||
code = modul.main(edit(v, str(v / "CLAUDE.md")))
|
||||
erwartet = Fall("", "befund", lambda _: b"", "meldung",
|
||||
["RuntimeError: Testfehler"], "regelwerk_nach_edit.py: RuntimeError")
|
||||
probleme = pruefe_meldung(erwartet, puffer.getvalue())
|
||||
if code != 0:
|
||||
probleme.append(f"main gab {code} zurueck statt 0")
|
||||
return not probleme, "\n".join(" " + p for p in probleme)
|
||||
|
||||
|
||||
def echter_lauf() -> int:
|
||||
vault = HIER.parent.parent
|
||||
code, out, err, dauer = fahre(HOOK, edit(vault, str(vault / "CLAUDE.md")), HIER)
|
||||
print(f"exit={code} Laufzeit {dauer:.2f} s")
|
||||
if out.strip():
|
||||
try:
|
||||
print(json.dumps(json.loads(out), ensure_ascii=False, indent=2))
|
||||
except ValueError:
|
||||
print("FORMFEHLER, stdout ist kein JSON:")
|
||||
print(out)
|
||||
return 1
|
||||
else:
|
||||
print("(keine Ausgabe: Regelwerk-Check ohne Befund)")
|
||||
if err.strip():
|
||||
print("--- stderr ---")
|
||||
print(err)
|
||||
return 0 if code == 0 else 1
|
||||
|
||||
|
||||
def main() -> int:
|
||||
if not HOOK.is_file():
|
||||
print(f"ABBRUCH: {HOOK} fehlt")
|
||||
return 2
|
||||
if "--echt" in sys.argv[1:]:
|
||||
return echter_lauf()
|
||||
|
||||
arbeit = Path(tempfile.mkdtemp(prefix="nach-edit-faelle-")).resolve()
|
||||
try:
|
||||
print("=" * 78)
|
||||
print("GRUNDLINIE")
|
||||
print("=" * 78)
|
||||
for fall in GRUNDLINIE:
|
||||
ok, text = lauf_fall(fall, arbeit)
|
||||
print(f"{'BESTANDEN' if ok else 'FEHLER':10} {fall.name} {text}")
|
||||
if not ok:
|
||||
print("ABBRUCH: Die Grundlinie ist nicht gruen. Der Rest misst nichts.")
|
||||
return 2
|
||||
|
||||
print()
|
||||
print("=" * 78)
|
||||
print("ANGRIFFSLISTE")
|
||||
print("=" * 78)
|
||||
bestanden = 0
|
||||
gesamt = 0
|
||||
for fall in FAELLE:
|
||||
gesamt += 1
|
||||
try:
|
||||
ok, text = lauf_fall(fall, arbeit)
|
||||
except Exception as exc:
|
||||
ok, text = False, f"ABSTURZ im Treiber: {type(exc).__name__}: {exc}"
|
||||
bestanden += int(ok)
|
||||
print(f"{'BESTANDEN' if ok else 'FEHLER':10} {fall.name} {text}")
|
||||
|
||||
gesamt += 1
|
||||
try:
|
||||
ok, text = ip_hook_wirft(arbeit)
|
||||
except Exception as exc:
|
||||
ok, text = False, f"ABSTURZ im Treiber: {type(exc).__name__}: {exc}"
|
||||
bestanden += int(ok)
|
||||
print(f"{'BESTANDEN' if ok else 'FEHLER':10} absturz | Hook selbst wirft (im Prozess) {text}")
|
||||
|
||||
print()
|
||||
print(f"{bestanden} von {gesamt} Faellen bestanden.")
|
||||
return 0 if bestanden == gesamt else 1
|
||||
finally:
|
||||
shutil.rmtree(arbeit, ignore_errors=True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
sys.stdout.reconfigure(encoding="utf-8")
|
||||
except AttributeError:
|
||||
pass
|
||||
raise SystemExit(main())
|
||||
```
|
||||
|
||||
## Schritt 2 — Fehlschlag beobachten
|
||||
|
||||
```powershell
|
||||
Set-Location D:\Claude
|
||||
$env:PYTHONIOENCODING = "utf-8"
|
||||
python -m py_compile .vault-tools\session-hooks\faelle_nach_edit.py; "exit=$LASTEXITCODE"
|
||||
python .vault-tools\session-hooks\faelle_nach_edit.py; "exit=$LASTEXITCODE"
|
||||
```
|
||||
|
||||
Erwartet vor der Umsetzung:
|
||||
|
||||
```text
|
||||
exit=0
|
||||
ABBRUCH: D:\Claude\.vault-tools\session-hooks\regelwerk_nach_edit.py fehlt
|
||||
exit=2
|
||||
```
|
||||
|
||||
## Schritt 3 — Umsetzung: `regelwerk_nach_edit.py`
|
||||
|
||||
Datei `D:\Claude\.vault-tools\session-hooks\regelwerk_nach_edit.py` mit genau diesem Inhalt anlegen:
|
||||
|
||||
```python
|
||||
"""PostToolUse-Hook: startet den Regelwerk-Check nach einer Änderung an der Anweisungsschicht.
|
||||
|
||||
Ersteller: Pascal Michel
|
||||
Erstellt: 2026-09-13
|
||||
Entwurf: ENTWURF.md im selben Ordner (Entscheidungsliste, Herkunft, Angriffsliste)
|
||||
|
||||
GELTUNGSBEREICH
|
||||
Ausgelöst wird nur, wenn tool_input.file_path innerhalb der Vault-Wurzel auf
|
||||
CLAUDE.md, AI/Regelwerk/**, .claude/agents/** oder .claude/skills/** zeigt.
|
||||
Die Wurzel ist das Feld cwd der Hook-Eingabe; ohne brauchbares cwd der Ordner
|
||||
zwei Ebenen über dieser Datei. Verglichen wird ohne Rücksicht auf Groß- und
|
||||
Kleinschreibung und auf die Richtung der Schrägstriche, nach Auflösen von
|
||||
"." und "..". Ein relativer Pfad gilt relativ zur Wurzel.
|
||||
|
||||
Still bleibt der Hook nur in zwei Lagen: Die Eingabe taugt nicht (leer, kein
|
||||
JSON-Objekt, kein file_path als Zeichenkette), oder der Pfad liegt außerhalb
|
||||
des Geltungsbereichs. Ein Check ohne Befund bleibt ebenfalls still. Eine
|
||||
fehlende, abstürzende oder hängende Prüfung ist ein Befund.
|
||||
|
||||
BENANNTE LÜCKEN
|
||||
* Ob tool_name Edit, Write oder MultiEdit ist, prüft der Matcher in
|
||||
settings.json, nicht dieses Skript.
|
||||
* Symlinks und Junctions werden nicht aufgelöst. Ein Link von außen auf eine
|
||||
Regelwerk-Datei löst nichts aus.
|
||||
* Still nach Exit 0 bleibt der Hook nur, solange regelwerk_check.py seine
|
||||
Schlusszeile "Keine der N gelaufenen Prüfungen hat angeschlagen." behält.
|
||||
Ändert sich der Wortlaut, meldet der Hook jeden Lauf, also laut statt still.
|
||||
* Die Längengrenze für additionalContext ist in der Hook-Doku nicht belegt.
|
||||
40 Zeilen und 4000 Zeichen sind eine Projektvorgabe.
|
||||
|
||||
DIESER HOOK IST EIN FANGNETZ, KEIN NACHWEIS. Er reicht nur weiter, was
|
||||
regelwerk_check.py sieht, und das sind formale Widersprüche, keine inhaltlichen.
|
||||
|
||||
Exit ist immer 0: PostToolUse ignoriert Exit 2, und JSON auf stdout wird nur
|
||||
bei Exit 0 gelesen.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
SKRIPT = Path(__file__).resolve()
|
||||
CHECK_REL = Path(".vault-tools") / "regelwerk-check" / "regelwerk_check.py"
|
||||
# Unter dem timeout von 30 s, den der settings.json-Eintrag setzen soll.
|
||||
TIMEOUT_S = 20
|
||||
MAX_ZEILEN = 40
|
||||
MAX_ZEICHEN = 4000
|
||||
|
||||
# Kleinbuchstaben und Schrägstrich vorwärts, verglichen gegen normiere().
|
||||
GELTUNG_DATEIEN = ("claude.md",)
|
||||
GELTUNG_ORDNER = ("ai/regelwerk/", ".claude/agents/", ".claude/skills/")
|
||||
|
||||
# Schlusszeile von regelwerk_check.py ohne Befund (dort Zeile 370).
|
||||
_GRUEN = re.compile(r"^Keine der \d+ gelaufenen Prüfungen hat angeschlagen\.$")
|
||||
|
||||
|
||||
def lies_stdin() -> bytes:
|
||||
if sys.stdin is None or sys.stdin.isatty():
|
||||
return b""
|
||||
return sys.stdin.buffer.read()
|
||||
|
||||
|
||||
def lies_eingabe(roh: bytes) -> object:
|
||||
# errors=replace: ein kaputtes Byte im Pfad soll laut enden, nicht still.
|
||||
text = roh.decode("utf-8-sig", errors="replace")
|
||||
if not text.strip():
|
||||
return None
|
||||
try:
|
||||
return json.loads(text)
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
|
||||
def bestimme_wurzel(daten: dict) -> Path:
|
||||
cwd = daten.get("cwd")
|
||||
if isinstance(cwd, str) and cwd.strip() and os.path.isdir(cwd):
|
||||
return Path(cwd)
|
||||
return SKRIPT.parents[2]
|
||||
|
||||
|
||||
def normiere(pfad: str) -> str:
|
||||
return os.path.normpath(os.path.abspath(pfad)).replace("\\", "/").lower().rstrip("/")
|
||||
|
||||
|
||||
def im_geltungsbereich(wurzel: Path, datei: str) -> bool:
|
||||
kandidat = datei.replace("\\", "/")
|
||||
try:
|
||||
# Relativ zur Wurzel auflösen, nie zum Arbeitsverzeichnis des Prozesses.
|
||||
if not os.path.isabs(kandidat):
|
||||
kandidat = os.path.join(str(wurzel), kandidat)
|
||||
pfad = normiere(kandidat)
|
||||
basis = normiere(str(wurzel))
|
||||
except (ValueError, OSError):
|
||||
return False
|
||||
# Mit Trenner vergleichen, sonst träfe "<wurzel>-CLAUDE.md" neben dem Vault.
|
||||
if not pfad.startswith(basis + "/"):
|
||||
return False
|
||||
rel = pfad[len(basis) + 1:]
|
||||
return rel in GELTUNG_DATEIEN or rel.startswith(GELTUNG_ORDNER)
|
||||
|
||||
|
||||
def dekodiere(roh: bytes | str | None) -> str:
|
||||
if roh is None:
|
||||
return ""
|
||||
if isinstance(roh, str):
|
||||
return roh
|
||||
return roh.decode("utf-8", errors="replace")
|
||||
|
||||
|
||||
def fuehre_check(wurzel: Path) -> tuple[str, str] | None:
|
||||
"""None ohne Befund, sonst (systemMessage, Rumpf für additionalContext)."""
|
||||
check = wurzel / CHECK_REL
|
||||
if not check.is_file():
|
||||
return (
|
||||
f"Regelwerk-Check fehlt: {check}",
|
||||
f"{check} fehlt. Die Prüfung ist nicht gelaufen; das ist ein Befund, keine Entwarnung.",
|
||||
)
|
||||
umgebung = dict(os.environ, PYTHONIOENCODING="utf-8")
|
||||
try:
|
||||
lauf = subprocess.run(
|
||||
[sys.executable, str(check), "--vault", str(wurzel)],
|
||||
cwd=str(wurzel),
|
||||
env=umgebung,
|
||||
stdin=subprocess.DEVNULL,
|
||||
capture_output=True,
|
||||
timeout=TIMEOUT_S,
|
||||
)
|
||||
except subprocess.TimeoutExpired as abgelaufen:
|
||||
teil = (dekodiere(abgelaufen.stdout) + dekodiere(abgelaufen.stderr)).strip()
|
||||
return (
|
||||
f"Regelwerk-Check nach {TIMEOUT_S} s abgebrochen",
|
||||
f"regelwerk_check.py lief länger als {TIMEOUT_S} s und wurde beendet. "
|
||||
f"Bis dahin ausgegeben:\n{teil or '(nichts)'}",
|
||||
)
|
||||
except OSError as fehler:
|
||||
return (
|
||||
f"Regelwerk-Check nicht startbar: {type(fehler).__name__}",
|
||||
f"regelwerk_check.py ließ sich nicht starten: {type(fehler).__name__}: {fehler}",
|
||||
)
|
||||
|
||||
ausgabe = dekodiere(lauf.stdout)
|
||||
fehlertext = dekodiere(lauf.stderr).strip()
|
||||
zeilen = [z.strip() for z in ausgabe.splitlines() if z.strip()]
|
||||
gruen = any(_GRUEN.match(z) for z in zeilen)
|
||||
achtung = any(z.startswith("ACHTUNG") for z in zeilen)
|
||||
if lauf.returncode == 0 and gruen and not achtung:
|
||||
return None
|
||||
|
||||
befundzeilen = sum(1 for z in zeilen if z.startswith("["))
|
||||
rumpf = ausgabe.strip()
|
||||
if fehlertext:
|
||||
rumpf += "\n--- stderr ---\n" + fehlertext
|
||||
return (
|
||||
f"Regelwerk-Check: Exit {lauf.returncode}, {befundzeilen} Befundzeile(n)",
|
||||
rumpf or "(keine Ausgabe)",
|
||||
)
|
||||
|
||||
|
||||
def kuerze(text: str) -> str:
|
||||
zeilen = text.splitlines()
|
||||
rest = len(zeilen) - MAX_ZEILEN
|
||||
if rest > 0:
|
||||
zeilen = zeilen[:MAX_ZEILEN] + [f"[... {rest} weitere Zeilen gekürzt]"]
|
||||
ergebnis = "\n".join(zeilen)
|
||||
if len(ergebnis) > MAX_ZEICHEN:
|
||||
ergebnis = ergebnis[:MAX_ZEICHEN] + f"\n[... gekürzt auf {MAX_ZEICHEN} Zeichen]"
|
||||
return ergebnis
|
||||
|
||||
|
||||
def melde(system: str, kontext: str) -> None:
|
||||
ausgabe = {
|
||||
"hookSpecificOutput": {
|
||||
"hookEventName": "PostToolUse",
|
||||
"additionalContext": kontext,
|
||||
},
|
||||
"systemMessage": " ".join(system.split()),
|
||||
}
|
||||
sys.stdout.write(json.dumps(ausgabe, ensure_ascii=True))
|
||||
sys.stdout.flush()
|
||||
|
||||
|
||||
def main(roh: bytes | None = None) -> int:
|
||||
try:
|
||||
daten = lies_eingabe(lies_stdin() if roh is None else roh)
|
||||
if not isinstance(daten, dict):
|
||||
return 0
|
||||
eingabe = daten.get("tool_input")
|
||||
if not isinstance(eingabe, dict):
|
||||
return 0
|
||||
datei = eingabe.get("file_path")
|
||||
if not isinstance(datei, str) or not datei.strip():
|
||||
return 0
|
||||
wurzel = bestimme_wurzel(daten)
|
||||
if not im_geltungsbereich(wurzel, datei):
|
||||
return 0
|
||||
ergebnis = fuehre_check(wurzel)
|
||||
if ergebnis is None:
|
||||
return 0
|
||||
system, rumpf = ergebnis
|
||||
melde(system, kuerze(f"Regelwerk-Check nach Änderung an {datei} (Vault: {wurzel}):\n{rumpf}"))
|
||||
except Exception as fehler:
|
||||
melde(
|
||||
f"regelwerk_nach_edit.py: {type(fehler).__name__}",
|
||||
f"regelwerk_nach_edit.py ist abgebrochen: {type(fehler).__name__}: {fehler}. "
|
||||
"Der Regelwerk-Check ist damit nicht gelaufen.",
|
||||
)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
main()
|
||||
except Exception:
|
||||
# Letzte Schranke: Exit 0 ist Pflicht, sonst liest Claude Code stdout nicht.
|
||||
pass
|
||||
sys.exit(0)
|
||||
```
|
||||
|
||||
## Verifikation
|
||||
|
||||
```powershell
|
||||
Set-Location D:\Claude
|
||||
$env:PYTHONIOENCODING = "utf-8"
|
||||
python -m py_compile .vault-tools\session-hooks\regelwerk_nach_edit.py .vault-tools\session-hooks\faelle_nach_edit.py; "exit=$LASTEXITCODE"
|
||||
python .vault-tools\session-hooks\faelle_nach_edit.py; "exit=$LASTEXITCODE"
|
||||
python .vault-tools\session-hooks\faelle_nach_edit.py --echt; "exit=$LASTEXITCODE"
|
||||
```
|
||||
|
||||
Erwartet:
|
||||
|
||||
1. `py_compile`: keine Ausgabe, `exit=0`.
|
||||
2. Treiber: Beide Grundlinien-Zeilen `BESTANDEN`, keine Zeile beginnt mit `FEHLER` oder enthält `ABSTURZ`, Schlusszeile `N von N Faellen bestanden.` mit derselben Zahl vorn und hinten, `exit=0`. Der Hänge-Fall allein dauert rund 20 Sekunden.
|
||||
3. `--echt`: erste Ausgabezeile `exit=0 Laufzeit … s`, danach ein JSON-Objekt, am Ende `exit=0`. Solange `AI/Regelwerk/pruefwerkzeuge.md` auf `status: pending-review` steht, enthält `systemMessage` den Text `Exit 1, 1 Befundzeile(n)` und `additionalContext` die Zeile `[klein] AI/Regelwerk/pruefwerkzeuge.md — status: pending-review — Freigabe steht aus`. Weicht das ab, die Ausgabe wörtlich melden, nicht bewerten.
|
||||
|
||||
## Bericht an den Koordinator
|
||||
|
||||
- Ausgabe von Schritt 2 wörtlich.
|
||||
- Vollständige Ausgabe des Treibers und des Laufs mit `--echt`.
|
||||
- Jede Abweichung vom Plantext der beiden Dateien, auch Leerzeichen. Es darf keine geben.
|
||||
|
||||
## Nachtrag Runde 2
|
||||
|
||||
Nachbesserung nach der ersten Prüfrunde (Befunde siehe `ENTWURF.md`, Abschnitt „Nachbesserung Runde 2"). Weicht vom Plantext oben ab:
|
||||
|
||||
- **`bestimme_wurzel()`** nimmt keinen Parameter mehr und liest weder `CLAUDE_PROJECT_DIR` noch `cwd`. Sie gibt ausschließlich `SKRIPT.parents[2]` zurück. Grund: `cwd` folgt einem `cd` im Bash-Werkzeug und zeigt nach einem Wechsel in einen Projektordner nicht mehr auf den Vault (Prüferbefund Runde 2, Koordinatorentscheidung — der Hook ist ohnehin nur für den einen Vault eingetragen, in dem er liegt).
|
||||
- Der Kopfkommentar-Satz „PostToolUse ignoriert Exit 2" ist korrigiert: Exit 2 zeigt stderr laut Hilfetext von `claude.exe 2.1.263` sofort dem Modell. Die Umsetzung bleibt bei Exit 0 plus JSON.
|
||||
- **`faelle_nach_edit.py`** kopiert den Hook seither für jeden Fall in den Wegwerf-Vault (das Feld `kopie` auf `Fall` entfällt, weil es jetzt immer geschieht). Die Fälle `mit_projekt_dir`/`cd_in_anderes_projekt` sind entfallen, dafür neu: `cwd` zeigt auf einen anderen Ordner (Ergebnis bleibt der Skript-Vault), ein zweiter Umgebungsfall mit echtem Nicht-ASCII-Pfad ohne `PYTHONIOENCODING`. `ip_hook_wirft()` lädt jetzt ebenfalls eine Kopie des Hooks aus dem Wegwerf-Vault, sonst fand `main()` keinen Geltungsbereich für den Testpfad und der Fall blieb unbemerkt grün mit leerem `stdout`.
|
||||
- Details, Herkunft und Mutationsbelege stehen in `ENTWURF.md`, nicht hier verdoppelt.
|
||||
+301
@@ -0,0 +1,301 @@
|
||||
---
|
||||
type: plan-task
|
||||
title: "Task 03 — Linkcheck: Schließzaun nach CommonMark"
|
||||
status: draft
|
||||
summary: "strip_code schließt einen Fenced-Codeblock nur noch mit gleichem Zeichen, mindestens gleicher Länge und ohne Info-String; sechs neue Regressionsfälle, Herkunft im Entwurf."
|
||||
tags: [plan-task, vault-tools]
|
||||
created: 2026-09-13
|
||||
updated: 2026-09-13
|
||||
modell: haiku
|
||||
modell_begruendung: "Vier Ersetzungen mit wörtlichem Alt- und Neutext in drei namentlich genannten Dateien, erwartete Ausgabe vor und nach der Umsetzung steht fest; kein Ermessen."
|
||||
---
|
||||
|
||||
# Task 03 — Linkcheck: Schließzaun nach CommonMark
|
||||
|
||||
Globale Vorgaben: `00-uebersicht.md` im selben Ordner, Abschnitt „Globale Vorgaben". Sie gelten hier vollständig und werden nicht wiederholt.
|
||||
|
||||
## Zieldateien
|
||||
|
||||
- `D:\Claude\.vault-tools\linkcheck\faelle.py` (ändern)
|
||||
- `D:\Claude\.vault-tools\linkcheck\linkcheck.py` (ändern)
|
||||
- `D:\Claude\.vault-tools\linkcheck\ENTWURF-codeblock-erkennung.md` (ändern)
|
||||
|
||||
## Verbote
|
||||
|
||||
- Keine andere Datei anfassen, auch nicht `regelwerk_check.py`, obwohl dessen Kommentar die Zahl der Fälle nennt.
|
||||
- Keine Änderung an der Öffnerregel `_FENCE_RE`, an `_INLINE_CODE_RE`, an `wikilinks()` oder an `lauf()`.
|
||||
- Jede Ersetzung positionsgenau über den angegebenen Alttext, kein globales Suchen und Ersetzen.
|
||||
|
||||
## Schritt 1 — Test zuerst: sechs Fälle in `faelle.py`
|
||||
|
||||
Ersetze in `D:\Claude\.vault-tools\linkcheck\faelle.py` diesen Alttext:
|
||||
|
||||
```python
|
||||
(16, "escaptes Pipe am Zeilenende ohne Alias",
|
||||
r"Siehe [[Ziel\|]] dort.", ["Ziel"]),
|
||||
]
|
||||
```
|
||||
|
||||
durch diesen Neutext:
|
||||
|
||||
````python
|
||||
(16, "escaptes Pipe am Zeilenende ohne Alias",
|
||||
r"Siehe [[Ziel\|]] dort.", ["Ziel"]),
|
||||
(17, "Dreier-Zaun mit Info-String in Vierer-Zaun",
|
||||
"````markdown\n```bash\ngrep '[[:space:]]' datei\n```\n````\n[[Danach]]",
|
||||
["Danach"]),
|
||||
(18, "Dreier-Schliesszaun schliesst Vierer nicht",
|
||||
"````\n```\n[[drin]]\n````\n[[Danach]]", ["Danach"]),
|
||||
(19, "Vierer-Zaun schliesst Vierer-Zaun",
|
||||
"````\n[[drin]]\n````\n[[Danach]]", ["Danach"]),
|
||||
(20, "```bash in einem ```-Block schliesst nicht",
|
||||
"```\n```bash\n[[drin]]\n```\n[[Danach]]", ["Danach"]),
|
||||
(21, "Schliesszaun mit Leerraum dahinter",
|
||||
"```\n[[drin]]\n``` \t\n[[Danach]]", ["Danach"]),
|
||||
(22, "Schliesszaun mit CR am Zeilenende",
|
||||
"```\r\n[[drin]]\r\n```\r\n[[Danach]]\r\n", ["Danach"]),
|
||||
]
|
||||
````
|
||||
|
||||
Fall 19, 21 und 22 laufen schon vor der Umsetzung grün. Sie sichern ab, dass die neue Regel nichts kaputtmacht, was bisher ging (19), und dass der neue Ausdruck Leerraum und CR hinter dem Schließzaun zulässt (21, 22).
|
||||
|
||||
## Schritt 2 — Fehlschlag beobachten
|
||||
|
||||
```powershell
|
||||
Set-Location D:\Claude
|
||||
$env:PYTHONIOENCODING = "utf-8"
|
||||
python .vault-tools\linkcheck\faelle.py; "exit=$LASTEXITCODE"
|
||||
```
|
||||
|
||||
Erwartet vor der Umsetzung:
|
||||
|
||||
- Die Zeilen für `FALL 17`, `FALL 18` und `FALL 20` tragen `FEHLER`.
|
||||
- `FALL 17` meldet `erwartet ['Danach'], bekommen [':space:', 'Danach']`.
|
||||
- `FALL 18` meldet `erwartet ['Danach'], bekommen ['drin']`.
|
||||
- `FALL 20` meldet `erwartet ['Danach'], bekommen ['drin']`.
|
||||
- Alle anderen Fälle `ok`.
|
||||
- Schlusszeile `20 von 23 Faellen bestanden.`, danach `exit=1`.
|
||||
|
||||
Sieht die Ausgabe anders aus, hier anhalten und melden. Nicht mit Schritt 3 weitermachen.
|
||||
|
||||
## Schritt 3 — Umsetzung in `linkcheck.py`
|
||||
|
||||
### 3a Kopfkommentar, Herkunft
|
||||
|
||||
Alttext:
|
||||
|
||||
```text
|
||||
- Ungeschlossener Zaun gilt bis Dateiende: PROJEKTVORGABE - die
|
||||
konservative Wahl, lieber einen echten Link uebersehen als Rauschen
|
||||
erzeugen.
|
||||
```
|
||||
|
||||
Neutext:
|
||||
|
||||
```text
|
||||
- Ungeschlossener Zaun gilt bis Dateiende: PROJEKTVORGABE - die
|
||||
konservative Wahl, lieber einen echten Link uebersehen als Rauschen
|
||||
erzeugen.
|
||||
- Schliesszaun: QUELLE - dasselbe Zeichen wie der Oeffner, mindestens so
|
||||
lang, danach nur Leerzeichen oder Tab. Abgeschrieben aus markdown-it-py
|
||||
4.2.0, rules_block/fence.py Zeilen 42-43, 106 und 119-123; Referenz ist
|
||||
CommonMark 0.31.2, Abschnitt 4.5 Fenced code blocks. Fundstellen und der
|
||||
offene Abgleich mit dem Spezifikationstext: ENTWURF-codeblock-erkennung.md.
|
||||
```
|
||||
|
||||
### 3b Kopfkommentar, benannte Lücke
|
||||
|
||||
Alttext:
|
||||
|
||||
```text
|
||||
- Eingerueckte Codebloecke (vier Leerzeichen) werden NICHT erkannt. Sie
|
||||
wuerden eingerueckte Listenpunkte mit echten Links verschlucken.
|
||||
```
|
||||
|
||||
Neutext:
|
||||
|
||||
```text
|
||||
- Eingerueckte Codebloecke (vier Leerzeichen) werden NICHT erkannt. Sie
|
||||
wuerden eingerueckte Listenpunkte mit echten Links verschlucken.
|
||||
- Ein Oeffner mit Backtick im Info-String gilt hier als Zaun. CommonMark
|
||||
liest so eine Zeile als Inline-Code. Nicht behandelt.
|
||||
```
|
||||
|
||||
### 3c Ausdruck für den Schließzaun
|
||||
|
||||
Alttext:
|
||||
|
||||
```python
|
||||
_FENCE_RE = re.compile(r"^\s{0,3}(`{3,}|~{3,})")
|
||||
_INLINE_CODE_RE = re.compile(r"(`+)(?:(?!\1).)*?\1")
|
||||
```
|
||||
|
||||
Neutext:
|
||||
|
||||
```python
|
||||
_FENCE_RE = re.compile(r"^\s{0,3}(`{3,}|~{3,})")
|
||||
# Schliesszaun: nur Zaunzeichen, danach hoechstens Leerzeichen, Tab und ein
|
||||
# CR. Eine Zeile mit Info-String im Block ist deshalb Inhalt, kein Ende.
|
||||
_SCHLUSS_RE = re.compile(r"^\s{0,3}(`{3,}|~{3,})[ \t]*\r?$")
|
||||
_INLINE_CODE_RE = re.compile(r"(`+)(?:(?!\1).)*?\1")
|
||||
```
|
||||
|
||||
### 3d Zustand in `strip_code()`
|
||||
|
||||
Alttext:
|
||||
|
||||
```python
|
||||
if zaun is None:
|
||||
if m:
|
||||
zaun = m.group(1)[0] * 3
|
||||
aus.append(" " * len(zeile))
|
||||
continue
|
||||
aus.append(_INLINE_CODE_RE.sub(lambda t: " " * len(t.group(0)), zeile))
|
||||
else:
|
||||
if m and m.group(1)[0] * 3 == zaun:
|
||||
zaun = None
|
||||
aus.append(" " * len(zeile))
|
||||
```
|
||||
|
||||
Neutext:
|
||||
|
||||
```python
|
||||
if zaun is None:
|
||||
if m:
|
||||
zaun = m.group(1)
|
||||
aus.append(" " * len(zeile))
|
||||
continue
|
||||
aus.append(_INLINE_CODE_RE.sub(lambda t: " " * len(t.group(0)), zeile))
|
||||
else:
|
||||
s = _SCHLUSS_RE.match(zeile)
|
||||
# Gleiches Zeichen und mindestens so lang wie der Oeffner: ein
|
||||
# Dreier-Zaun schliesst keinen Vierer-Zaun.
|
||||
if s and s.group(1)[0] == zaun[0] and len(s.group(1)) >= len(zaun):
|
||||
zaun = None
|
||||
aus.append(" " * len(zeile))
|
||||
```
|
||||
|
||||
## Schritt 4 — Entwurf ergänzen
|
||||
|
||||
Alle Ersetzungen in `D:\Claude\.vault-tools\linkcheck\ENTWURF-codeblock-erkennung.md`.
|
||||
|
||||
### 4a Entscheidungsliste, neue Zeile 7
|
||||
|
||||
Alttext (eine Zeile):
|
||||
|
||||
```text
|
||||
| 6 | Auch Überschriften betroffen? | **Ja, gleicher Fehler** — `extract_headings` liest `# Kommentar` aus Bash-Blöcken als Überschrift. **In dieser Runde nicht geändert**, weil es den Index-Inhalt verschiebt und einen eigenen Nachweis braucht | **Befund, benannt statt stillschweigend miterledigt** |
|
||||
```
|
||||
|
||||
Neutext (zwei Zeilen):
|
||||
|
||||
```text
|
||||
| 6 | Auch Überschriften betroffen? | **Ja, gleicher Fehler** — `extract_headings` liest `# Kommentar` aus Bash-Blöcken als Überschrift. **In dieser Runde nicht geändert**, weil es den Index-Inhalt verschiebt und einen eigenen Nachweis braucht | **Befund, benannt statt stillschweigend miterledigt** |
|
||||
| 7 | Welche Zeile schließt einen Zaun? | Nur eine Zeile aus demselben Zaunzeichen, mindestens so lang wie der Öffner, bis zu drei Zeichen eingerückt, danach nur Leerzeichen oder Tab. Eine Zeile mit Info-String (drei Backticks und `bash`) im Block ist Inhalt | **Quelle** — Fundstellen im Abschnitt „Nachbesserung 2026-09-13" |
|
||||
```
|
||||
|
||||
### 4b Angriffsliste, sechs neue Zeilen
|
||||
|
||||
Alttext (eine Zeile):
|
||||
|
||||
```text
|
||||
| 16 | escaptes Pipe ohne Alias | Ziel ohne Backslash |
|
||||
```
|
||||
|
||||
Neutext (sieben Zeilen):
|
||||
|
||||
```text
|
||||
| 16 | escaptes Pipe ohne Alias | Ziel ohne Backslash |
|
||||
| 17 | Dreier-Zaun mit Info-String innerhalb eines Vierer-Zauns, darin `[[:space:]]` | kein Link aus dem Block, Link danach bleibt |
|
||||
| 18 | Dreier-Schließzaun in einem Vierer-Zaun | schließt nicht |
|
||||
| 19 | Vierer-Zaun schließt Vierer-Zaun | schließt, Link danach bleibt |
|
||||
| 20 | Zeile mit drei Backticks und `bash` in einem Dreier-Block | schließt nicht |
|
||||
| 21 | Schließzaun mit Leerzeichen und Tab dahinter | schließt — neuer Ausdruck, eigener Fall |
|
||||
| 22 | Schließzaun mit CR am Zeilenende | schließt — neuer Ausdruck, eigener Fall |
|
||||
```
|
||||
|
||||
### 4c Neuer Abschnitt vor „Absturzfestigkeit"
|
||||
|
||||
Alttext (eine Zeile):
|
||||
|
||||
```text
|
||||
## Absturzfestigkeit
|
||||
```
|
||||
|
||||
Neutext:
|
||||
|
||||
````text
|
||||
## Nachbesserung 2026-09-13: Schließzaun nach CommonMark
|
||||
|
||||
**Anlass.** Der Linkcheck meldete am 2026-09-13 einen toten Wikilink `[[:space:]]` in `Code Projekte/active - Logging/docs/plans/2026-09-08-a-betriebsmodelle/task-09-gate.md`. Die Datei öffnet in Zeile 356 einen Block mit vier Backticks und zeigt darin ab Zeile 406 ein eingerücktes Beispiel mit drei Backticks und `bash`. Die alte Regel schloss den äußeren Block schon an Zeile 406, weil jede Zaunzeile aus demselben Zeichen genügte. Zeile 407 mit der POSIX-Zeichenklasse lag damit im Fließtext.
|
||||
|
||||
**Regel.** Ein Block schließt nur mit einer Zeile, die
|
||||
|
||||
- aus demselben Zaunzeichen besteht wie der Öffner (Backtick oder Tilde),
|
||||
- mindestens so viele Zeichen hat wie der Öffner,
|
||||
- nach den Zaunzeichen nur Leerzeichen oder Tab trägt, also keinen Info-String.
|
||||
|
||||
Die Einrückung von bis zu drei Zeichen übernimmt der Schließzaun vom Öffner (`\s{0,3}`). Ein CR am Zeilenende ist zugelassen, damit eine Eingabe mit CRLF nicht an jedem Schließzaun vorbeiläuft.
|
||||
|
||||
**Herkunft.**
|
||||
|
||||
| Aussage | Fundstelle |
|
||||
|---|---|
|
||||
| dasselbe Zeichen | markdown-it-py 4.2.0, `markdown_it/rules_block/fence.py` Zeile 106 |
|
||||
| mindestens so lang | ebenda Zeilen 42–43, `closing_len >= opening_len` |
|
||||
| danach nur Leerraum | ebenda Zeilen 119–123; Leerraum heißt Leerzeichen oder Tab laut `markdown_it/common/utils.py` Zeilen 167–169 |
|
||||
|
||||
Gelesen am 2026-09-13 unter `Code Projekte/active - Agentic Cockpit/.venv/Lib/site-packages/markdown_it/`. Die Bibliothek nennt ihre Regel selbst CommonMark-konform (`fence.py` Zeile 145). Referenz der Regel ist die CommonMark-Spezifikation 0.31.2, Abschnitt 4.5 Fenced code blocks, https://spec.commonmark.org/0.31.2/#fenced-code-blocks. **Den Wortlaut der Spezifikation hat beim Planen niemand gezogen, der Planlauf hatte keinen Netzzugriff. Belegt ist die Regel an der Implementierung. Der Abgleich mit dem Spezifikationstext ist offen.**
|
||||
|
||||
**Benannte Lücke.** Ein Öffner mit Backtick im Info-String ist nach markdown-it-py kein Zaun (`fence.py` Zeilen 79–80). Hier gilt er weiter als Zaun. Diese Runde ändert nur den Schließzaun. Die Öffnerregel verschiebt, welche Zeilen überhaupt als Code gelten, und braucht einen eigenen Lauf über den Vault.
|
||||
|
||||
**Was schiefgehen kann.** Hat bisher eine Zeile mit Info-String einen Block versehentlich geschlossen, läuft der Block jetzt weiter, notfalls bis zum Dateiende. Wikilinks dahinter prüft der Linkcheck dann nicht mehr. Deshalb gehört zur Abnahme ein Vergleichslauf alt gegen neu über den ganzen Vault.
|
||||
|
||||
## Absturzfestigkeit
|
||||
````
|
||||
|
||||
### 4d Abbruchbedingung ohne gepflegte Zahl
|
||||
|
||||
Alttext:
|
||||
|
||||
```text
|
||||
**und** die vierzehn Angriffsfälle grün sind.
|
||||
```
|
||||
|
||||
Neutext:
|
||||
|
||||
```text
|
||||
**und** alle Angriffsfälle aus `faelle.py` grün sind.
|
||||
```
|
||||
|
||||
## Verifikation
|
||||
|
||||
```powershell
|
||||
Set-Location D:\Claude
|
||||
$env:PYTHONIOENCODING = "utf-8"
|
||||
python -m py_compile .vault-tools\linkcheck\linkcheck.py .vault-tools\linkcheck\faelle.py; "exit=$LASTEXITCODE"
|
||||
python .vault-tools\linkcheck\faelle.py; "exit=$LASTEXITCODE"
|
||||
python .vault-tools\linkcheck\linkcheck.py --vault . --alle; "exit=$LASTEXITCODE"
|
||||
python .vault-tools\regelwerk-check\regelwerk_check.py --vault .; "exit=$LASTEXITCODE"
|
||||
```
|
||||
|
||||
Erwartet:
|
||||
|
||||
1. `py_compile`: keine Ausgabe, `exit=0`.
|
||||
2. `faelle.py`: keine Zeile mit `FEHLER` oder `ABSTURZ`, Schlusszeile `N von N Faellen bestanden.` mit derselben Zahl vorn und hinten, `exit=0`.
|
||||
3. `linkcheck.py`: In der Liste der toten Wikilinks steht keine Zeile mehr mit `task-09-gate.md -> [[:space:]]`. Die übrigen Befunde vor und nach der Änderung melden (Liste wörtlich in den Bericht übernehmen). `exit` ist 1, solange andere tote Links bleiben, sonst 0.
|
||||
4. `regelwerk_check.py`: dieselben Befunde wie vorher. Beim Planen war das genau einer, `[klein] AI/Regelwerk/pruefwerkzeuge.md — status: pending-review — Freigabe steht aus`, und `exit=1`. Eine zusätzliche Befundzeile ist ein Fehler dieses Tasks und wird gemeldet.
|
||||
|
||||
## Bericht an den Koordinator
|
||||
|
||||
- Ausgabe von Schritt 2 wörtlich (die drei FEHLER-Zeilen und die Schlusszeile).
|
||||
- Ausgabe der vier Verifikationsbefehle wörtlich.
|
||||
- Jede Stelle, an der ein Alttext nicht genau einmal gefunden wurde. In diesem Fall die Ersetzung nicht vornehmen, sondern melden.
|
||||
|
||||
## Nachtrag Runde 2
|
||||
|
||||
Nachbesserung nach der ersten Prüfrunde (Befunde 5 und 6, Details in `ENTWURF-codeblock-erkennung.md`, Abschnitt „Nachbesserung Runde 2"). Weicht vom Plantext oben ab:
|
||||
|
||||
- **`_SCHLUSS_RE`** in `linkcheck.py` verlangt jetzt `^[ ]{0,3}` statt `^\s{0,3}` vor dem Zaunzeichen — nur Leerzeichen, kein Tab. Ein Tab vor dem Schließzaun zählt bei markdown-it-py als vier Spalten Einrückung und macht die Zeile zu eingerücktem Code, kein Zaun mehr (`state_block.py` Zeilen 257–261). Die Bedingung „gleiches Zaunzeichen" (`s.group(1)[0] == zaun[0]`) war schon vorher im Code, hatte aber keinen Fall.
|
||||
- **`faelle.py`** bekam drei weitere Fälle (23–25): anderes Zaunzeichen schließt nicht, Schließzaun mit vier Leerzeichen davor schließt nicht, Schließzaun mit Tab davor schließt nicht.
|
||||
- Details, Herkunft und Mutationsbelege stehen in `ENTWURF-codeblock-erkennung.md`, nicht hier verdoppelt.
|
||||
Reference in new issue
Block a user