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,552 @@
|
||||
"""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.
|
||||
|
||||
Seit der Nachbesserung Runde 2 bestimmt regelwerk_nach_edit.py seine
|
||||
Vault-Wurzel ausschließlich aus dem eigenen Skriptpfad, das Feld cwd der
|
||||
Hook-Eingabe zählt nicht mehr (Prüferbefund Runde 2, Koordinatorentscheidung).
|
||||
lauf_fall() kopiert den Hook deshalb für jeden Fall zuerst in den Wegwerf-
|
||||
Vault, statt das Original mit einem cwd-Feld dorthin zu verweisen.
|
||||
|
||||
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, ".claude/workflows/demo.js", "export const meta = {}\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 = ""
|
||||
max_dauer: float = 10.0
|
||||
# None: Subprozess erbt die Umgebung dieses Treibers unveraendert (bisheriges
|
||||
# Verhalten). Gesetzt: ersetzt die Umgebung vollstaendig (Pruefbefunde Runde 2).
|
||||
umgebung: Callable[[Path], dict[str, str]] | None = None
|
||||
|
||||
|
||||
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]) -> Fall:
|
||||
return Fall(name, "befund", stdin, "meldung", BEFUND, BEFUND_SYSTEM)
|
||||
|
||||
|
||||
def cwd_zeigt_anderswo(v: Path) -> bytes:
|
||||
"""cwd zeigt auf einen anderen, echten (leeren) Ordner. Seit der
|
||||
Nachbesserung Runde 2 liest der Hook cwd gar nicht mehr - die Wurzel ist
|
||||
ausschliesslich der Ordner, in dem der Hook selbst liegt (hier: v, weil
|
||||
lauf_fall() ihn dorthin kopiert). Das Ergebnis muss trotzdem melden,
|
||||
nicht still bleiben (Pruefbefund Runde 2)."""
|
||||
anderswo = v.parent / "anderswo"
|
||||
anderswo.mkdir(parents=True, exist_ok=True)
|
||||
return edit(v, str(v / "AI" / "Regelwerk" / "regel.md"), cwd=str(anderswo))
|
||||
|
||||
|
||||
def ohne_pythonioencoding(_: Path) -> dict[str, str]:
|
||||
"""Claude Code setzt PYTHONIOENCODING beim Hook-Aufruf nicht; nur die globale
|
||||
Planvorgabe setzt sie vor dem Aufruf dieses Treibers selbst (Pruefbefund Runde 2)."""
|
||||
umgebung = dict(os.environ)
|
||||
umgebung.pop("PYTHONIOENCODING", None)
|
||||
return umgebung
|
||||
|
||||
|
||||
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("verweis | Ordnername mit gleichem Anfang wie workflows",
|
||||
lambda v: edit(v, str(v / ".claude" / "workflowsAlt" / "x.js"))),
|
||||
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("pfad | Workflowdatei",
|
||||
lambda v: edit(v, str(v / ".claude" / "workflows" / "demo.js"))),
|
||||
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")),
|
||||
Fall("umgebung | ungueltiges UTF-8 im Pfad bleibt gueltiges JSON ohne PYTHONIOENCODING",
|
||||
"befund",
|
||||
lambda v: edit(v, "PLATZHALTER").replace(
|
||||
b"PLATZHALTER", vorwaerts(v).encode("utf-8") + b"/AI/Regelwerk/regel\xff.md"),
|
||||
"meldung", enthaelt=BEFUND, system_enthaelt=BEFUND_SYSTEM, umgebung=ohne_pythonioencoding),
|
||||
Fall("umgebung | Nicht-ASCII-Pfad ohne PYTHONIOENCODING bleibt gueltiges UTF-8-JSON",
|
||||
"befund",
|
||||
lambda v: edit(v, str(v / "AI" / "Regelwerk" / "Prüfüng Übersicht 🚀.md")),
|
||||
"meldung", enthaelt=BEFUND, system_enthaelt=BEFUND_SYSTEM, umgebung=ohne_pythonioencoding),
|
||||
meldet("pfad | cwd mit abschliessendem Backslash",
|
||||
lambda v: edit(v, str(v / "CLAUDE.md"), cwd=str(v) + "\\")),
|
||||
meldet("fehlt | cwd fehlt, wird nicht mehr gelesen",
|
||||
lambda v: ohne_cwd(v, str(v / "CLAUDE.md"))),
|
||||
meldet("null | cwd ist null, wird nicht mehr gelesen",
|
||||
lambda v: edit(v, str(v / "CLAUDE.md"), cwd=None)),
|
||||
meldet("falscher Typ| cwd ist eine Zahl, wird nicht mehr gelesen",
|
||||
lambda v: edit(v, str(v / "CLAUDE.md"), cwd=5)),
|
||||
meldet("verweis | cwd existiert nicht, wird nicht mehr gelesen",
|
||||
lambda v: edit(v, str(v / "CLAUDE.md"), cwd=str(v / "gibtsnicht"))),
|
||||
meldet("verweis | cwd zeigt auf einen anderen Ordner, Ergebnis bleibt der Skript-Vault",
|
||||
cwd_zeigt_anderswo),
|
||||
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 streng_utf8(roh: bytes) -> str:
|
||||
# stdout muss gueltiges UTF-8 sein. Ein ungueltiges Byte darf nicht still
|
||||
# ersetzt werden, sonst besteht der Umgebungsfall auch dann, wenn der Hook
|
||||
# cp1252 schreibt (Pruefrunde 2, Befund zur Dekodierung).
|
||||
try:
|
||||
return roh.decode("utf-8")
|
||||
except UnicodeDecodeError as exc:
|
||||
return "UNGUELTIGES-UTF-8: %s" % exc
|
||||
|
||||
|
||||
def fahre(
|
||||
skript: Path,
|
||||
eingabe: bytes,
|
||||
arbeitsordner: Path,
|
||||
umgebung: dict[str, str] | None = None,
|
||||
) -> 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,
|
||||
env=umgebung,
|
||||
)
|
||||
dauer = time.perf_counter() - start
|
||||
return (
|
||||
lauf.returncode,
|
||||
streng_utf8(lauf.stdout),
|
||||
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)
|
||||
# Seit der Nachbesserung Runde 2 bestimmt regelwerk_nach_edit.py seine
|
||||
# Vault-Wurzel ausschliesslich aus dem eigenen Skriptpfad, das Feld cwd
|
||||
# der Hook-Eingabe zaehlt nicht mehr. Nur eine Kopie im Wegwerf-Vault sorgt
|
||||
# dafuer, dass diese Wurzel auch tatsaechlich v ist statt des echten
|
||||
# Vaults, in dem das Original unter HOOK liegt.
|
||||
ziel = v / ".vault-tools" / "session-hooks"
|
||||
ziel.mkdir(parents=True, exist_ok=True)
|
||||
skript = ziel / HOOK.name
|
||||
shutil.copy2(HOOK, skript)
|
||||
umgebung = fall.umgebung(v) if fall.umgebung is not None else None
|
||||
code, out, err, dauer = fahre(skript, fall.stdin(v), arbeit, umgebung)
|
||||
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")
|
||||
# Kopie in v, wie in lauf_fall(): main() bestimmt die Wurzel seit Runde 2
|
||||
# ausschliesslich aus dem eigenen Skriptpfad, der file_path unten zeigt
|
||||
# aber in v hinein - ohne die Kopie faende main() keinen Geltungsbereich.
|
||||
ziel = v / ".vault-tools" / "session-hooks"
|
||||
ziel.mkdir(parents=True, exist_ok=True)
|
||||
kopie = ziel / HOOK.name
|
||||
shutil.copy2(HOOK, kopie)
|
||||
spec = importlib.util.spec_from_file_location("hook_wirft", kopie)
|
||||
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())
|
||||
Reference in new issue
Block a user