Automatischer Push: 45 Datei(en) geändert
- .claude/agents/planer.md - .claude/agents/quorum-stimme.md - .claude/skills/bookstack-upload/SKILL.md - .claude/skills/bookstack-upload/scripts/bookstack/archiv.py - .claude/skills/bookstack-upload/scripts/bookstack/auswahl.py - .claude/skills/bookstack-upload/scripts/bookstack/beschreibung.py - .claude/skills/bookstack-upload/scripts/bookstack/bilder.py - .claude/skills/bookstack-upload/scripts/bookstack/ergebnis.py - .claude/skills/bookstack-upload/scripts/bookstack/hochladen.py - .claude/skills/bookstack-upload/scripts/bookstack/konvertierung.py - .claude/skills/bookstack-upload/scripts/bookstack/struktur.py - .claude/skills/bookstack-upload/scripts/bookstack/verweise.py - … und 33 weitere Erzeugt vom PostToolUse-Hook .vault-tools/auto-push/auto_push.py. Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
1 parent
60dee0ab68
commit
deec79a6ea
45 files changed
+8645
-262
No files matched your search
@@ -0,0 +1,735 @@
|
||||
"""Fährt die Angriffsliste gegen waisen_bestand.py, je Fall auf einem Wegwerf-Vault.
|
||||
|
||||
Ersteller: Pascal Michel
|
||||
Erstellt: 2026-09-29
|
||||
Plan: docs/plans/2026-09-29-waisen/ im selben Ordner
|
||||
|
||||
Grundlinie zuerst: Der Probelauf über den unbeschädigten Wegwerf-Vault muss
|
||||
genau die erwarteten Anhänge und genau die erwartete Liste liefern und darf
|
||||
nichts ändern. Sonst misst der Rest nichts.
|
||||
|
||||
Je Fall eine frische Kopie der Grundlinie, genau eine Änderung, das Skript als
|
||||
eigener Prozess. Die Git-Fälle legen im Wegwerf-Vault ein Repo mit Zweig oder
|
||||
Arbeitsbaum an und brauchen git im PATH. Verglichen wird jede Datei des
|
||||
Wegwerf-Vaults vor und nach dem Lauf, Byte für Byte.
|
||||
|
||||
Zwei Fälle laufen über HUELLE: Sie lädt waisen_bestand.py in einen eigenen
|
||||
Prozess und setzt entweder einen fremden Schreiber ein, der nach der Sicherung
|
||||
an CHANGELOG.md anhängt, oder schreibt je git-Aufruf den Wert von
|
||||
GIT_OPTIONAL_LOCKS mit.
|
||||
|
||||
--rotlauf baut jeden Mutanten aus MUTANTEN in eine Kopie des Skripts und fährt
|
||||
die genannten Fälle dagegen. Getötet heißt: jeder genannte Fall schlägt fehl.
|
||||
|
||||
Aufruf:
|
||||
python .vault-tools/linkcheck/faelle_waisen_bestand.py
|
||||
python .vault-tools/linkcheck/faelle_waisen_bestand.py --rotlauf
|
||||
"""
|
||||
|
||||
import os
|
||||
import shutil
|
||||
import stat
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import time
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
from typing import Callable, Optional
|
||||
|
||||
HIER = Path(__file__).resolve().parent
|
||||
BESTAND = HIER / "waisen_bestand.py"
|
||||
BEGLEITER = [HIER / "waisen.py", HIER / "linkcheck.py", HIER / "waisen-ausnahmen.txt"]
|
||||
|
||||
P = "Code Projekte/active - Alpha"
|
||||
E = P + "/docs/plans/2026-01-01-eins"
|
||||
S = "IT Workspace/SEO/kunde-audit/15012026"
|
||||
X = "IT Workspace/Thema/Projekt X"
|
||||
XP = X + "/_arbeitsstand/docs/plans/2026-01-03-p"
|
||||
W = "docs/plans/2026-01-04-w"
|
||||
SPEC = "docs/specs/2026-01-04-spec.md"
|
||||
NEU = P + "/docs/plans/2026-01-09-neu"
|
||||
DUMP = "AI/secondbrain/alpha-memory/2026-01-01-thema.md"
|
||||
DUMP_OHNE = "AI/secondbrain/alpha-memory/2026-01-05-ohne.md"
|
||||
UMLAUT = "task-09-prüfung x.md"
|
||||
UMLAUT_LINK = "task-09-pr%C3%BCfung%20x.md"
|
||||
ORDNER_KOPF = "## Dateien in diesem Ordner"
|
||||
BEFUND_KOPF = "## Befunddateien dieses Laufs"
|
||||
|
||||
ZIELE = {
|
||||
P + "/CHANGELOG.md": ["Projekt: [README](README.md)"],
|
||||
P + "/docs/code/modul.md": ["Projekt: [README](../../README.md)"],
|
||||
P + "/docs/lose.md": ["Projekt: [README](../README.md)"],
|
||||
P + "/docs/plans/2026-01-02-einzel.md": ["Projekt: [README](../../README.md)"],
|
||||
E + "/00-uebersicht.md": ["Projekt: [README](../../../README.md)", "", ORDNER_KOPF, "",
|
||||
"- [task-01-a.md](task-01-a.md)", f"- [{UMLAUT}]({UMLAUT_LINK})"],
|
||||
DUMP: ["Tagesnotiz: [[Inbox/2026-01-01]]"],
|
||||
S + "/ABSCHLUSSBERICHT.md": [BEFUND_KOPF, "", f"- [[{S}/findings/a]]", f"- [[{S}/findings/b]]"],
|
||||
X + "/_arbeitsstand/notiz.md": [f"Projekt: [[{X}/_MOC-projekt-x]]"],
|
||||
XP + "/00-uebersicht.md": [f"Projekt: [[{X}/_MOC-projekt-x]]", "", ORDNER_KOPF, "",
|
||||
"- [task-01-t.md](task-01-t.md)"],
|
||||
W + "/00-uebersicht.md": ["Vault: [README](../../../README.md)", "", ORDNER_KOPF, "",
|
||||
"- [task-01-w.md](task-01-w.md)"],
|
||||
SPEC: ["Vault: [README](../../README.md)"],
|
||||
}
|
||||
REST = {
|
||||
P + "/notizen.md": "keine Regel passt",
|
||||
DUMP_OHNE: "Tagesnotiz Inbox/2026-01-05.md fehlt",
|
||||
W + "/befunde/b1.md": "Plandatei tiefer als ein Ordner unter docs/plans",
|
||||
}
|
||||
|
||||
HUELLE = '''"""Huelle aus faelle_waisen_bestand.py: laedt waisen_bestand.py und biegt zwei Stellen um."""
|
||||
import importlib.util
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
skript, art, *rest = sys.argv[1:]
|
||||
# Die Variable darf nicht schon von aussen kommen, sonst misst der Fall die Shell.
|
||||
os.environ.pop("GIT_OPTIONAL_LOCKS", None)
|
||||
spec = importlib.util.spec_from_file_location("waisen_bestand_huelle", skript)
|
||||
modul = importlib.util.module_from_spec(spec)
|
||||
# Vor exec_module eintragen: dataclasses sucht das Modul in sys.modules.
|
||||
sys.modules["waisen_bestand_huelle"] = modul
|
||||
spec.loader.exec_module(modul)
|
||||
aufrufe = []
|
||||
echt_run = subprocess.run
|
||||
echt_copy2 = modul.shutil.copy2
|
||||
|
||||
|
||||
def run(befehl, *a, **k):
|
||||
# Git-Double: merkt sich je git-Aufruf den Wert von GIT_OPTIONAL_LOCKS.
|
||||
if befehl and befehl[0] == "git":
|
||||
aufrufe.append((k.get("env") or os.environ).get("GIT_OPTIONAL_LOCKS"))
|
||||
return echt_run(befehl, *a, **k)
|
||||
|
||||
|
||||
def copy2(quelle, ziel, *a, **k):
|
||||
# Fremder Schreiber: haengt nach der Sicherung von CHANGELOG.md etwas an.
|
||||
ergebnis = echt_copy2(quelle, ziel, *a, **k)
|
||||
if art == "fremd" and Path(quelle).name == "CHANGELOG.md":
|
||||
with open(quelle, "ab") as datei:
|
||||
datei.write(b"fremd")
|
||||
return ergebnis
|
||||
|
||||
|
||||
subprocess.run = run
|
||||
modul.shutil.copy2 = copy2
|
||||
sys.stdout.reconfigure(encoding="utf-8")
|
||||
code = modul.main(rest)
|
||||
ohne = [wert for wert in aufrufe if wert != "0"]
|
||||
if aufrufe and not ohne:
|
||||
print("HUELLE git ok")
|
||||
else:
|
||||
print(f"HUELLE git: {len(aufrufe)} Aufrufe, {len(ohne)} ohne GIT_OPTIONAL_LOCKS=0")
|
||||
raise SystemExit(code)
|
||||
'''
|
||||
|
||||
|
||||
def schreibe(v: Path, rel: str, inhalt) -> None:
|
||||
# Immer Bytes: write_text würde unter Windows jedes LF zu CRLF machen.
|
||||
pfad = v / rel
|
||||
pfad.parent.mkdir(parents=True, exist_ok=True)
|
||||
pfad.write_bytes(inhalt if isinstance(inhalt, bytes) else inhalt.encode("utf-8"))
|
||||
|
||||
|
||||
def haenge_an(v: Path, rel: str, inhalt: bytes) -> None:
|
||||
with open(v / rel, "ab") as datei:
|
||||
datei.write(inhalt)
|
||||
|
||||
|
||||
def _schreibbar(funktion, pfad, _info) -> None:
|
||||
# Git legt Objektdateien schreibgeschützt an; rmtree scheitert sonst unter Windows.
|
||||
os.chmod(pfad, stat.S_IWRITE)
|
||||
funktion(pfad)
|
||||
|
||||
|
||||
def entferne(pfad: Path) -> None:
|
||||
if not pfad.exists():
|
||||
return
|
||||
if sys.version_info >= (3, 12):
|
||||
shutil.rmtree(pfad, onexc=_schreibbar)
|
||||
else:
|
||||
shutil.rmtree(pfad, onerror=_schreibbar)
|
||||
|
||||
|
||||
def nichts(v: Path) -> None:
|
||||
return None
|
||||
|
||||
|
||||
def baue_grundlinie(v: Path) -> None:
|
||||
schreibe(v, ".obsidian/app.json", '{"userIgnoreFilters": ["_index/"]}')
|
||||
schreibe(v, "Wissen/start.md",
|
||||
f"[[{P}/README]]\n[[IT Workspace/SEO/_MOC-seo]]\n[[{X}/_MOC-projekt-x]]\n[[README]]\n")
|
||||
schreibe(v, "README.md", "# Vault\n")
|
||||
schreibe(v, P + "/README.md", "# Alpha\n")
|
||||
schreibe(v, P + "/CHANGELOG.md", "# Changelog")
|
||||
schreibe(v, P + "/notizen.md", "# Notizen\n")
|
||||
schreibe(v, P + "/data/2026-01-01.md", "# Auswertung\n")
|
||||
schreibe(v, P + "/docs/code/modul.md", b"# Modul\r\n\r\nText\r\n")
|
||||
schreibe(v, P + "/docs/lose.md", "# Lose\n")
|
||||
schreibe(v, P + "/docs/plans/2026-01-02-einzel.md", "# Einzel\n")
|
||||
schreibe(v, E + "/00-uebersicht.md", "# Eins\n")
|
||||
schreibe(v, E + "/task-01-a.md", "# A\n")
|
||||
schreibe(v, E + "/" + UMLAUT, "# Z\n")
|
||||
schreibe(v, DUMP, "# Dump\n")
|
||||
schreibe(v, DUMP_OHNE, "# Dump\n")
|
||||
schreibe(v, "Inbox/2026-01-01.md", "# Tag\n")
|
||||
schreibe(v, "IT Workspace/SEO/_MOC-seo.md", f"[[{S}/ABSCHLUSSBERICHT]]\n")
|
||||
schreibe(v, S + "/ABSCHLUSSBERICHT.md", "# Bericht\n")
|
||||
schreibe(v, S + "/findings/a.md", "# A\n")
|
||||
schreibe(v, S + "/findings/b.md", "# B\n")
|
||||
schreibe(v, X + "/_MOC-projekt-x.md", "# X\n")
|
||||
schreibe(v, X + "/_arbeitsstand/notiz.md", "# N\n")
|
||||
schreibe(v, XP + "/00-uebersicht.md", "# P\n")
|
||||
schreibe(v, XP + "/task-01-t.md", "# T\n")
|
||||
schreibe(v, W + "/00-uebersicht.md", "# W\n")
|
||||
schreibe(v, W + "/task-01-w.md", "# T\n")
|
||||
schreibe(v, W + "/befunde/b1.md", "# B\n")
|
||||
schreibe(v, SPEC, "# Spec\n")
|
||||
schreibe(v, "Bookstack/archiv/seite.md", "# S\n")
|
||||
schreibe(v, "_index/INDEX.md", "# I\n")
|
||||
|
||||
|
||||
def git(ordner: Path, *argumente: str) -> None:
|
||||
lauf = subprocess.run(
|
||||
["git", "-C", str(ordner), "-c", "user.name=Faelle", "-c", "[email protected]",
|
||||
"-c", "commit.gpgsign=false", *argumente],
|
||||
capture_output=True,
|
||||
stdin=subprocess.DEVNULL,
|
||||
)
|
||||
if lauf.returncode != 0:
|
||||
raise RuntimeError(f"git {' '.join(argumente)}: {lauf.stderr.decode('utf-8', errors='replace')}")
|
||||
|
||||
|
||||
def repo_anlegen(v: Path, ohne: Optional[str] = None) -> Path:
|
||||
repo = v / P
|
||||
git(repo, "init", "-q", "-b", "main")
|
||||
git(repo, "config", "core.autocrlf", "false")
|
||||
git(repo, "add", "-A")
|
||||
if ohne:
|
||||
git(repo, "rm", "-q", "--cached", ohne)
|
||||
git(repo, "commit", "-q", "-m", "Stand")
|
||||
return repo
|
||||
|
||||
|
||||
def zweig_aendert(v: Path) -> None:
|
||||
repo = repo_anlegen(v)
|
||||
git(repo, "checkout", "-q", "-b", "neben")
|
||||
schreibe(v, E + "/00-uebersicht.md", "# Eins auf dem Zweig\n")
|
||||
git(repo, "commit", "-q", "-am", "Zweig")
|
||||
git(repo, "checkout", "-q", "main")
|
||||
|
||||
|
||||
def arbeitsbaum_aendert(v: Path) -> None:
|
||||
repo = repo_anlegen(v)
|
||||
baum = v / ".arbeitsbaeume" / "wb"
|
||||
git(repo, "worktree", "add", "-q", "-b", "wb", str(baum))
|
||||
haenge_an(baum, "docs/plans/2026-01-01-eins/00-uebersicht.md", b"offen\n")
|
||||
|
||||
|
||||
def eigener_baum_aendert(v: Path) -> None:
|
||||
repo_anlegen(v)
|
||||
haenge_an(v, E + "/00-uebersicht.md", b"offen\n")
|
||||
|
||||
|
||||
def nie_committet(v: Path) -> None:
|
||||
repo_anlegen(v, ohne="CHANGELOG.md")
|
||||
|
||||
|
||||
def sauberes_repo(v: Path) -> None:
|
||||
repo_anlegen(v)
|
||||
|
||||
|
||||
def neuer_planordner(v: Path) -> None:
|
||||
# Ganz ungetrackter Ordner: im Modus normal nennt git nur den Ordner.
|
||||
repo_anlegen(v)
|
||||
schreibe(v, NEU + "/00-uebersicht.md", "# Neu\n")
|
||||
schreibe(v, NEU + "/task-01-x.md", "# X\n")
|
||||
|
||||
|
||||
def umbenannt_im_index(v: Path) -> None:
|
||||
# Der alte Pfad ab/CHANGELOG.md ohne seine ersten drei Zeichen wäre CHANGELOG.md.
|
||||
schreibe(v, P + "/ab/CHANGELOG.md", "# Alt\n")
|
||||
repo = repo_anlegen(v)
|
||||
git(repo, "mv", "ab/CHANGELOG.md", "ab/neu.md")
|
||||
# Ohne erkannte Umbenennung misst der Fall nichts.
|
||||
status = subprocess.run(["git", "-C", str(repo), "status", "--porcelain"],
|
||||
capture_output=True, stdin=subprocess.DEVNULL).stdout
|
||||
if not status.startswith(b"R "):
|
||||
raise RuntimeError(f"git meldet keine Umbenennung: {status!r}")
|
||||
|
||||
|
||||
def wurzel_repo(v: Path, ignorieren: str) -> None:
|
||||
schreibe(v, ".gitignore", ignorieren)
|
||||
git(v, "init", "-q", "-b", "main")
|
||||
git(v, "config", "core.autocrlf", "false")
|
||||
git(v, "add", ".gitignore")
|
||||
git(v, "commit", "-q", "-m", "Stand")
|
||||
|
||||
|
||||
def wurzel_ignoriert_alles(v: Path) -> None:
|
||||
# Wie die Positivliste der Vault-Wurzel: alles ignoriert außer .gitignore.
|
||||
wurzel_repo(v, "/*\n!/.gitignore\n")
|
||||
|
||||
|
||||
def wurzel_docs_offen(v: Path) -> None:
|
||||
# docs/ ist nicht ignoriert, seine Dateien sind also neu im Repo der Wurzel.
|
||||
wurzel_repo(v, "/*\n!/.gitignore\n!/docs/\n")
|
||||
|
||||
|
||||
def abbild(v: Path) -> dict:
|
||||
ergebnis = {}
|
||||
for pfad in sorted(v.rglob("*")):
|
||||
rel = pfad.relative_to(v)
|
||||
if ".git" in rel.parts or not pfad.is_file():
|
||||
continue
|
||||
ergebnis[rel.as_posix()] = pfad.read_bytes()
|
||||
return ergebnis
|
||||
|
||||
|
||||
@dataclass
|
||||
class Fall:
|
||||
name: str
|
||||
aendern: Callable = nichts
|
||||
# probe, anwenden, zweimal, ohne_sicherung, sicherung_da, sicherung_im_vault, anwenden_abbruch, anwenden_fremd
|
||||
modus: str = "probe"
|
||||
code: int = 0
|
||||
ziele: Optional[dict] = None
|
||||
ziel_zeilen: dict = field(default_factory=dict)
|
||||
mit_ziel: list = field(default_factory=list)
|
||||
ohne_ziel: list = field(default_factory=list)
|
||||
rest: Optional[dict] = None
|
||||
mit_rest: dict = field(default_factory=dict)
|
||||
ueber: dict = field(default_factory=dict)
|
||||
texte: list = field(default_factory=list)
|
||||
nachher: dict = field(default_factory=dict)
|
||||
# leer: Skript direkt; fremd oder sperre: über HUELLE
|
||||
huelle: str = ""
|
||||
|
||||
|
||||
GRUNDLINIE = Fall("GRUNDLINIE", ziele=ZIELE, rest=REST,
|
||||
texte=["Waisen außerhalb der Ausnahmen vorher: 20",
|
||||
"Durch Anhänge mitverbunden, ohne eigene Regel: 1"])
|
||||
|
||||
FAELLE = [
|
||||
Fall("anwenden | schreibt genau die erwarteten Bytes", modus="anwenden", ziele=ZIELE, rest=REST,
|
||||
texte=["Waisen außerhalb der Ausnahmen nachher: 3"],
|
||||
nachher={
|
||||
P + "/CHANGELOG.md": b"# Changelog\n\nProjekt: [README](README.md)\n",
|
||||
P + "/docs/code/modul.md": b"# Modul\r\n\r\nText\r\n\r\nProjekt: [README](../../README.md)\r\n",
|
||||
E + "/00-uebersicht.md": (
|
||||
"# Eins\n\nProjekt: [README](../../../README.md)\n\n## Dateien in diesem Ordner\n\n"
|
||||
"- [task-01-a.md](task-01-a.md)\n- [task-09-prüfung x.md](task-09-pr%C3%BCfung%20x.md)\n"
|
||||
).encode("utf-8"),
|
||||
}),
|
||||
Fall("anwenden | zweiter Lauf findet nichts mehr", modus="zweimal", ziele=ZIELE, rest=REST),
|
||||
Fall("anwenden | ohne --sicherung bricht ab", modus="ohne_sicherung", code=2,
|
||||
texte=["braucht --sicherung"]),
|
||||
Fall("anwenden | Sicherungsordner existiert schon", modus="sicherung_da", code=2,
|
||||
texte=["existiert schon"]),
|
||||
Fall("anwenden | Sicherungsordner im Vault", modus="sicherung_im_vault", code=2,
|
||||
texte=["liegt im Vault"]),
|
||||
Fall("anwenden | Befund des Zaehlers bricht ohne Schreiben ab",
|
||||
lambda v: entferne(v / ".obsidian"), modus="anwenden_abbruch", code=2,
|
||||
texte=["Bestandslauf - ABGEBROCHEN", "Ordner .obsidian fehlt"]),
|
||||
Fall("anwenden | fremder Schreiber zwischen Lesen und Anhaengen bricht ab", modus="anwenden_fremd",
|
||||
huelle="fremd", code=2,
|
||||
texte=["hat sich seit dem Lesen geändert", "Vor dem Abbruch schon angehängt (1)"],
|
||||
nachher={
|
||||
DUMP: b"# Dump\n\nTagesnotiz: [[Inbox/2026-01-01]]\n",
|
||||
P + "/CHANGELOG.md": b"# Changelogfremd",
|
||||
}),
|
||||
Fall("zeilenende | gemischte Zeilenenden werden uebersprungen",
|
||||
lambda v: schreibe(v, P + "/docs/code/modul.md", b"# Modul\r\nText\n"),
|
||||
ueber={P + "/docs/code/modul.md": "gemischte Zeilenenden"}, ohne_ziel=[P + "/docs/code/modul.md"]),
|
||||
Fall("kodierung | kein UTF-8 wird uebersprungen",
|
||||
lambda v: schreibe(v, P + "/docs/plans/2026-01-02-einzel.md", b"# Einzel \xff\n"),
|
||||
ueber={P + "/docs/plans/2026-01-02-einzel.md": "nicht als UTF-8 lesbar"}),
|
||||
Fall("git | anderer Zweig hat die Uebersicht geaendert", zweig_aendert,
|
||||
ueber={E + "/00-uebersicht.md": "Zweig neben"}, mit_ziel=[P + "/CHANGELOG.md"]),
|
||||
Fall("git | anderer Arbeitsbaum hat die Uebersicht uncommittet geaendert", arbeitsbaum_aendert,
|
||||
ueber={E + "/00-uebersicht.md": "Arbeitsbaum"}, mit_ziel=[P + "/CHANGELOG.md"]),
|
||||
Fall("git | Uebersicht im eigenen Arbeitsbaum geaendert", eigener_baum_aendert,
|
||||
ueber={E + "/00-uebersicht.md": "Arbeitsbaum"}),
|
||||
Fall("git | nie committete Datei zaehlt als geaendert", nie_committet,
|
||||
ueber={P + "/CHANGELOG.md": "Arbeitsbaum"}),
|
||||
Fall("git | sauberes Repo ohne Zweig wird angehaengt", sauberes_repo, ziele=ZIELE, rest=REST),
|
||||
Fall("git | ungetrackter Planordner zaehlt je Datei", neuer_planordner,
|
||||
ueber={NEU + "/00-uebersicht.md": "Arbeitsbaum"}, mit_ziel=[P + "/CHANGELOG.md"]),
|
||||
Fall("git | Umbenennung: alter Pfad ist kein eigener Eintrag", umbenannt_im_index,
|
||||
mit_ziel=[P + "/CHANGELOG.md"]),
|
||||
Fall("git | jeder git-Aufruf mit GIT_OPTIONAL_LOCKS=0", sauberes_repo, huelle="sperre",
|
||||
ziele=ZIELE, rest=REST, texte=["HUELLE git ok"]),
|
||||
Fall("git | Vault-Wurzel als Repo, ignorierte Dateien zaehlen nicht", wurzel_ignoriert_alles,
|
||||
ziele=ZIELE, rest=REST),
|
||||
Fall("git | Vault-Wurzel als Repo, docs/ nicht ignoriert", wurzel_docs_offen,
|
||||
ueber={W + "/00-uebersicht.md": "Arbeitsbaum", SPEC: "Arbeitsbaum"},
|
||||
mit_ziel=[DUMP, P + "/CHANGELOG.md"]),
|
||||
Fall("regel | Planordner ohne 00-uebersicht.md",
|
||||
lambda v: (v / E / "00-uebersicht.md").unlink(),
|
||||
mit_rest={E + "/task-01-a.md": "Planordner ohne 00-uebersicht.md",
|
||||
E + "/" + UMLAUT: "Planordner ohne 00-uebersicht.md"}),
|
||||
Fall("regel | Planordner nur mit Uebersicht",
|
||||
lambda v: ((v / E / "task-01-a.md").unlink(), (v / E / UMLAUT).unlink()),
|
||||
ziel_zeilen={E + "/00-uebersicht.md": ["Projekt: [README](../../../README.md)"]}),
|
||||
Fall("regel | Projekt ohne README",
|
||||
lambda v: (v / P / "README.md").unlink(),
|
||||
mit_rest={P + "/CHANGELOG.md": "README.md fehlt", E + "/task-01-a.md": "README.md fehlt"},
|
||||
ohne_ziel=[P + "/CHANGELOG.md", E + "/00-uebersicht.md"]),
|
||||
Fall("regel | Befunde ohne Bericht",
|
||||
lambda v: (v / S / "ABSCHLUSSBERICHT.md").unlink(),
|
||||
mit_rest={S + "/findings/a.md": "kein Bericht", S + "/findings/b.md": "kein Bericht"}),
|
||||
Fall("regel | FULL-AUDIT-REPORT als Bericht",
|
||||
lambda v: (v / S / "ABSCHLUSSBERICHT.md").rename(v / S / "FULL-AUDIT-REPORT.md"),
|
||||
ziel_zeilen={S + "/FULL-AUDIT-REPORT.md": [BEFUND_KOPF, "", f"- [[{S}/findings/a]]",
|
||||
f"- [[{S}/findings/b]]"]},
|
||||
ohne_ziel=[S + "/ABSCHLUSSBERICHT.md"]),
|
||||
Fall("regel | zwei MOC-Dateien ueber _arbeitsstand",
|
||||
lambda v: schreibe(v, X + "/_MOC-zwei.md", "# Z\n"),
|
||||
mit_rest={X + "/_arbeitsstand/notiz.md": "2 _MOC-Dateien"}),
|
||||
Fall("regel | Abschnitt schon vorhanden",
|
||||
lambda v: schreibe(v, E + "/00-uebersicht.md", "# Eins\n\n## Dateien in diesem Ordner\n\n- alt\n"),
|
||||
ueber={E + "/00-uebersicht.md": "enthält schon"}),
|
||||
Fall("regel | Uebersicht verlinkt das README schon",
|
||||
lambda v: schreibe(v, E + "/00-uebersicht.md", "# Eins\n\n[README](../../../README.md)\n"),
|
||||
ziel_zeilen={E + "/00-uebersicht.md": [ORDNER_KOPF, "", "- [task-01-a.md](task-01-a.md)",
|
||||
f"- [{UMLAUT}]({UMLAUT_LINK})"]}),
|
||||
Fall("regel | Dump tiefer als ein Ordner",
|
||||
lambda v: schreibe(v, "AI/secondbrain/alpha-memory/drehbuecher/2026-01-01-x.md", "# D\n"),
|
||||
mit_rest={"AI/secondbrain/alpha-memory/drehbuecher/2026-01-01-x.md": "keine Regel passt"}),
|
||||
Fall("regel | Vault-Wurzel ohne README",
|
||||
lambda v: (v / "README.md").unlink(),
|
||||
mit_rest={W + "/task-01-w.md": "README.md der Vault-Wurzel fehlt",
|
||||
SPEC: "README.md der Vault-Wurzel fehlt"},
|
||||
ohne_ziel=[W + "/00-uebersicht.md", SPEC]),
|
||||
Fall("regel | Wurzel-Planordner ohne 00-uebersicht.md",
|
||||
lambda v: (v / W / "00-uebersicht.md").unlink(),
|
||||
mit_rest={W + "/task-01-w.md": "Planordner ohne 00-uebersicht.md"}),
|
||||
]
|
||||
|
||||
ALLE = [GRUNDLINIE] + FAELLE
|
||||
|
||||
# (Name, Zeile in waisen_bestand.py, Ersatzzeile, Fälle, die rot werden müssen)
|
||||
MUTANTEN = [
|
||||
("Zeilenende immer LF",
|
||||
r' ende = "\r\n" if crlf else "\n"',
|
||||
r' ende = "\n"',
|
||||
["anwenden | schreibt genau die erwarteten Bytes"]),
|
||||
("Git-Pruefung aus",
|
||||
" repo = repo_von(vault, rel)",
|
||||
" repo = None",
|
||||
["git | anderer Zweig hat die Uebersicht geaendert",
|
||||
"git | anderer Arbeitsbaum hat die Uebersicht uncommittet geaendert",
|
||||
"git | Uebersicht im eigenen Arbeitsbaum geaendert",
|
||||
"git | nie committete Datei zaehlt als geaendert"]),
|
||||
("vorhandene Zeile nicht erkannt",
|
||||
" if zeile in vorhanden:",
|
||||
" if False:",
|
||||
["regel | Abschnitt schon vorhanden"]),
|
||||
("Sicherung leer",
|
||||
" shutil.copy2(vault / ziel.rel, kopie)",
|
||||
" kopie.write_bytes(b\"\")",
|
||||
["anwenden | schreibt genau die erwarteten Bytes"]),
|
||||
("Liste nicht gefiltert",
|
||||
" rest = [(rel, grund) for rel, grund in rest if rel not in verbunden]",
|
||||
" rest = list(rest)",
|
||||
["GRUNDLINIE"]),
|
||||
("Sicherung im Vault erlaubt",
|
||||
" if sicherung_pfad == vault or vault in sicherung_pfad.parents:",
|
||||
" if False:",
|
||||
["anwenden | Sicherungsordner im Vault"]),
|
||||
("fremde Aenderung vor dem Anhaengen nicht erkannt",
|
||||
" if pfad.read_bytes() != alt:",
|
||||
" if False:",
|
||||
["anwenden | fremder Schreiber zwischen Lesen und Anhaengen bricht ab"]),
|
||||
("GEAENDERT.txt unter anderem Namen",
|
||||
r' (sicherung / "GEAENDERT.txt").write_text("\n".join(GESCHRIEBEN) + "\n", encoding="utf-8", newline="\n")',
|
||||
r' (sicherung / "ANDERS.txt").write_text("\n".join(GESCHRIEBEN) + "\n", encoding="utf-8", newline="\n")',
|
||||
["anwenden | schreibt genau die erwarteten Bytes"]),
|
||||
("ungetrackte Ordner nur als Ordner",
|
||||
' roh = git(Path(baum), "status", "--porcelain", "-z", "--untracked-files=all")',
|
||||
' roh = git(Path(baum), "status", "--porcelain", "-z", "--untracked-files=normal")',
|
||||
["git | ungetrackter Planordner zaehlt je Datei"]),
|
||||
("ignorierte Dateien zaehlen als geaendert",
|
||||
' roh = git(Path(baum), "status", "--porcelain", "-z", "--untracked-files=all")',
|
||||
' roh = git(Path(baum), "status", "--porcelain", "-z", "--untracked-files=all", "--ignored")',
|
||||
["git | Vault-Wurzel als Repo, ignorierte Dateien zaehlen nicht"]),
|
||||
("Umbenennung als zwei Eintraege gelesen",
|
||||
' if "R" in eintrag[:2] or "C" in eintrag[:2]:',
|
||||
" if False:",
|
||||
["git | Umbenennung: alter Pfad ist kein eigener Eintrag"]),
|
||||
("git ohne GIT_OPTIONAL_LOCKS",
|
||||
' umgebung["GIT_OPTIONAL_LOCKS"] = "0"',
|
||||
" pass",
|
||||
["git | jeder git-Aufruf mit GIT_OPTIONAL_LOCKS=0"]),
|
||||
("Vault-Wurzel nicht als Repo geprueft",
|
||||
" while ordner == vault or vault in ordner.parents:",
|
||||
" while ordner != vault and vault in ordner.parents:",
|
||||
["git | Vault-Wurzel als Repo, docs/ nicht ignoriert"]),
|
||||
("docs/ an der Wurzel ohne Regel",
|
||||
' if teile[0] == "docs" and len(teile) >= 2:',
|
||||
" if False:",
|
||||
["GRUNDLINIE"]),
|
||||
("Wurzel-README nicht geprueft",
|
||||
" if not existiert(vault, hub):",
|
||||
" if False:",
|
||||
["regel | Vault-Wurzel ohne README"]),
|
||||
("Wurzel-Planordner ohne Uebersicht nicht erkannt",
|
||||
" if not existiert(vault, kopf):",
|
||||
" if False:",
|
||||
["regel | Wurzel-Planordner ohne 00-uebersicht.md"]),
|
||||
("Wurzel-Plandatei zu tief angehaengt",
|
||||
' if teile[1] == "plans" and len(teile) > 4:',
|
||||
" if False:",
|
||||
["GRUNDLINIE"]),
|
||||
("Datei direkt unter docs/ ohne Regel",
|
||||
' if innen == ["CHANGELOG.md"] or (innen[:1] == ["docs"] and len(innen) >= 2):',
|
||||
' if innen == ["CHANGELOG.md"] or (innen[:1] == ["docs"] and len(innen) >= 3):',
|
||||
["GRUNDLINIE"]),
|
||||
]
|
||||
|
||||
|
||||
def streng_utf8(roh: bytes) -> str:
|
||||
try:
|
||||
return roh.decode("utf-8")
|
||||
except UnicodeDecodeError as exc:
|
||||
return f"UNGUELTIGES-UTF-8: {exc}"
|
||||
|
||||
|
||||
def fahre(skript: Path, argumente: list) -> tuple:
|
||||
start = time.perf_counter()
|
||||
lauf = subprocess.run(
|
||||
[sys.executable, str(skript), *argumente],
|
||||
capture_output=True,
|
||||
stdin=subprocess.DEVNULL,
|
||||
timeout=300,
|
||||
)
|
||||
dauer = time.perf_counter() - start
|
||||
return lauf.returncode, streng_utf8(lauf.stdout), lauf.stderr.decode("utf-8", errors="replace"), dauer
|
||||
|
||||
|
||||
def lies_ausgabe(out: str) -> tuple:
|
||||
ziele, rest, ueber = {}, {}, {}
|
||||
aktuell = None
|
||||
for zeile in out.replace("\r\n", "\n").split("\n"):
|
||||
if zeile.startswith("ZIEL "):
|
||||
aktuell = zeile[5:].split(" [", 1)[0]
|
||||
ziele[aktuell] = []
|
||||
elif zeile.startswith(" | ") and aktuell is not None:
|
||||
ziele[aktuell].append(zeile[6:])
|
||||
else:
|
||||
aktuell = None
|
||||
if zeile.startswith("REST "):
|
||||
rel, _, grund = zeile[5:].partition(": ")
|
||||
rest[rel] = grund
|
||||
elif zeile.startswith("UEBERSPRUNGEN "):
|
||||
rel, _, grund = zeile[14:].partition(": ")
|
||||
ueber[rel] = grund
|
||||
return ziele, rest, ueber
|
||||
|
||||
|
||||
def bewerte(fall: Fall, code: int, out: str, v: Path, vorher: dict, sicherung: Path) -> list:
|
||||
probleme = []
|
||||
if code != fall.code:
|
||||
probleme.append(f"Exit {code} statt {fall.code}")
|
||||
ziele, rest, ueber = lies_ausgabe(out)
|
||||
if fall.ziele is not None:
|
||||
if sorted(ziele) != sorted(fall.ziele):
|
||||
probleme.append(f"Ziele {sorted(ziele)} statt {sorted(fall.ziele)}")
|
||||
for rel, zeilen in fall.ziele.items():
|
||||
if rel in ziele and ziele[rel] != zeilen:
|
||||
probleme.append(f"{rel}: Zeilen {ziele[rel]!r} statt {zeilen!r}")
|
||||
for rel, zeilen in fall.ziel_zeilen.items():
|
||||
if ziele.get(rel) != zeilen:
|
||||
probleme.append(f"{rel}: Zeilen {ziele.get(rel)!r} statt {zeilen!r}")
|
||||
for rel in fall.mit_ziel:
|
||||
if rel not in ziele:
|
||||
probleme.append(f"Ziel fehlt: {rel}")
|
||||
for rel in fall.ohne_ziel:
|
||||
if rel in ziele:
|
||||
probleme.append(f"Ziel unerwartet: {rel}")
|
||||
if fall.rest is not None and sorted(rest) != sorted(fall.rest):
|
||||
probleme.append(f"Liste {sorted(rest)} statt {sorted(fall.rest)}")
|
||||
for rel, teil in list((fall.rest or {}).items()) + list(fall.mit_rest.items()):
|
||||
if rel not in rest:
|
||||
probleme.append(f"nicht auf der Liste: {rel}")
|
||||
elif teil not in rest[rel]:
|
||||
probleme.append(f"Liste {rel}: {rest[rel]!r} ohne {teil!r}")
|
||||
for rel, teil in fall.ueber.items():
|
||||
if rel not in ueber:
|
||||
probleme.append(f"nicht uebersprungen: {rel}")
|
||||
elif teil not in ueber[rel]:
|
||||
probleme.append(f"uebersprungen {rel}: {ueber[rel]!r} ohne {teil!r}")
|
||||
for text in fall.texte:
|
||||
if text not in out:
|
||||
probleme.append(f"fehlt in der Ausgabe: {text}")
|
||||
nachher = abbild(v)
|
||||
geaendert = sorted(r for r in set(vorher) | set(nachher) if vorher.get(r) != nachher.get(r))
|
||||
if fall.modus in ("anwenden", "zweimal"):
|
||||
erwartet = sorted(fall.ziele or {})
|
||||
if geaendert != erwartet:
|
||||
probleme.append(f"geaendert {geaendert} statt {erwartet}")
|
||||
liste = sicherung / "GEAENDERT.txt"
|
||||
if not liste.is_file() or liste.read_bytes() != ("\n".join(erwartet) + "\n").encode("utf-8"):
|
||||
probleme.append(f"GEAENDERT.txt fehlt oder nennt nicht genau {erwartet}")
|
||||
for rel in erwartet:
|
||||
kopie = sicherung / rel
|
||||
if not kopie.is_file() or kopie.read_bytes() != vorher.get(rel):
|
||||
probleme.append(f"Sicherung fehlt oder weicht ab: {rel}")
|
||||
if rel in fall.nachher or rel not in nachher:
|
||||
continue
|
||||
ende = "\r\n" if b"\r\n" in vorher.get(rel, b"") else "\n"
|
||||
schwanz = (ende.join(fall.ziele[rel]) + ende).encode("utf-8")
|
||||
if not nachher[rel].startswith(vorher.get(rel, b"")) or not nachher[rel].endswith(schwanz):
|
||||
probleme.append(f"{rel}: nicht genau alter Inhalt plus Anhang")
|
||||
for rel, soll in fall.nachher.items():
|
||||
if nachher.get(rel) != soll:
|
||||
probleme.append(f"{rel}: {nachher.get(rel)!r} statt {soll!r}")
|
||||
elif fall.modus == "anwenden_fremd":
|
||||
erwartet = sorted(fall.nachher)
|
||||
if geaendert != erwartet:
|
||||
probleme.append(f"geaendert {geaendert} statt {erwartet}")
|
||||
for rel, soll in fall.nachher.items():
|
||||
if nachher.get(rel) != soll:
|
||||
probleme.append(f"{rel}: {nachher.get(rel)!r} statt {soll!r}")
|
||||
elif geaendert:
|
||||
probleme.append(f"hat Dateien geaendert: {geaendert}")
|
||||
if fall.modus == "anwenden_abbruch" and sicherung.exists():
|
||||
probleme.append("Sicherungsordner trotz Abbruch angelegt")
|
||||
return probleme
|
||||
|
||||
|
||||
def lauf_fall(fall: Fall, basis: Path, arbeit: Path, skript: Path) -> tuple:
|
||||
v = arbeit / "fall"
|
||||
entferne(v)
|
||||
for alt in arbeit.glob("sicherung*"):
|
||||
entferne(alt)
|
||||
shutil.copytree(basis, v)
|
||||
fall.aendern(v)
|
||||
vorher = abbild(v)
|
||||
sicherung = arbeit / "sicherung"
|
||||
argumente = ["--vault", str(v)]
|
||||
if fall.modus in ("anwenden", "zweimal", "anwenden_abbruch", "anwenden_fremd"):
|
||||
argumente += ["--anwenden", "--sicherung", str(sicherung)]
|
||||
elif fall.modus == "ohne_sicherung":
|
||||
argumente += ["--anwenden"]
|
||||
elif fall.modus == "sicherung_da":
|
||||
sicherung.mkdir()
|
||||
argumente += ["--anwenden", "--sicherung", str(sicherung)]
|
||||
elif fall.modus == "sicherung_im_vault":
|
||||
argumente += ["--anwenden", "--sicherung", str(v / "Sicherung")]
|
||||
ausfuehren = skript
|
||||
if fall.huelle:
|
||||
ausfuehren = arbeit / "huelle.py"
|
||||
ausfuehren.write_bytes(HUELLE.encode("utf-8"))
|
||||
argumente = [str(skript), fall.huelle] + argumente
|
||||
code, out, err, dauer = fahre(ausfuehren, argumente)
|
||||
probleme = bewerte(fall, code, out, v, vorher, sicherung)
|
||||
if fall.modus == "zweimal" and not probleme:
|
||||
erster = abbild(v)
|
||||
code2, out2, err2, _ = fahre(skript, ["--vault", str(v), "--anwenden", "--sicherung",
|
||||
str(arbeit / "sicherung-2")])
|
||||
ziele2, _, _ = lies_ausgabe(out2)
|
||||
if code2 != 0:
|
||||
probleme.append(f"zweiter Lauf Exit {code2}")
|
||||
if ziele2:
|
||||
probleme.append(f"zweiter Lauf will anhaengen: {sorted(ziele2)}")
|
||||
if abbild(v) != erster:
|
||||
probleme.append("zweiter Lauf hat Dateien geaendert")
|
||||
out, err = out + out2, err + err2
|
||||
kopf = f"exit={code} {dauer:.2f}s"
|
||||
if not probleme:
|
||||
return True, kopf
|
||||
text = kopf + "\n" + "\n".join(" " + p for p in probleme)
|
||||
return False, text + f"\n stdout: {out[:2000]!r}\n stderr: {err[:800]!r}"
|
||||
|
||||
|
||||
def fahre_liste(faelle: list, skript: Path, arbeit: Path, ausgabe: bool) -> dict:
|
||||
basis = arbeit / "grundlinie"
|
||||
if not basis.exists():
|
||||
baue_grundlinie(basis)
|
||||
ergebnis = {}
|
||||
for fall in faelle:
|
||||
try:
|
||||
ok, text = lauf_fall(fall, basis, arbeit, skript)
|
||||
except Exception as exc:
|
||||
ok, text = False, f"ABSTURZ im Treiber: {type(exc).__name__}: {exc}"
|
||||
ergebnis[fall.name] = ok
|
||||
if ausgabe:
|
||||
print(f"{'BESTANDEN' if ok else 'FEHLER':10} {fall.name} {text}")
|
||||
if fall is GRUNDLINIE and not ok:
|
||||
break
|
||||
return ergebnis
|
||||
|
||||
|
||||
def rotlauf() -> int:
|
||||
quelle = BESTAND.read_text(encoding="utf-8")
|
||||
nach_name = {f.name: f for f in ALLE}
|
||||
alle_ok = True
|
||||
for name, aus, wird, rot_bei in MUTANTEN:
|
||||
anzahl = quelle.count(aus + "\n")
|
||||
unbekannt = [n for n in rot_bei if n not in nach_name]
|
||||
if anzahl != 1 or unbekannt:
|
||||
print(f"MUSTER {name}: steht {anzahl}-mal in waisen_bestand.py, unbekannte Faelle {unbekannt}")
|
||||
alle_ok = False
|
||||
continue
|
||||
arbeit = Path(tempfile.mkdtemp(prefix="bestand-rot-")).resolve()
|
||||
try:
|
||||
ordner = arbeit / "skript"
|
||||
ordner.mkdir()
|
||||
for begleiter in BEGLEITER:
|
||||
shutil.copy2(begleiter, ordner / begleiter.name)
|
||||
skript = ordner / BESTAND.name
|
||||
with open(skript, "w", encoding="utf-8", newline="\n") as datei:
|
||||
datei.write(quelle.replace(aus + "\n", wird + "\n"))
|
||||
ergebnis = fahre_liste([nach_name[n] for n in rot_bei], skript, arbeit, ausgabe=False)
|
||||
finally:
|
||||
entferne(arbeit)
|
||||
gruen = [n for n in rot_bei if ergebnis.get(n)]
|
||||
if gruen:
|
||||
alle_ok = False
|
||||
print(f"UEBERLEBT {name}: gruen blieben {gruen}")
|
||||
else:
|
||||
print(f"GETOETET {name}")
|
||||
if alle_ok:
|
||||
print("Rotlauf: alle Mutanten getoetet.")
|
||||
return 0
|
||||
print("Rotlauf: mindestens ein Mutant nicht getoetet oder ein Muster falsch.")
|
||||
return 1
|
||||
|
||||
|
||||
def main() -> int:
|
||||
for pfad in [BESTAND] + BEGLEITER:
|
||||
if not pfad.is_file():
|
||||
print(f"ABBRUCH: {pfad} fehlt")
|
||||
return 2
|
||||
if shutil.which("git") is None:
|
||||
print("ABBRUCH: git fehlt im PATH, die Git-Faelle messen sonst nichts")
|
||||
return 2
|
||||
namen = [f.name for f in ALLE]
|
||||
if len(set(namen)) != len(namen):
|
||||
print("ABBRUCH: ein Fallname steht doppelt")
|
||||
return 2
|
||||
if "--rotlauf" in sys.argv[1:]:
|
||||
return rotlauf()
|
||||
arbeit = Path(tempfile.mkdtemp(prefix="bestand-faelle-")).resolve()
|
||||
try:
|
||||
ergebnis = fahre_liste(ALLE, BESTAND, arbeit, ausgabe=True)
|
||||
if not ergebnis.get(GRUNDLINIE.name):
|
||||
print("ABBRUCH: Die Grundlinie ist nicht gruen. Der Rest misst nichts.")
|
||||
return 2
|
||||
bestanden = sum(1 for ok in ergebnis.values() if ok)
|
||||
print(f"\n{bestanden} von {len(ergebnis)} Faellen bestanden.")
|
||||
return 0 if bestanden == len(ergebnis) else 1
|
||||
finally:
|
||||
entferne(arbeit)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
sys.stdout.reconfigure(encoding="utf-8")
|
||||
except AttributeError:
|
||||
pass
|
||||
raise SystemExit(main())
|
||||
Reference in new issue
Block a user