regelwerk_check: Pruefung 7 meldet unsichtbare Zeichen in gespeicherten Workflows
Jedes Zeichen der Kategorie Cc ausser LF und jedes der Kategorie Cf in .claude/workflows/**/*.js, gelesen als Bytes. Anlass: plankritik.js mit CRLF scheiterte am Workflow-Werkzeug. Der Hook nach dem Bearbeiten loest auch bei Workflows aus. Regressionslauf 28/28 angeschlagen, 6/6 stumm; Hook-Treiber 58/58; Vorfall in exakter Form gemeldet (U+000D). Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
3082ab5b75
commit
e50c8296d7
7 files changed
+184
-13
No files matched your search
@@ -48,11 +48,25 @@ Benannte Lücken, bewusst nicht geprüft:
|
||||
|
||||
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 sechs Prüfungen schränken **geschlossene** Mengen ein: die Menge der Wikilinks in einer Datei, die Menge der Pflichtfelder, die Menge der Agentendateien, die Menge der Regelwerk-Dateien. Es gibt keine Prüfung, die eine offene Menge verbotener Muster abzählt — und damit auch keine handgepflegte Negativliste, die gegen sie verlieren könnte.
|
||||
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 sechs Achsen. Sie gilt ausdrücklich nicht für die Frage, ob zwei Regeln inhaltlich dasselbe sagen. Das sieht die Prüfung nicht und kann es nicht sehen.
|
||||
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
|
||||
|
||||
@@ -83,6 +97,12 @@ Je Fall eine Wegwerfkopie des Vaults, Rücksetzen dazwischen, Exit-Code und ausl
|
||||
| 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).
|
||||
|
||||
|
||||
@@ -8,13 +8,14 @@ Entwurf und Angriffsliste: .vault-tools/regelwerk-check/ENTWURF.md
|
||||
GELTUNGSBEREICH — was diese Prüfung sieht
|
||||
===============================================================================
|
||||
|
||||
Geprüft werden genau vier Orte:
|
||||
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
|
||||
|
||||
Sechs Prüfungen, jede über eine GESCHLOSSENE Menge:
|
||||
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?
|
||||
@@ -22,6 +23,7 @@ Sechs Prüfungen, jede über eine GESCHLOSSENE Menge:
|
||||
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
|
||||
@@ -67,11 +69,26 @@ WAS DIESE PRÜFUNG NICHT SIEHT — benannte Lücken
|
||||
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 sechs formalen Achsen verletzt ist.
|
||||
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.
|
||||
@@ -88,12 +105,16 @@ 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"\[\[([^\]]+)\]\]")
|
||||
|
||||
@@ -423,6 +444,49 @@ def pruefe_planablage(vault: Path, dateien: list[Path], befunde: list[Befund]) -
|
||||
)
|
||||
|
||||
|
||||
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:
|
||||
@@ -479,6 +543,7 @@ def main() -> int:
|
||||
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
|
||||
@@ -495,11 +560,11 @@ def main() -> int:
|
||||
print()
|
||||
if befunde:
|
||||
# Die Zahlen werden gerechnet, nicht geschrieben.
|
||||
print(f"{len(befunde)} Befund(e) aus {gelaufen} von 6 Prüfungen.")
|
||||
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 < 6 and not abbruch:
|
||||
print(f"ACHTUNG: nur {gelaufen} von 6 Prüfungen sind gelaufen — der Rest hatte keine Voraussetzung.")
|
||||
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
|
||||
|
||||
@@ -66,6 +66,11 @@ def baue_grundlinie(ziel: Path) -> None:
|
||||
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,
|
||||
@@ -332,6 +337,71 @@ def g_link_im_codeblock(v: Path) -> None:
|
||||
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).
|
||||
@@ -340,6 +410,8 @@ GEGENFAELLE = [
|
||||
("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),
|
||||
]
|
||||
|
||||
|
||||
@@ -364,6 +436,14 @@ FAELLE = [
|
||||
("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"),
|
||||
]
|
||||
|
||||
|
||||
|
||||
Reference in new issue
Block a user