Regelwerk, Agenten, Skills und Werkzeuge veröffentlichen
Erster Stand des öffentlichen Ausschnitts aus dem Vault. Die .gitignore arbeitet als Positivliste: ausgeschlossen ist alles, eingeschlossen nur, was ausdrücklich wieder aufgenommen wird. Notizen, Projekte, Finanzen, das Sitzungsgedächtnis und settings.local.json bleiben damit außen vor. Der Kundenname in AI/Regelwerk/seo.md ist durch eine neutrale Bezeichnung ersetzt; die Regeln selbst sind unverändert. Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
commit
c064c729ed
82 files changed
+13428
No files matched your search
@@ -0,0 +1,641 @@
|
||||
"""Fährt die Angriffsliste gegen sitzungsstart.py, je Fall auf einem Wegwerf-Vault.
|
||||
|
||||
Ersteller: Pascal Michel
|
||||
Erstellt: 2026-09-13
|
||||
Entwurf: ENTWURF.md im selben Ordner
|
||||
|
||||
Grundlinie zuerst: Liefert der unbeschädigte Wegwerf-Vault nicht genau die
|
||||
erwarteten Zeilen, misst der Rest nichts und der Treiber bricht ab.
|
||||
|
||||
Je Fall eine frische Kopie der Grundlinie, genau eine Änderung und der Hook als
|
||||
eigener Prozess. Der Hook wird dafür immer zuerst in den Wegwerf-Vault kopiert:
|
||||
seit der Nachbesserung Runde 2 bestimmt sitzungsstart.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). Nur eine Kopie im
|
||||
Wegwerf-Vault sorgt dafür, dass diese Wurzel auch tatsächlich der Wegwerf-Vault
|
||||
ist statt des echten Vaults, in dem das Original liegt. Vier Fehlerpfade laufen
|
||||
im Prozess, weil sie sich von außen nicht auslösen lassen. Protokolliert werden
|
||||
Exit-Code, Laufzeit und bei Abweichung der ganze Kontext. Bestanden heißt:
|
||||
Exit 0, stdout ist genau ein JSON-Objekt der erwarteten Form, jede erwartete
|
||||
Zeile steht da, keine verbotene.
|
||||
|
||||
Aufruf:
|
||||
python .vault-tools/session-hooks/faelle_sitzungsstart.py
|
||||
python .vault-tools/session-hooks/faelle_sitzungsstart.py --echt
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import contextlib
|
||||
import datetime
|
||||
import importlib.util
|
||||
import io
|
||||
import json
|
||||
import os
|
||||
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 / "sitzungsstart.py"
|
||||
LINKCHECK = HIER.parent / "linkcheck" / "linkcheck.py"
|
||||
HEUTE = datetime.date.today().isoformat()
|
||||
GESTERN = (datetime.date.today() - datetime.timedelta(days=1)).isoformat()
|
||||
ZIEL_LAUFZEIT_S = 5.0
|
||||
|
||||
DA = f"- Datum laut Maschinenuhr: {HEUTE} (das Sitzungsdatum aus dem Kontext hat Vorrang)"
|
||||
TN = f"- Tagesnotiz Inbox/{HEUTE}.md: "
|
||||
IN = "- Inbox-Notizen ohne type: daily: "
|
||||
NR = "- Notizen mit Tag needs-review: "
|
||||
RV = "- Notizen mit Tag review ohne review-done: "
|
||||
PR = "- Regelwerk-Dateien mit status: pending-review: "
|
||||
SI = f"- Sitzungsdateien AI/sessions/{HEUTE}-*.md: "
|
||||
FEHLERZEILE = "Sitzungsstart-Hook:"
|
||||
|
||||
|
||||
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_grundlinie(v: Path) -> None:
|
||||
schreibe(v, f"Inbox/{HEUTE}.md",
|
||||
"---\ntype: daily\ntitle: \"Heute\"\nstatus: open\ntags: [daily]\n---\n\n# Heute\n")
|
||||
schreibe(v, "Inbox/offen.md", "---\ntype: note\ntags: [inbox]\n---\n\nUnverarbeitet.\n")
|
||||
schreibe(v, "AI/Regelwerk/regel.md", "---\ntype: resource\nstatus: reference\n---\n\n# Regel\n")
|
||||
schreibe(v, f"AI/sessions/{HEUTE}-10.md", "---\ntype: session\n---\n")
|
||||
schreibe(v, f"AI/sessions/{GESTERN}-10.md", "---\ntype: session\n---\n")
|
||||
schreibe(v, "Notizen/sauber.md",
|
||||
"---\ntype: note\ntags: [thema]\n---\n\nKein Tag hier, nur das Wort review ohne Raute.\n")
|
||||
|
||||
|
||||
def grundzeilen(v: Path) -> list[str]:
|
||||
return [
|
||||
f"Sitzungsstart-Hook (Vault: {v})",
|
||||
DA,
|
||||
TN + "vorhanden",
|
||||
IN + "1 (offen.md)",
|
||||
NR + "0",
|
||||
RV + "0",
|
||||
PR + "0",
|
||||
SI + f"1 ({HEUTE}-10.md)",
|
||||
]
|
||||
|
||||
|
||||
def kopiere_hook(v: Path, linkcheck_kopieren: bool = True) -> Path:
|
||||
"""Kopiert den Hook (und optional linkcheck.py) in den Wegwerf-Vault.
|
||||
|
||||
Seit der Nachbesserung Runde 2 bestimmt sitzungsstart.py seine Vault-
|
||||
Wurzel ausschließlich aus dem eigenen Skriptpfad (zwei Ebenen darüber,
|
||||
siehe bestimme_wurzel()). Nur eine Kopie im Wegwerf-Vault sorgt dafür,
|
||||
dass diese Wurzel auch tatsächlich der Wegwerf-Vault ist statt des echten
|
||||
Vaults, in dem das Original unter HIER liegt.
|
||||
"""
|
||||
ziel = v / ".vault-tools" / "session-hooks"
|
||||
ziel.mkdir(parents=True, exist_ok=True)
|
||||
skript = ziel / HOOK.name
|
||||
shutil.copy2(HOOK, skript)
|
||||
if linkcheck_kopieren:
|
||||
lc_ziel = v / ".vault-tools" / "linkcheck"
|
||||
lc_ziel.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copy2(LINKCHECK, lc_ziel / "linkcheck.py")
|
||||
return skript
|
||||
|
||||
|
||||
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
|
||||
(Prüferbefund Runde 2)."""
|
||||
umgebung = dict(os.environ)
|
||||
umgebung.pop("PYTHONIOENCODING", None)
|
||||
return umgebung
|
||||
|
||||
|
||||
def _baue_anderswo(v: Path) -> Path:
|
||||
"""Ein echter, leerer Ordner neben v. Würde cwd doch noch gelesen, wäre
|
||||
das am fehlenden Inhalt sofort sichtbar (Prüferbefund Runde 2)."""
|
||||
anderswo = v.parent / "anderswo"
|
||||
anderswo.mkdir(parents=True, exist_ok=True)
|
||||
return anderswo
|
||||
|
||||
|
||||
@dataclass
|
||||
class Fall:
|
||||
name: str
|
||||
aendern: Callable[[Path], object]
|
||||
# None heisst: genau die Zeilen der Grundlinie, Kopfzeile eingeschlossen.
|
||||
erwartet: list[str] | None
|
||||
verboten: list[str] = field(default_factory=lambda: [FEHLERZEILE])
|
||||
praefixe: list[str] = field(default_factory=list)
|
||||
# Ueberschreibt die Standard-Eingabe (cwd auf den Wegwerf-Vault). Der Hook
|
||||
# liest cwd seit Runde 2 nicht mehr; das Feld bleibt hier nur fuer Faelle,
|
||||
# die gezielt eine kaputte oder ungewoehnliche stdin-Form pruefen wollen.
|
||||
stdin: Callable[[Path], bytes] | None = None
|
||||
linkcheck_kopieren: bool = True
|
||||
# None: Subprozess erbt die Umgebung dieses Treibers unveraendert. Gesetzt:
|
||||
# ersetzt die Umgebung vollstaendig (Pruefbefund Runde 2).
|
||||
umgebung: Callable[[Path], dict[str, str]] | None = None
|
||||
|
||||
|
||||
def nichts(v: Path) -> None:
|
||||
return None
|
||||
|
||||
|
||||
FAELLE: list[Fall] = [
|
||||
Fall("umgebung | Inbox-Ordner fehlt",
|
||||
lambda v: shutil.rmtree(v / "Inbox"),
|
||||
[TN + "fehlt", IN + "Ordner Inbox/ fehlt"]),
|
||||
Fall("fehlt | Tagesnotiz fehlt",
|
||||
lambda v: (v / "Inbox" / f"{HEUTE}.md").unlink(),
|
||||
[TN + "fehlt", IN + "1 (offen.md)"]),
|
||||
Fall("wertebereich| nur die Tagesnotiz von gestern",
|
||||
lambda v: (v / "Inbox" / f"{HEUTE}.md").rename(v / "Inbox" / f"{GESTERN}.md"),
|
||||
[TN + "fehlt", IN + "1 (offen.md)"]),
|
||||
Fall("leer | leere Datei in der Inbox",
|
||||
lambda v: schreibe(v, "Inbox/leer.md", b""),
|
||||
[IN + "2 (leer.md, offen.md)"]),
|
||||
Fall("leer | leere Notiz im Vault",
|
||||
lambda v: schreibe(v, "Notizen/leer.md", b""),
|
||||
[NR + "0", RV + "0"]),
|
||||
Fall("typ | Tagesnotiz ohne Frontmatter",
|
||||
lambda v: schreibe(v, f"Inbox/{HEUTE}.md", "# Heute\n"),
|
||||
[TN + "vorhanden", IN + f"2 ({HEUTE}.md, offen.md)"]),
|
||||
Fall("typ | type in Anfuehrungszeichen",
|
||||
lambda v: schreibe(v, "Inbox/offen.md", "---\ntype: \"daily\"\n---\n"),
|
||||
[IN + "0"]),
|
||||
Fall("CRLF | Frontmatter-Tag mit CRLF",
|
||||
lambda v: schreibe(v, "Notizen/crlf.md",
|
||||
b"---\r\ntype: note\r\ntags: [needs-review]\r\n---\r\n\r\nText\r\n"),
|
||||
[NR + "1 (Notizen/crlf.md)"]),
|
||||
Fall("CRLF | type: daily mit CRLF",
|
||||
lambda v: schreibe(v, "Inbox/offen.md", b"---\r\ntype: daily\r\n---\r\n"),
|
||||
[IN + "0"]),
|
||||
Fall("BOM | BOM vor dem Frontmatter mit Tag",
|
||||
lambda v: schreibe(v, "Notizen/bom.md", b"\xef\xbb\xbf---\ntags: [needs-review]\n---\n"),
|
||||
[NR + "1 (Notizen/bom.md)"]),
|
||||
Fall("BOM | BOM vor type: daily",
|
||||
lambda v: schreibe(v, "Inbox/offen.md", b"\xef\xbb\xbf---\ntype: daily\n---\n"),
|
||||
[IN + "0"]),
|
||||
Fall("kodierung | ungueltiges UTF-8 vor dem Tag",
|
||||
lambda v: schreibe(v, "Notizen/kaputt.md", b"Text \xff\xfe kaputt\n#needs-review\n"),
|
||||
[NR + "1 (Notizen/kaputt.md)"]),
|
||||
Fall("yaml | Tag-Liste nicht geschlossen",
|
||||
lambda v: schreibe(v, "Notizen/yaml.md", "---\ntags: [needs-review\ntype: note\n---\n"),
|
||||
[NR + "1 (Notizen/yaml.md)"]),
|
||||
Fall("yaml | Frontmatter ohne Schlusszeile",
|
||||
lambda v: schreibe(v, "Notizen/ohne-schluss.md", "---\ntags: [needs-review]\ntype: note\n"),
|
||||
[NR + "0"]),
|
||||
Fall("yaml | Block-Liste, Doppelpunkt im Titel",
|
||||
lambda v: schreibe(v, "Notizen/block.md",
|
||||
"---\ntitle: \"A: B\"\ntags:\n - thema\n - needs-review\n---\n"),
|
||||
[NR + "1 (Notizen/block.md)"]),
|
||||
Fall("yaml | Tag mit Raute in Anfuehrungszeichen",
|
||||
lambda v: schreibe(v, "Notizen/raute.md", "---\ntags: [\"#needs-review\"]\n---\n"),
|
||||
[NR + "1 (Notizen/raute.md)"]),
|
||||
Fall("falscher Typ| tags als Skalar statt Liste",
|
||||
lambda v: schreibe(v, "Notizen/skalar.md", "---\ntags: needs-review\n---\n"),
|
||||
[NR + "1 (Notizen/skalar.md)"]),
|
||||
Fall("code | Tags im Fenced-Codeblock",
|
||||
lambda v: schreibe(v, "Notizen/code.md", "Text\n\n```\n#needs-review\n#review\n```\n"),
|
||||
[NR + "0", RV + "0"]),
|
||||
Fall("code | Tag im Tilde-Codeblock",
|
||||
lambda v: schreibe(v, "Notizen/tilde.md", "~~~\n#needs-review\n~~~\n"),
|
||||
[NR + "0"]),
|
||||
Fall("code | Tags im Inline-Code",
|
||||
lambda v: schreibe(v, "Notizen/inline.md",
|
||||
"Das Wort `#needs-review` und `#review` nur erwaehnt.\n"),
|
||||
[NR + "0", RV + "0"]),
|
||||
Fall("reihenfolge | Tag an erster Stelle der Datei",
|
||||
lambda v: schreibe(v, "Notizen/erste.md", "#needs-review am Anfang\n\nText\n"),
|
||||
[NR + "1 (Notizen/erste.md)"]),
|
||||
Fall("reihenfolge | Tag an letzter Stelle, ohne Zeilenende",
|
||||
lambda v: schreibe(v, "Notizen/letzte.md", "Text\n\nzuletzt #needs-review"),
|
||||
[NR + "1 (Notizen/letzte.md)"]),
|
||||
Fall("reihenfolge | Tag direkt nach einem Codeblock",
|
||||
lambda v: schreibe(v, "Notizen/nach-code.md", "```\ncode\n```\n#needs-review\n"),
|
||||
[NR + "1 (Notizen/nach-code.md)"]),
|
||||
Fall("grenze | aehnliche Tags zaehlen nicht",
|
||||
lambda v: schreibe(v, "Notizen/aehnlich.md",
|
||||
"#needs-review-done #needs-reviewer abc#needs-review #needs-review/unter\n"),
|
||||
[NR + "0"]),
|
||||
Fall("grenze | Grossschreibung zaehlt",
|
||||
lambda v: schreibe(v, "Notizen/gross.md", "Bitte #Needs-Review.\n"),
|
||||
[NR + "1 (Notizen/gross.md)"]),
|
||||
Fall("grenze | Frontmatter-Tag in Grossschreibung zaehlt",
|
||||
lambda v: schreibe(v, "Notizen/frontmatter-gross.md", "---\ntags: [Needs-Review]\n---\n"),
|
||||
[NR + "1 (Notizen/frontmatter-gross.md)"]),
|
||||
Fall("review | review ohne review-done",
|
||||
lambda v: schreibe(v, "Notizen/r.md", "Text #review\n"),
|
||||
[RV + "1"]),
|
||||
Fall("review | review-done neben review",
|
||||
lambda v: schreibe(v, "Notizen/r.md", "#review\n\nspaeter #review-done\n"),
|
||||
[RV + "0"]),
|
||||
Fall("review | review im Frontmatter, review-done im Text",
|
||||
lambda v: schreibe(v, "Notizen/r.md", "---\ntags: [review]\n---\n\nAbgenommen #review-done\n"),
|
||||
[RV + "0"]),
|
||||
Fall("review | review-done allein",
|
||||
lambda v: schreibe(v, "Notizen/r.md", "#review-done\n"),
|
||||
[RV + "0"]),
|
||||
Fall("review | needs-review ist kein review",
|
||||
lambda v: schreibe(v, "Notizen/nur-needs.md", "#needs-review\n"),
|
||||
[NR + "1 (Notizen/nur-needs.md)", RV + "0"]),
|
||||
Fall("grenze | aehnliche review-Tags zaehlen nicht",
|
||||
lambda v: schreibe(v, "Notizen/aehnlich-review.md",
|
||||
"#reviewer abc#review #review/x\n"),
|
||||
[RV + "0"]),
|
||||
Fall("grenze | aehnlicher review-done-Tag zaehlt nicht als review-done",
|
||||
lambda v: schreibe(v, "Notizen/aehnlich-review-done.md",
|
||||
"#review\n\n#review-done-x\n"),
|
||||
[RV + "1"]),
|
||||
Fall("grenze | abc#review-done zaehlt nicht als review-done",
|
||||
lambda v: schreibe(v, "Notizen/eingebettet-done.md", "#review\n\nabc#review-done\n"),
|
||||
[RV + "1"]),
|
||||
Fall("ausschluss | Punktdatei ausserhalb von Punktordnern",
|
||||
lambda v: schreibe(v, "Notizen/.versteckt.md", "#needs-review\n"),
|
||||
[NR + "0"]),
|
||||
Fall("reihenfolge | review an erster und an letzter Stelle",
|
||||
lambda v: (schreibe(v, "Notizen/a-erste.md", "#review\n\nText\n"),
|
||||
schreibe(v, "Notizen/z-letzte.md", "Text\n\n#review")),
|
||||
[RV + "2"]),
|
||||
Fall("ausschluss | Import/ unter der Wurzel",
|
||||
lambda v: schreibe(v, "Import/x.md", "#needs-review\n#review\n"),
|
||||
[NR + "0", RV + "0"]),
|
||||
Fall("ausschluss | _index/ unter der Wurzel",
|
||||
lambda v: schreibe(v, "_index/INDEX.md", "#needs-review\n"),
|
||||
[NR + "0"]),
|
||||
Fall("ausschluss | Punktordner",
|
||||
lambda v: schreibe(v, ".obsidian/x.md", "#needs-review\n"),
|
||||
[NR + "0"]),
|
||||
Fall("ausschluss | node_modules in der Tiefe",
|
||||
lambda v: schreibe(v, "Code/app/node_modules/pkg/README.md", "#needs-review\n"),
|
||||
[NR + "0"]),
|
||||
Fall("ausschluss | Import/ in der Tiefe zaehlt",
|
||||
lambda v: schreibe(v, "Projekt/Import/x.md", "#needs-review\n"),
|
||||
[NR + "1 (Projekt/Import/x.md)"]),
|
||||
Fall("ausschluss | Punktordner in der Inbox",
|
||||
lambda v: schreibe(v, "Inbox/.trash/versteckt.md", "x\n"),
|
||||
[IN + "1 (offen.md)"]),
|
||||
Fall("zusaetzlich | sieben weitere Inbox-Notizen, Namen gekuerzt",
|
||||
lambda v: [schreibe(v, f"Inbox/n0{i}.md", "---\ntype: note\n---\n") for i in range(1, 8)],
|
||||
[IN + "8 (n01.md, n02.md, n03.md, n04.md, n05.md, +3 weitere)"]),
|
||||
Fall("zusaetzlich | Notiz in einem Inbox-Unterordner",
|
||||
lambda v: schreibe(v, "Inbox/sub/tief.md", "x\n"),
|
||||
[IN + "2 (offen.md, sub/tief.md)"]),
|
||||
Fall("regelwerk | status: pending-review",
|
||||
lambda v: schreibe(v, "AI/Regelwerk/b.md", "---\ntype: resource\nstatus: pending-review\n---\n"),
|
||||
[PR + "1 (b.md)"]),
|
||||
Fall("regelwerk | status in Anfuehrungszeichen",
|
||||
lambda v: schreibe(v, "AI/Regelwerk/c.md", "---\nstatus: \"pending-review\"\n---\n"),
|
||||
[PR + "1 (c.md)"]),
|
||||
Fall("regelwerk | pending-review nur im Text",
|
||||
lambda v: schreibe(v, "AI/Regelwerk/d.md", "---\nstatus: reference\n---\nstatus: pending-review\n"),
|
||||
[PR + "0"]),
|
||||
Fall("umgebung | AI/Regelwerk fehlt",
|
||||
lambda v: shutil.rmtree(v / "AI" / "Regelwerk"),
|
||||
[PR + "Ordner AI/Regelwerk/ fehlt"]),
|
||||
Fall("doppelt | zwei Sitzungsdateien von heute",
|
||||
lambda v: schreibe(v, f"AI/sessions/{HEUTE}-10b.md", "x\n"),
|
||||
[SI + f"2 ({HEUTE}-10.md, {HEUTE}-10b.md)"]),
|
||||
Fall("umgebung | AI/sessions fehlt",
|
||||
lambda v: shutil.rmtree(v / "AI" / "sessions"),
|
||||
[SI + "Ordner AI/sessions/ fehlt"]),
|
||||
Fall("leer | Vault ohne jeden Ordner",
|
||||
lambda v: [shutil.rmtree(p) for p in list(v.iterdir())],
|
||||
[TN + "fehlt", IN + "Ordner Inbox/ fehlt", NR + "0", RV + "0",
|
||||
PR + "Ordner AI/Regelwerk/ fehlt", SI + "Ordner AI/sessions/ fehlt"]),
|
||||
Fall("leer | stdin leer", nichts, None, stdin=lambda v: b""),
|
||||
Fall("falscher Typ| stdin kein JSON", nichts, None, stdin=lambda v: b"kein json"),
|
||||
Fall("kodierung | stdin ungueltiges UTF-8", nichts, None, stdin=lambda v: b"\xff\xfe{"),
|
||||
Fall("falscher Typ| stdin ist eine Liste", nichts, None, stdin=lambda v: b'[{"cwd": "x"}]'),
|
||||
Fall("fehlt | cwd fehlt", nichts, None,
|
||||
stdin=lambda v: b'{"hook_event_name": "SessionStart"}'),
|
||||
Fall("null | cwd ist null", nichts, None, stdin=lambda v: b'{"cwd": null}'),
|
||||
Fall("falscher Typ| cwd ist eine Zahl", nichts, None, stdin=lambda v: b'{"cwd": 5}'),
|
||||
Fall("leer | cwd ist leer", nichts, None, stdin=lambda v: b'{"cwd": ""}'),
|
||||
Fall("verweis | cwd existiert nicht", nichts, None,
|
||||
stdin=lambda v: json.dumps({"cwd": str(v / "gibtsnicht")}).encode("utf-8")),
|
||||
Fall("falscher Typ| cwd zeigt auf eine Datei", nichts, None,
|
||||
stdin=lambda v: json.dumps({"cwd": str(v / "Inbox" / "offen.md")}).encode("utf-8")),
|
||||
Fall("verweis | cwd zeigt auf einen anderen Ordner, Ergebnis bleibt der Skript-Vault",
|
||||
nichts, None,
|
||||
stdin=lambda v: json.dumps({"cwd": str(_baue_anderswo(v))}).encode("utf-8")),
|
||||
Fall("umgebung | ohne PYTHONIOENCODING mit Nicht-ASCII-Inhalt bleibt gueltiges UTF-8-JSON",
|
||||
lambda v: schreibe(v, "Notizen/Übersicht.md", "Prüfung #needs-review äöü 🚀\n"),
|
||||
[NR + "1 (Notizen/Übersicht.md)"],
|
||||
umgebung=ohne_pythonioencoding),
|
||||
Fall("umgebung | linkcheck.py fehlt", nichts,
|
||||
[DA, TN + "vorhanden", IN + "1 (offen.md)", PR + "0", SI + f"1 ({HEUTE}-10.md)"],
|
||||
verboten=[NR, RV],
|
||||
praefixe=["Sitzungsstart-Hook: needs-review/review: RuntimeError: linkcheck.py nicht importierbar"],
|
||||
stdin=lambda v: b"{}",
|
||||
linkcheck_kopieren=False),
|
||||
]
|
||||
|
||||
|
||||
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=60,
|
||||
env=umgebung,
|
||||
)
|
||||
dauer = time.perf_counter() - start
|
||||
return (
|
||||
lauf.returncode,
|
||||
streng_utf8(lauf.stdout),
|
||||
lauf.stderr.decode("utf-8", errors="replace"),
|
||||
dauer,
|
||||
)
|
||||
|
||||
|
||||
def lies_kontext(code: int, out: str) -> tuple[list[str] | None, str]:
|
||||
if code != 0:
|
||||
return None, f"Exit {code} statt 0"
|
||||
roh = out.strip()
|
||||
if not (roh.startswith("{") and roh.endswith("}")):
|
||||
return None, "stdout ist kein JSON-Objekt"
|
||||
try:
|
||||
daten = json.loads(roh)
|
||||
except ValueError as exc:
|
||||
return None, f"stdout nicht parsebar: {exc}"
|
||||
if not isinstance(daten, dict) or set(daten) != {"hookSpecificOutput"}:
|
||||
return None, "oberste Ebene ist nicht genau {hookSpecificOutput}"
|
||||
innen = daten["hookSpecificOutput"]
|
||||
if (not isinstance(innen, dict)
|
||||
or innen.get("hookEventName") != "SessionStart"
|
||||
or not isinstance(innen.get("additionalContext"), str)):
|
||||
return None, "hookSpecificOutput hat nicht die erwartete Form"
|
||||
return innen["additionalContext"].split("\n"), ""
|
||||
|
||||
|
||||
def bewerte(zeilen: list[str], erwartet: list[str], verboten: list[str], praefixe: list[str]) -> list[str]:
|
||||
probleme = [f"fehlt: {z}" for z in erwartet if z not in zeilen]
|
||||
probleme += [f"fehlt Zeile mit Anfang: {p}" for p in praefixe
|
||||
if not any(z.startswith(p) for z in zeilen)]
|
||||
probleme += [f"verboten: {z}" for z in zeilen if any(z.startswith(p) for p in verboten)]
|
||||
return probleme
|
||||
|
||||
|
||||
def frische_kopie(basis: Path, arbeit: Path) -> Path:
|
||||
v = arbeit / "fall"
|
||||
if v.exists():
|
||||
shutil.rmtree(v)
|
||||
shutil.copytree(basis, v)
|
||||
return v
|
||||
|
||||
|
||||
def lauf_fall(fall: Fall, basis: Path, arbeit: Path) -> tuple[bool, str]:
|
||||
v = frische_kopie(basis, arbeit)
|
||||
fall.aendern(v)
|
||||
skript = kopiere_hook(v, fall.linkcheck_kopieren)
|
||||
if fall.stdin is None:
|
||||
eingabe = json.dumps({"session_id": "faelle", "cwd": str(v),
|
||||
"hook_event_name": "SessionStart", "source": "startup"}).encode("utf-8")
|
||||
else:
|
||||
eingabe = fall.stdin(v)
|
||||
umgebung = fall.umgebung(v) if fall.umgebung is not None else None
|
||||
code, out, err, dauer = fahre(skript, eingabe, arbeit, umgebung)
|
||||
kopf = f"exit={code} {dauer:.2f}s"
|
||||
zeilen, problem = lies_kontext(code, out)
|
||||
if zeilen is None:
|
||||
return False, f"{kopf}\n FORM: {problem}\n stdout: {out[:500]!r}\n stderr: {err[:800]!r}"
|
||||
if fall.erwartet is None:
|
||||
erwartet = grundzeilen(v)
|
||||
else:
|
||||
erwartet = [f"Sitzungsstart-Hook (Vault: {v})"] + fall.erwartet
|
||||
probleme = bewerte(zeilen, erwartet, fall.verboten, fall.praefixe)
|
||||
if not probleme:
|
||||
return True, kopf
|
||||
text = kopf + "\n" + "\n".join(" " + p for p in probleme)
|
||||
text += "\n Kontext:\n" + "\n".join(" " + z for z in zeilen)
|
||||
return False, text
|
||||
|
||||
|
||||
def lade_hook(name: str, skript_pfad: Path):
|
||||
spec = importlib.util.spec_from_file_location(name, skript_pfad)
|
||||
modul = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(modul)
|
||||
return modul
|
||||
|
||||
|
||||
def im_prozess(modul, v: Path) -> list[str] | None:
|
||||
puffer = io.StringIO()
|
||||
with contextlib.redirect_stdout(puffer):
|
||||
code = modul.main(json.dumps({"cwd": str(v)}))
|
||||
zeilen, _ = lies_kontext(code, puffer.getvalue())
|
||||
return zeilen
|
||||
|
||||
|
||||
def ip_punkt_wirft(v: Path) -> tuple[bool, str]:
|
||||
skript = kopiere_hook(v)
|
||||
modul = lade_hook("hook_punkt_wirft", skript)
|
||||
|
||||
def wirft(wurzel: Path, heute: str) -> list[str]:
|
||||
raise RuntimeError("Testfehler")
|
||||
|
||||
for i, (name, _) in enumerate(modul.PUNKTE):
|
||||
if name == "Sitzungen":
|
||||
modul.PUNKTE[i] = (name, wirft)
|
||||
zeilen = im_prozess(modul, v)
|
||||
if zeilen is None:
|
||||
return False, " keine gueltige Ausgabe"
|
||||
erwartet = [z for z in grundzeilen(v) if not z.startswith(SI)]
|
||||
erwartet.append("Sitzungsstart-Hook: Sitzungen: RuntimeError: Testfehler")
|
||||
probleme = bewerte(zeilen, erwartet, [SI], [])
|
||||
return not probleme, "\n".join(" " + p for p in probleme)
|
||||
|
||||
|
||||
def ip_hauptablauf_wirft(v: Path) -> tuple[bool, str]:
|
||||
skript = kopiere_hook(v)
|
||||
modul = lade_hook("hook_hauptablauf_wirft", skript)
|
||||
|
||||
def kaputt(wurzel: Path, heute: str, zeilen: list[str]) -> None:
|
||||
zeilen.append("- vorher gesammelt")
|
||||
raise RuntimeError("Testfehler Hauptablauf")
|
||||
|
||||
modul.baue_kontext = kaputt
|
||||
zeilen = im_prozess(modul, v)
|
||||
erwartet = ["- vorher gesammelt", "Sitzungsstart-Hook: RuntimeError: Testfehler Hauptablauf"]
|
||||
if zeilen != erwartet:
|
||||
return False, f" erwartet {erwartet!r}, bekommen {zeilen!r}"
|
||||
return True, ""
|
||||
|
||||
|
||||
def ip_datei_gesperrt(v: Path) -> tuple[bool, str]:
|
||||
schreibe(v, "Notizen/gesperrt.md", "#needs-review\n")
|
||||
skript = kopiere_hook(v)
|
||||
modul = lade_hook("hook_datei_gesperrt", skript)
|
||||
original = modul.lies_text
|
||||
|
||||
def gesperrt(pfad: Path) -> str:
|
||||
if Path(pfad).name == "gesperrt.md":
|
||||
raise PermissionError("Testsperre")
|
||||
return original(pfad)
|
||||
|
||||
modul.lies_text = gesperrt
|
||||
zeilen = im_prozess(modul, v)
|
||||
if zeilen is None:
|
||||
return False, " keine gueltige Ausgabe"
|
||||
erwartet = grundzeilen(v) + ["Sitzungsstart-Hook: 1 Datei(en) nicht lesbar (erste: Notizen/gesperrt.md)"]
|
||||
probleme = bewerte(zeilen, erwartet, [], [])
|
||||
return not probleme, "\n".join(" " + p for p in probleme)
|
||||
|
||||
|
||||
class _WalkPatch:
|
||||
"""Ersetzt os.walk nur im Namensraum des geladenen Hook-Moduls (modul.os),
|
||||
ohne das echte, global geteilte os-Modul anzufassen."""
|
||||
|
||||
def __init__(self, echt_os, fehler_pfad: Path) -> None:
|
||||
self._echt = echt_os
|
||||
self._fehler_pfad = fehler_pfad
|
||||
|
||||
def __getattr__(self, name):
|
||||
return getattr(self._echt, name)
|
||||
|
||||
def walk(self, start, onerror=None, **kwargs):
|
||||
if onerror is not None:
|
||||
fehler = OSError(13, "Testsperre")
|
||||
fehler.filename = str(self._fehler_pfad)
|
||||
onerror(fehler)
|
||||
yield from self._echt.walk(start, onerror=onerror, **kwargs)
|
||||
|
||||
|
||||
def ip_ordner_gesperrt(v: Path) -> tuple[bool, str]:
|
||||
"""Der onerror-Pfad von os.walk in durchlaufe(): von aussen nicht ohne
|
||||
echte Rechteaenderung ausloesbar, deshalb im Prozess."""
|
||||
skript = kopiere_hook(v)
|
||||
modul = lade_hook("hook_ordner_gesperrt", skript)
|
||||
fehler_pfad = v / "gesperrt"
|
||||
modul.os = _WalkPatch(modul.os, fehler_pfad)
|
||||
zeilen = im_prozess(modul, v)
|
||||
if zeilen is None:
|
||||
return False, " keine gueltige Ausgabe"
|
||||
erwartet = grundzeilen(v) + [f"Sitzungsstart-Hook: 1 Ordner nicht lesbar (erster: {fehler_pfad})"]
|
||||
probleme = bewerte(zeilen, erwartet, [], [])
|
||||
return not probleme, "\n".join(" " + p for p in probleme)
|
||||
|
||||
|
||||
IM_PROZESS = [
|
||||
("absturz | ein Punkt wirft", ip_punkt_wirft),
|
||||
("absturz | Hauptablauf wirft", ip_hauptablauf_wirft),
|
||||
("lesen | Datei nicht lesbar", ip_datei_gesperrt),
|
||||
("lesen | Ordner nicht lesbar", ip_ordner_gesperrt),
|
||||
]
|
||||
|
||||
|
||||
def echter_lauf() -> int:
|
||||
vault = HIER.parent.parent
|
||||
eingabe = json.dumps({"session_id": "echt", "cwd": str(vault),
|
||||
"hook_event_name": "SessionStart", "source": "startup"}).encode("utf-8")
|
||||
code, out, err, dauer = fahre(HOOK, eingabe, HIER)
|
||||
print(f"exit={code} Laufzeit {dauer:.2f} s (Ziel: unter {ZIEL_LAUFZEIT_S:.0f} s)")
|
||||
zeilen, problem = lies_kontext(code, out)
|
||||
if zeilen is None:
|
||||
print(f"FORMFEHLER: {problem}")
|
||||
print(out)
|
||||
print(err)
|
||||
return 1
|
||||
print("\n".join(zeilen))
|
||||
if err.strip():
|
||||
print("--- stderr ---")
|
||||
print(err)
|
||||
if dauer >= ZIEL_LAUFZEIT_S:
|
||||
print("LAUFZEIT UEBER ZIEL")
|
||||
return 1
|
||||
return 0
|
||||
|
||||
|
||||
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="sitzungsstart-faelle-")).resolve()
|
||||
try:
|
||||
basis = arbeit / "grundlinie"
|
||||
baue_grundlinie(basis)
|
||||
|
||||
print("=" * 78)
|
||||
print("GRUNDLINIE")
|
||||
print("=" * 78)
|
||||
v = frische_kopie(basis, arbeit)
|
||||
skript = kopiere_hook(v)
|
||||
eingabe = json.dumps({"cwd": str(v), "hook_event_name": "SessionStart"}).encode("utf-8")
|
||||
code, out, err, dauer = fahre(skript, eingabe, arbeit)
|
||||
zeilen, problem = lies_kontext(code, out)
|
||||
if zeilen != grundzeilen(v):
|
||||
print(f"exit={code} {problem}")
|
||||
print("erwartet:")
|
||||
print("\n".join(grundzeilen(v)))
|
||||
print("bekommen (stdout):")
|
||||
print(out)
|
||||
print("stderr:")
|
||||
print(err)
|
||||
print("ABBRUCH: Die Grundlinie ist nicht gruen. Der Rest misst nichts.")
|
||||
return 2
|
||||
print(f"Grundlinie gruen (exit 0, {dauer:.2f}s).")
|
||||
|
||||
print()
|
||||
print("=" * 78)
|
||||
print("ANGRIFFSLISTE")
|
||||
print("=" * 78)
|
||||
bestanden = 0
|
||||
gesamt = 0
|
||||
for fall in FAELLE:
|
||||
gesamt += 1
|
||||
try:
|
||||
ok, text = lauf_fall(fall, basis, 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}")
|
||||
|
||||
for name, funktion in IM_PROZESS:
|
||||
gesamt += 1
|
||||
try:
|
||||
ok, text = funktion(frische_kopie(basis, 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} {name} (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