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,171 @@
|
||||
"""Vergleicht die Codebloecke einer Plan-Taskdatei byteweise mit den Zieldateien im Projekt.
|
||||
|
||||
Geltungsbereich (Fangnetz, kein Nachweis):
|
||||
- Prueft nur, was der Plan als Codeblock mit vorangestellter Zieldatei-Zeile fuehrt.
|
||||
Konvention der Planer-Taskdateien (Projektvorgabe, Plan 3a und 4): direkt vor dem
|
||||
Zaun steht eine Zeile mit dem Pfad in Backticks, z. B. "`tests/x.py` neu anlegen:".
|
||||
- "neu anlegen" / "ersetzen" = ganze Datei, Gleichheit nach Normalisierung von CRLF
|
||||
und der letzten Leerzeile. "ergaenzen" / "einfuegen" / "Abschnitt" / "Ausschnitt" =
|
||||
Fragment, Pruefung auf Enthaltensein.
|
||||
- Mehrere Bloecke fuer dieselbe Datei: der letzte Ganzdatei-Block gilt (Plan-Reihenfolge).
|
||||
- Nicht gesehen: Bloecke ohne Pfadzeile (werden als "nicht zugeordnet" gemeldet, nicht
|
||||
geprueft), Zieldateien ohne Block (gemeldet), semantische Abweichungen bei gleichem
|
||||
Byteinhalt, Dateien ausserhalb der Liste "## Zieldateien" des Tasks.
|
||||
- Annahme, die im Skript erzwungen wird: jede Zieldatei aus "## Zieldateien" hat einen
|
||||
Block; fehlt er, ist das ein gemeldeter Befund (Exit 3), kein stilles Ueberspringen.
|
||||
|
||||
Exit-Codes: 0 alles gleich, 1 mindestens eine Abweichung oder fehlende Datei,
|
||||
3 keine Abweichung, aber Zieldateien ohne Block oder Bloecke ohne Zuordnung.
|
||||
|
||||
Aufruf: python plan_diff.py <taskdatei.md> --projekt <projektordner> [--nur <pfad> ...]
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import difflib
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ZAUN = re.compile(r"^```")
|
||||
PFAD_IN_BACKTICKS = re.compile(r"`([^`\s]+\.[A-Za-z0-9]+)`")
|
||||
# Fragmentwoerter werden ausserhalb der Backtick-Pfade gesucht: "Kopfzeile.tsx" traf sonst
|
||||
# "zeile" und wurde still nur auf Enthaltensein statt auf Gleichheit geprueft (2026-09-07).
|
||||
OHNE_PFAD = re.compile(r"`[^`]*`")
|
||||
FRAGMENT_WOERTER = ("ergänz", "ergaenz", "einfüg", "einfueg", "abschnitt", "ausschnitt", "zeile", "ersetzt durch", "wird ersetzt", "nach der zeile", "nach dem absatz")
|
||||
|
||||
|
||||
def normalisiere(text: str) -> str:
|
||||
text = text.replace("\r\n", "\n").replace("\r", "\n")
|
||||
return text.rstrip("\n") + "\n"
|
||||
|
||||
|
||||
def zieldateien(zeilen: list[str]) -> list[str]:
|
||||
pfade: list[str] = []
|
||||
aktiv = False
|
||||
for zeile in zeilen:
|
||||
if zeile.startswith("## "):
|
||||
aktiv = zeile.strip().lower().startswith("## zieldateien")
|
||||
continue
|
||||
if aktiv:
|
||||
for treffer in PFAD_IN_BACKTICKS.findall(zeile):
|
||||
if treffer not in pfade:
|
||||
pfade.append(treffer)
|
||||
return pfade
|
||||
|
||||
|
||||
def bloecke(zeilen: list[str], kandidaten: list[str]) -> tuple[dict[str, list[tuple[str, bool]]], list[int]]:
|
||||
"""Liefert je Pfad die Bloecke (Inhalt, ist_fragment) und die Zeilennummern nicht zugeordneter Bloecke."""
|
||||
ergebnis: dict[str, list[tuple[str, bool]]] = {}
|
||||
ohne: list[int] = []
|
||||
i = 0
|
||||
n = len(zeilen)
|
||||
while i < n:
|
||||
if ZAUN.match(zeilen[i]):
|
||||
start = i
|
||||
i += 1
|
||||
inhalt: list[str] = []
|
||||
while i < n and not ZAUN.match(zeilen[i]):
|
||||
inhalt.append(zeilen[i])
|
||||
i += 1
|
||||
# Pfadzeile: die naechste Zeile mit einem Zielpfad vor dem Zaun, hoechstens zehn
|
||||
# zurueck und nicht ueber einen anderen Zaun hinweg. Fragment, wenn die Pfadzeile
|
||||
# oder eine Zeile zwischen ihr und dem Zaun ein Fragmentwort traegt.
|
||||
pfad = None
|
||||
fragment = False
|
||||
dazwischen: list[str] = []
|
||||
for k in range(start - 1, max(start - 11, -1), -1):
|
||||
zeile = zeilen[k].strip()
|
||||
if ZAUN.match(zeile):
|
||||
break
|
||||
treffer = [p for p in PFAD_IN_BACKTICKS.findall(zeile) if p in kandidaten]
|
||||
if treffer:
|
||||
pfad = treffer[0]
|
||||
pruefen = [OHNE_PFAD.sub("", zeile).lower()] + [OHNE_PFAD.sub("", z).lower() for z in dazwischen]
|
||||
fragment = any(w in z for z in pruefen for w in FRAGMENT_WOERTER)
|
||||
break
|
||||
dazwischen.append(zeile)
|
||||
if pfad is None:
|
||||
ohne.append(start + 1)
|
||||
else:
|
||||
ergebnis.setdefault(pfad, []).append(("\n".join(inhalt) + "\n", fragment))
|
||||
i += 1
|
||||
return ergebnis, ohne
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("taskdatei")
|
||||
parser.add_argument("--projekt", required=True)
|
||||
parser.add_argument("--nur", nargs="*", default=None)
|
||||
args = parser.parse_args()
|
||||
|
||||
task = Path(args.taskdatei)
|
||||
projekt = Path(args.projekt)
|
||||
try:
|
||||
zeilen = task.read_text(encoding="utf-8").replace("\r\n", "\n").split("\n")
|
||||
except OSError as fehler:
|
||||
print(f"Taskdatei nicht lesbar: {fehler}")
|
||||
return 1
|
||||
|
||||
ziele = zieldateien(zeilen)
|
||||
if args.nur:
|
||||
ziele = [z for z in ziele if z in args.nur] + [z for z in args.nur if z not in ziele]
|
||||
if not ziele:
|
||||
print("Keine Zieldateien im Abschnitt '## Zieldateien' gefunden.")
|
||||
return 3
|
||||
|
||||
gefunden, ohne = bloecke(zeilen, ziele)
|
||||
abweichung = False
|
||||
luecke = False
|
||||
|
||||
for ziel in ziele:
|
||||
eintraege = gefunden.get(ziel, [])
|
||||
datei = projekt / ziel
|
||||
if not eintraege:
|
||||
print(f"KEIN BLOCK {ziel}")
|
||||
luecke = True
|
||||
continue
|
||||
try:
|
||||
ist = normalisiere(datei.read_text(encoding="utf-8"))
|
||||
except FileNotFoundError:
|
||||
print(f"FEHLT {ziel}")
|
||||
abweichung = True
|
||||
continue
|
||||
except UnicodeDecodeError as fehler:
|
||||
print(f"NICHT UTF-8 {ziel}: {fehler}")
|
||||
abweichung = True
|
||||
continue
|
||||
ganze = [inhalt for inhalt, frag in eintraege if not frag]
|
||||
fragmente = [inhalt for inhalt, frag in eintraege if frag]
|
||||
if ganze:
|
||||
soll = normalisiere(ganze[-1])
|
||||
if soll == ist:
|
||||
print(f"OK {ziel} ({len(ganze)} Ganzblock/-bloecke, {len(fragmente)} Fragment(e))")
|
||||
else:
|
||||
abweichung = True
|
||||
print(f"DIFF {ziel}")
|
||||
diff = difflib.unified_diff(soll.splitlines(), ist.splitlines(), "plan", "datei", lineterm="", n=2)
|
||||
for zeile in list(diff)[:60]:
|
||||
print(" " + zeile)
|
||||
for nr, fragment in enumerate(fragmente, 1):
|
||||
if normalisiere(fragment).rstrip("\n") in ist:
|
||||
print(f"FRAGMENT OK {ziel} #{nr}")
|
||||
else:
|
||||
abweichung = True
|
||||
print(f"FRAGMENT FEHLT {ziel} #{nr}")
|
||||
for zeile in normalisiere(fragment).splitlines()[:8]:
|
||||
print(" | " + zeile)
|
||||
|
||||
if ohne:
|
||||
luecke = True
|
||||
print(f"NICHT ZUGEORDNET: Bloecke ab Zeile {', '.join(str(z) for z in ohne)} (ohne Pfadzeile oder Pfad nicht in Zieldateien)")
|
||||
|
||||
if abweichung:
|
||||
return 1
|
||||
return 3 if luecke else 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in new issue
Block a user