Files
Pascal MichelandClaude Opus 5.5 fa1c26b407 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]>
2026-10-14 11:27:59 +02:00

102 lines
7.9 KiB
Python

"""Mutantenlauf gegen quorum_werkzeug.py: je Mutant genau eine Ersetzung in einer Kopie, dann faelle_quorum_werkzeug.py.
Jede Ersetzung muss genau einmal vorkommen, sonst bricht der Lauf mit Exit 2 ab (veralteter Mutant).
Aufruf: python mutanten_quorum_werkzeug.py
Exit 0, wenn jeder Mutant gefangen wird, 1 bei einem Ueberlebenden, 2 bei einem veralteten Muster.
"""
import shutil
import subprocess
import sys
import tempfile
from pathlib import Path
HIER = Path(__file__).resolve().parent
MUTANTEN = [
("Ausschluss docs/quorum fehlt", 'AUSSCHLUSS = ":(exclude)docs/quorum"', 'AUSSCHLUSS = "."'),
("erster statt hoechster Bericht", 'return max(berichte, key=lambda b: b["nummer"])', "return berichte[0]"),
("letzter statt hoechster Bericht", 'return max(berichte, key=lambda b: b["nummer"])', "return berichte[-1]"),
("unsauberer Arbeitsbaum erlaubt", " if unsauber:\n", " if False:\n"),
("unversionierte Dateien zusammengefasst", '"--porcelain", "--untracked-files=all", "--"', '"--porcelain", "--"'),
("erstes Quorum ohne basis nimmt HEAD", ' raise Befund("erstes Quorum in diesem Projekt: --basis angeben, etwa den Commit der letzten abgenommenen Version")', " basis = stand"),
("leer ohne Vorbericht erlaubt", " if not dateien and not vorher:\n", " if False:\n"),
("zwei gleiche Nummern erlaubt", " if doppelt:\n", " if False:\n"),
("nummer nicht hochgezaehlt", '"nummer": (vorher["nummer"] if vorher else 0) + 1', '"nummer": (vorher["nummer"] if vorher else 0)'),
("Berichtspfad ueberschrieben", " while pfad.exists():\n", " while False:\n"),
("CHANGELOG mit entfernten Zeilen", 'if z.startswith("+") and not z.startswith("+++")', 'if z.startswith(("+", "-")) and not z.startswith(("+++", "---"))'),
("Suite-Datei uebergangen", " suite_kurz, suite_zeilen = lies_suite(a.suite_datei)", ' suite_kurz, suite_zeilen = "ohne", []'),
("Kontext ungeprueft", " kontext = pruefe_kontext(a.kontext or [])", " kontext = [str(k) for k in (a.kontext or [])]"),
("geaenderte Datei im Satz uebersehen", " elif alt[rel] != summe:", " elif False:"),
("entfernte Datei uebersehen", ' dateien.append({"pfad": rel, "art": "entfernt"})', " pass"),
("Pfad ausserhalb des Ordners erlaubt", ' if rel.anchor or ".." in rel.parts:', " if False:"),
("Lesemarke im Codex-Auftrag", 'wurzel=teil["wurzel"].as_posix(), anfang=', 'wurzel=teil["wurzel"].as_posix() + " " + marke, anfang='),
("usage limit nur klein geschrieben", r'USAGE_LIMIT = re.compile(r"^\[[^\]]+\] Codex error: .*usage limit", re.I | re.M)', r'USAGE_LIMIT = re.compile(r"^\[[^\]]+\] Codex error: .*usage limit", re.M)'),
("usage limit ueberall im Log gezaehlt", r'USAGE_LIMIT = re.compile(r"^\[[^\]]+\] Codex error: .*usage limit", re.I | re.M)', 'USAGE_LIMIT = re.compile(r"usage limit", re.I)'),
("usage limit ohne Zeitstempel am Zeilenanfang", r'USAGE_LIMIT = re.compile(r"^\[[^\]]+\] Codex error: .*usage limit", re.I | re.M)', 'USAGE_LIMIT = re.compile(r"Codex error: .*usage limit", re.I | re.M)'),
("usage limit nur in der ersten Zeile", r'USAGE_LIMIT = re.compile(r"^\[[^\]]+\] Codex error: .*usage limit", re.I | re.M)', r'USAGE_LIMIT = re.compile(r"^\[[^\]]+\] Codex error: .*usage limit", re.I)'),
("erster statt letzter Block", "roh.rfind(MARKE_ANFANG)", "roh.find(MARKE_ANFANG)"),
("Schwere ungeprueft", ' if felder["schwere"] not in SCHWEREN:\n', " if False:\n"),
("Befund ohne beleg-Feld gilt", " leer = [k for k, v in felder.items() if not isinstance(v, str)]", " leer = []"),
("Suite: erste statt letzte EXIT-Zeile", "exits[-1][len(", "exits[0][len("),
("Suite: 81 statt 80 Zeilen", "SUITE_ZEILEN = 80", "SUITE_ZEILEN = 81"),
("Suite: Leerzeile am Ende mitgezaehlt", ' if zeilen[-1] == "":\n', " if False:\n"),
("Umbenennung als geaendert", '"diff", "--name-status", "--no-renames", basis', '"diff", "--name-status", basis'),
("Hinweis basis weicht fehlt", ' if vorher and vorher["stand"] != basis:\n', " if False:\n"),
("Hinweis kein Vorfahr fehlt", ' if git(projekt, "merge-base", "--is-ancestor", basis, stand, pruefen=False).returncode != 0:\n', " if False:\n"),
("Stand-Liste unsortiert in der Pruefsumme", r'"\n".join(sorted(stand_liste))', r'"\n".join(stand_liste)'),
("dateien in der Zeile", " zeile = {k: ergebnis[k] for k in PAKET_ZEILE}", " zeile = dict(ergebnis)"),
("paket_summe ueber paket.md", 'hashlib.sha256((ziel / "paket.json").read_bytes())', 'hashlib.sha256((ziel / "paket.md").read_bytes())'),
("ablegen: paket.json unlesbar ohne Befund", 'raise Befund(f"paket.json nicht lesbar in {ordner.as_posix()} ({type(fehler).__name__})")', "raise"),
("ablegen: Ziel ueberschrieben", " if ziel.exists():\n", " if False:\n"),
("ablegen: Frontmatter ungeprueft", ' if zeilen is None:\n raise Befund("bericht ohne Frontmatter")\n', ' if False:\n raise Befund("bericht ohne Frontmatter")\n'),
("ablegen: nummer ungeprueft", ' if werte.get("nummer") != str(daten["nummer"]):\n', " if False:\n"),
("ablegen: basis ungeprueft", ' if werte.get("basis") != str(daten["basis"]):\n', " if False:\n"),
("ablegen: stand PAKET im Rumpf mitgezaehlt", ' treffer = [i for i, z in enumerate(zeilen) if z == "stand: PAKET"]', r' treffer = [i - 1 for i, z in enumerate(text.split("\n")) if z == "stand: PAKET"]'),
("ablegen: stand PAKET zweimal erlaubt", " if len(treffer) != 1:\n", " if not treffer:\n"),
("ablegen: fehlendes stand PAKET erlaubt", " if len(treffer) != 1:\n", " if len(treffer) > 1:\n"),
("ablegen: Kundendokument bekommt die Pruefsumme", ' if daten["art"] == "doku":\n', " if False:\n"),
("ablegen: Ordner nicht angelegt", " ziel.parent.mkdir(parents=True, exist_ok=True)\n", ""),
("git ohne GIT_OPTIONAL_LOCKS=0", "*argumente], env=umgebung,", "*argumente],"),
("ablegen: --summe ungeprueft", " if gemessen != a.summe:\n", " if False:\n"),
("ablegen: --summe ueber paket.md", 'gemessen = hashlib.sha256((ordner / "paket.json").read_bytes())', 'gemessen = hashlib.sha256((ordner / "paket.md").read_bytes())'),
]
# Mutanten, die bei heutigem git kein Verhalten aendern koennten, stehen hier mit Grund; ein
# Ueberlebender ausserhalb dieser Liste ist ein Befund.
WIRKUNGSLOS = {}
def main() -> int:
ueberlebt, veraltet = [], []
for name, alt, neu in MUTANTEN:
with tempfile.TemporaryDirectory(ignore_cleanup_errors=True) as tmp:
ziel = Path(tmp) / "q"
ziel.mkdir()
for datei in ("quorum_werkzeug.py", "faelle_quorum_werkzeug.py"):
shutil.copy2(HIER / datei, ziel / datei)
pfad = ziel / "quorum_werkzeug.py"
text = pfad.read_text(encoding="utf-8")
if text.count(alt) != 1:
veraltet.append(name)
print(f"VERALTET {name} (Muster {text.count(alt)}-mal)")
continue
pfad.write_text(text.replace(alt, neu), encoding="utf-8", newline="\n")
lauf = subprocess.run([sys.executable, str(ziel / "faelle_quorum_werkzeug.py")], capture_output=True, text=True, encoding="utf-8", errors="replace")
rot = [z for z in lauf.stdout.splitlines() if z.startswith("FEHLER")]
if lauf.returncode == 0:
grund = WIRKUNGSLOS.get(name)
if grund:
print(f"WIRKUNGSLOS {name} ({grund})")
else:
ueberlebt.append(name)
print(f"UEBERLEBT {name}")
else:
print(f"GEFANGEN {name} ({rot[0][7:90] if rot else 'Absturz'})")
print(f"{len(MUTANTEN)} Mutanten, {len(ueberlebt)} ueberlebt, {len(veraltet)} veraltet")
return 2 if veraltet else 1 if ueberlebt else 0
if __name__ == "__main__":
sys.stdout.reconfigure(errors="replace")
sys.exit(main())