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,168 @@
|
||||
"""Gegenpruefung nach einer Executer-Welle: Menge der geaenderten Dateien und Byte-Vergleich gegen die Kopie.
|
||||
|
||||
Koordinator-Werkzeug im Scratchpad. Fragen und Herkunft der Antwort:
|
||||
1. Welche Dateien hat die Welle geaendert? Quelle: `git status --porcelain -z` im Projekt.
|
||||
2. Welche durfte sie aendern? Quelle: Abschnitt "## Zieldateien" der uebergebenen Taskdateien.
|
||||
3. Ist jede Zieldatei byte-gleich mit dem Plan? Quelle: die Kopie `mat`, auf der plan_materialisiere.py
|
||||
alle Codebloecke des Plans angewendet hat.
|
||||
Was es NICHT sieht (Fangnetz, kein Nachweis):
|
||||
- Dateien, die .gitignore ausblendet (data/, artifacts/, node_modules/): Ein Executer, der dorthin
|
||||
schreibt, bleibt unsichtbar.
|
||||
- Ob die Kopie selbst sauber ist. Eine verschmutzte Kopie erzeugt Fehlalarm, kein falsches Gruen.
|
||||
- Prosa-Dateien (--prosa): nur Mengenpruefung, kein Byte-Vergleich; die liest der Koordinator selbst.
|
||||
- Bereits committete Aenderungen: Verglichen wird der Arbeitsbaum gegen HEAD.
|
||||
Eine fehlende Voraussetzung (Kopie fehlt, Taskdatei ohne Zieldateien, git laeuft nicht) ist ein Befund.
|
||||
Aufruf: python welle_vergleich.py --projekt <pfad> --mat <pfad> [--prosa <rel> ...] <taskdatei> [...]
|
||||
Exit 0 ohne Befund, 2 mit Befund.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
import traceback
|
||||
from pathlib import Path, PureWindowsPath
|
||||
|
||||
ZIEL = re.compile(r"^- `(.+?)`\s*\((.*)\)\s*$")
|
||||
|
||||
|
||||
def zieldateien(taskdatei: Path, projekt: Path, befunde: list[str]) -> dict[str, bool]:
|
||||
"""Relativer Pfad -> True, wenn die Datei geloescht werden soll."""
|
||||
try:
|
||||
zeilen = taskdatei.read_text(encoding="utf-8").replace("\r\n", "\n").split("\n")
|
||||
except OSError as fehler:
|
||||
befunde.append(f"{taskdatei.name}: nicht lesbar ({fehler})")
|
||||
return {}
|
||||
drin = False
|
||||
ziele: dict[str, bool] = {}
|
||||
for zeile in zeilen:
|
||||
if zeile.startswith("## "):
|
||||
drin = zeile.startswith("## Zieldateien")
|
||||
continue
|
||||
if not drin:
|
||||
continue
|
||||
treffer = ZIEL.match(zeile)
|
||||
if not treffer:
|
||||
continue
|
||||
absolut = PureWindowsPath(treffer.group(1))
|
||||
try:
|
||||
rel = absolut.relative_to(PureWindowsPath(str(projekt)))
|
||||
except ValueError:
|
||||
befunde.append(f"{taskdatei.name}: Zieldatei {absolut} liegt nicht unter dem Projekt {projekt}")
|
||||
continue
|
||||
ziele[rel.as_posix()] = "gelöscht" in treffer.group(2) or "geloescht" in treffer.group(2)
|
||||
if not ziele:
|
||||
befunde.append(f"{taskdatei.name}: keine Zieldateien gefunden -- ohne sie misst der Lauf nichts")
|
||||
return ziele
|
||||
|
||||
|
||||
def geaendert(projekt: Path, befunde: list[str]) -> dict[str, str]:
|
||||
"""Relativer Pfad -> zweistelliger Status aus git."""
|
||||
try:
|
||||
ergebnis = subprocess.run(
|
||||
["git", "-C", str(projekt), "status", "--porcelain", "-z", "--untracked-files=all"],
|
||||
capture_output=True,
|
||||
stdin=subprocess.DEVNULL,
|
||||
)
|
||||
except OSError as fehler:
|
||||
befunde.append(f"git laesst sich nicht starten ({fehler})")
|
||||
return {}
|
||||
if ergebnis.returncode != 0:
|
||||
befunde.append(f"git status endet mit {ergebnis.returncode}: {ergebnis.stderr.decode('utf-8', 'replace').strip()}")
|
||||
return {}
|
||||
teile = ergebnis.stdout.decode("utf-8", "replace").split("\0")
|
||||
aus: dict[str, str] = {}
|
||||
i = 0
|
||||
while i < len(teile):
|
||||
eintrag = teile[i]
|
||||
i += 1
|
||||
if len(eintrag) < 4:
|
||||
continue
|
||||
status, pfad = eintrag[:2], eintrag[3:]
|
||||
if status[0] in "RC":
|
||||
alt = teile[i] if i < len(teile) else "?"
|
||||
i += 1
|
||||
befunde.append(f"{pfad}: git meldet Umbenennung oder Kopie aus {alt} -- im Plan nicht vorgesehen")
|
||||
aus[pfad] = status
|
||||
return aus
|
||||
|
||||
|
||||
def erste_abweichung(echt: bytes, kopie: bytes) -> str:
|
||||
if echt.replace(b"\r\n", b"\n") == kopie.replace(b"\r\n", b"\n"):
|
||||
return "nur die Zeilenenden weichen ab (CRLF gegen LF)"
|
||||
a, b = echt.split(b"\n"), kopie.split(b"\n")
|
||||
for nummer, (links, rechts) in enumerate(zip(a, b), start=1):
|
||||
if links != rechts:
|
||||
return f"Zeile {nummer}: Baum {links[:160]!r} | Plan {rechts[:160]!r}"
|
||||
return f"Laenge: Baum {len(a)} Zeilen, Plan {len(b)} Zeilen; die gemeinsamen Zeilen sind gleich"
|
||||
|
||||
|
||||
def pruefe(args: argparse.Namespace, befunde: list[str]) -> None:
|
||||
projekt, mat = Path(args.projekt), Path(args.mat)
|
||||
if not projekt.is_dir():
|
||||
befunde.append(f"Projektordner {projekt} fehlt")
|
||||
return
|
||||
if not mat.is_dir():
|
||||
befunde.append(f"Kopie {mat} fehlt -- ohne sie gibt es keinen Byte-Vergleich")
|
||||
return
|
||||
prosa = {p.replace("\\", "/") for p in args.prosa}
|
||||
ziele: dict[str, bool] = {}
|
||||
for name in args.taskdateien:
|
||||
ziele.update(zieldateien(Path(name), projekt, befunde))
|
||||
status = geaendert(projekt, befunde)
|
||||
|
||||
for pfad in sorted(set(status) - set(ziele)):
|
||||
befunde.append(f"{pfad}: geaendert ({status[pfad]!r}), liegt aber ausserhalb der Zieldateien dieser Welle")
|
||||
for pfad in sorted(prosa - set(ziele)):
|
||||
befunde.append(f"{pfad}: als Prosa uebergeben, steht aber in keiner Taskdatei als Zieldatei")
|
||||
|
||||
gleich = 0
|
||||
for pfad, loeschen in sorted(ziele.items()):
|
||||
echt, kopie = projekt / pfad, mat / pfad
|
||||
if loeschen:
|
||||
if echt.exists():
|
||||
befunde.append(f"{pfad}: soll geloescht sein, steht aber noch im Baum")
|
||||
if kopie.exists():
|
||||
befunde.append(f"{pfad}: soll geloescht sein, steht aber noch in der Kopie")
|
||||
if not echt.exists() and "D" not in status.get(pfad, ""):
|
||||
befunde.append(f"{pfad}: fehlt im Baum, git meldet aber keine Loeschung ({status.get(pfad)!r})")
|
||||
continue
|
||||
if pfad not in status:
|
||||
befunde.append(f"{pfad}: Zieldatei ist unveraendert gegenueber HEAD")
|
||||
continue
|
||||
if not echt.is_file():
|
||||
befunde.append(f"{pfad}: Zieldatei fehlt im Baum")
|
||||
continue
|
||||
if pfad in prosa:
|
||||
continue
|
||||
if not kopie.is_file():
|
||||
befunde.append(f"{pfad}: fehlt in der Kopie -- kein Byte-Vergleich moeglich")
|
||||
continue
|
||||
a, b = echt.read_bytes(), kopie.read_bytes()
|
||||
if a == b:
|
||||
gleich += 1
|
||||
else:
|
||||
befunde.append(f"{pfad}: weicht vom Plan ab -- {erste_abweichung(a, b)}")
|
||||
print(f"{len(ziele)} Zieldateien, {len(status)} geaenderte Dateien laut git, {gleich} byte-gleich mit der Kopie, {len(prosa & set(ziele))} nur als Menge geprueft.")
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--projekt", required=True)
|
||||
parser.add_argument("--mat", required=True)
|
||||
parser.add_argument("--prosa", nargs="*", default=[])
|
||||
parser.add_argument("taskdateien", nargs="+")
|
||||
args = parser.parse_args()
|
||||
befunde: list[str] = []
|
||||
try:
|
||||
pruefe(args, befunde)
|
||||
except Exception: # Ein Absturz ist selbst ein Befund; die gesammelten Befunde gehen trotzdem raus.
|
||||
befunde.append("unerwarteter Abbruch: " + traceback.format_exc().strip().splitlines()[-1])
|
||||
for befund in befunde:
|
||||
print("BEFUND " + befund)
|
||||
print(f"{len(befunde)} Befund(e).")
|
||||
return 2 if befunde else 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in new issue
Block a user