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
2c9a4475fc
97 files changed
+14627
No files matched your search
@@ -0,0 +1,32 @@
|
||||
---
|
||||
type: resource
|
||||
status: active
|
||||
title: "Planwerkzeuge: Plan auf eine Kopie anwenden und jede Welle dagegen vergleichen"
|
||||
summary: "Fünf Skripte für die Standardschleife bei Plänen mit ausgeschriebenem Code — Plan auf eine Kopie anwenden, je Welle Menge und Bytes vergleichen, Aus-wird-Blöcke prüfen, Codeblöcke auf unsichtbare Zeichen absuchen, Escape-Folgen per Codepunkt setzen. Entstanden 2026-09-17 in der Trading Plattform (Teil 3b)."
|
||||
tags: [vault-tools, codeworkflow]
|
||||
created: 2026-09-17
|
||||
updated: 2026-09-17
|
||||
---
|
||||
|
||||
# Planwerkzeuge für die Kopie mit angewendetem Plan
|
||||
|
||||
Die Regel dazu steht in [[AI/Regelwerk/codeworkflow|codeworkflow.md]] („Die Kopie mit dem angewendeten Plan bleibt stehen …"). Hier steht nur, wie man die Skripte benutzt. Alle laufen mit dem Windows-Python: `PYTHONIOENCODING=utf-8 python -X utf8 <skript> …`.
|
||||
|
||||
## Ablauf
|
||||
|
||||
1. **Kopie bauen — erst, wenn der Planer fertig gemeldet hat.** Den Projektbaum ohne `.git`, ohne Geheimnisdateien und ohne `node_modules` in einen Wegwerfordner kopieren. **Ausschlüsse als Pfade vom Wurzelordner angeben, nicht als Ordnernamen:** Ein Ausschluss von „jedem Ordner namens `data`" hat am 2026-09-17 auch das Paket `trading/data` getroffen, und die Suite der Kopie brach mit Sammelfehlern ab. Nach dem Kopieren die Paketliste gegen das Original halten. Braucht die Kopie `node_modules`, eine Junction setzen (`mklink /J`) — **und sie vor dem Löschen der Kopie mit `cmd //c rmdir <pfad>` wieder lösen**, sonst trifft `rm -rf` das echte `node_modules`. Nie `tsc -b` durch die Junction fahren (schreibt nach `node_modules/.tmp`); `npx tsc -p tsconfig.app.json --noEmit` nehmen.
|
||||
2. **Plan anwenden:** `plan_materialisiere.py <planordner> <kopie> task-01.md task-02.md …` in Wellenreihenfolge. Erkennt die Marken `Aus:` / `wird:`, „Die ganze Datei ist neu", „vollständig ersetzt" und „Häng diesen Block ans Ende"; Dateien, die `## Zieldateien` als „(gelöscht)" führt, löscht es in der Kopie. Prosa-Tasks („wird: ein neuer Abschnitt, er muss sagen …") lassen sich nicht anwenden.
|
||||
3. **Auf der Kopie fahren:** Suite, Typprüfung, Mutanten. Ein Mutantenlauf gibt je Mutant die Fehlerzeile aus, nicht nur den Exitcode.
|
||||
4. **Vor jeder Welle und nach jeder Plankorrektur:** `plan_bloecke.py vor <tasks…> --projekt <projekt>` (jeder Aus-Block steht genau einmal im echten Baum) und `plan_zeichen.py <planordner>` (keine Zeilen- und Absatztrenner, Format- und Steuerzeichen in Codeblöcken).
|
||||
5. **Nach jeder Welle:** `welle_vergleich.py --projekt <projekt> --mat <kopie> [--prosa <rel> …] -- <taskdateien der Welle>`. Prüft, ob genau die Zieldateien geändert sind und ob jede byte-gleich mit der Kopie ist. Es zeigt je Datei nur die erste Abweichung — danach `diff` auf die ganze Datei. Eigene Plankorrekturen im Baum meldet es zu Recht als „außerhalb der Zieldateien".
|
||||
6. **Escape-Folgen setzen:** `plan_escape.py <datei> <zeile> <codepunkt-hex> …` ersetzt echte Zeichen in einer Zeile durch ihre Escape-Folge, ohne dass eine Schreibschicht mitredet.
|
||||
|
||||
## Was belegt ist
|
||||
|
||||
`welle_vergleich_treiber.py` fährt 14 Angriffsfälle gegen `welle_vergleich.py` (Grundlinie zuerst; fremde Datei, unveränderte Zieldatei, Abweichung in erster und letzter Zeile und in der letzten Zieldatei, CRLF, Datei fehlt in der Kopie, Task ohne Zieldateien, Kopie fehlt, Löschung nicht ausgeführt, Prosa-Datei unverändert, Zieldatei außerhalb des Projekts, Datei länger als der Plan). `plan_zeichen.py` hat am 2026-09-17 an einem echten Fund rot gezeigt (U+2028 im Plan). `plan_materialisiere.py` reproduziert Task 108 des Teil-3b-Plans aus dem Stand vor den Wellen byte-genau.
|
||||
|
||||
## Was die Skripte nicht sehen
|
||||
|
||||
Dateien, die `.gitignore` ausblendet; ob die Kopie selbst sauber ist (eine verschmutzte Kopie erzeugt Fehlalarm, kein falsches Grün); **ob die Kopie älter ist als der Plan** — wird eine Taskdatei nach dem Anwenden noch überarbeitet, meldet `welle_vergleich.py` eine Abweichung, die keine ist (2026-09-17, Teil 4c: der Planer lief noch, als die Kopie entstand). Dann entscheidet `plan_bloecke.py nach` gegen den echten Baum, und die betroffenen Dateien der Kopie werden aus `git show HEAD:<pfad>` neu aufgebaut und der Task erneut angewendet; verwechselbare sichtbare Zeichen (kyrillisches „с"). Sie sind ein Fangnetz, kein Nachweis — den liefert der Lauf mit echten Daten.
|
||||
|
||||
Das ältere `.vault-tools/plan-diff/plan_diff.py` vergleicht Ganzdatei-Blöcke einer früheren Plankonvention und bleibt dafür in Gebrauch.
|
||||
@@ -0,0 +1,98 @@
|
||||
"""Baut die schlanke Kopie eines Projektbaums fuer den Byte-Vergleich -- Ausschluesse nur als Pfade vom Wurzelordner.
|
||||
|
||||
Ersteller: Pascal Michel
|
||||
Erstellt: 2026-09-17
|
||||
Doku: .vault-tools/plan-kopie/LIESMICH.md
|
||||
|
||||
Aufruf: python kopie_bauen.py <projekt> <ziel> --ohne <relpfad> [<relpfad> ...]
|
||||
|
||||
Warum Pfade und keine Namen: Ein Ausschluss von "jedem Ordner namens data" hat am 2026-09-17 auch das Paket
|
||||
trading/data getroffen. Nach dem Kopieren vergleicht das Skript deshalb die Python-Pakete (Ordner mit __init__.py)
|
||||
beider Baeume und meldet jedes, das in der Kopie fehlt und nicht unter einem ausgeschlossenen Pfad liegt.
|
||||
Das Ziel darf nicht existieren; Verknuepfungen und Junctions werden nicht verfolgt, sondern uebersprungen.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import shutil
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def _relativ(pfad: Path, wurzel: Path) -> str:
|
||||
return pfad.relative_to(wurzel).as_posix()
|
||||
|
||||
|
||||
def _pakete(wurzel: Path, ohne: set[str]) -> set[str]:
|
||||
gefunden = set()
|
||||
for ordner, unterordner, dateien in os.walk(wurzel):
|
||||
rel = _relativ(Path(ordner), wurzel)
|
||||
unterordner[:] = [
|
||||
name for name in unterordner
|
||||
if (f"{rel}/{name}" if rel != "." else name) not in ohne
|
||||
and not Path(ordner, name).is_symlink()
|
||||
and not Path(ordner, name).is_junction()
|
||||
]
|
||||
if "__init__.py" in dateien:
|
||||
gefunden.add(rel)
|
||||
return gefunden
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(allow_abbrev=False)
|
||||
parser.add_argument("projekt", type=Path)
|
||||
parser.add_argument("ziel", type=Path)
|
||||
parser.add_argument("--ohne", nargs="*", default=[], help="relative Pfade vom Wurzelordner, mit /")
|
||||
args = parser.parse_args()
|
||||
|
||||
projekt = args.projekt.resolve()
|
||||
ziel = args.ziel.resolve()
|
||||
if not projekt.is_dir():
|
||||
print(f"BEFUND Projekt {projekt} ist kein Ordner")
|
||||
return 2
|
||||
if ziel.exists():
|
||||
print(f"BEFUND Ziel {ziel} existiert schon -- erst ansehen, dann selbst loeschen")
|
||||
return 2
|
||||
|
||||
ohne = {eintrag.replace("\\", "/").strip("/") for eintrag in args.ohne}
|
||||
befunde = []
|
||||
for eintrag in sorted(ohne):
|
||||
if "/" not in eintrag and not (projekt / eintrag).exists():
|
||||
befunde.append(f"Ausschluss '{eintrag}' gibt es im Wurzelordner nicht -- ein Name statt eines Pfads?")
|
||||
elif not (projekt / eintrag).exists():
|
||||
befunde.append(f"Ausschluss '{eintrag}' gibt es nicht")
|
||||
|
||||
uebersprungen = []
|
||||
|
||||
def ignoriere(ordner: str, namen: list[str]) -> list[str]:
|
||||
rel = _relativ(Path(ordner), projekt)
|
||||
raus = []
|
||||
for name in namen:
|
||||
pfad = Path(ordner, name)
|
||||
relpfad = name if rel == "." else f"{rel}/{name}"
|
||||
if relpfad in ohne or name == "__pycache__":
|
||||
raus.append(name)
|
||||
elif pfad.is_symlink() or pfad.is_junction():
|
||||
uebersprungen.append(relpfad)
|
||||
raus.append(name)
|
||||
return raus
|
||||
|
||||
shutil.copytree(projekt, ziel, ignore=ignoriere)
|
||||
|
||||
fehlend = sorted(_pakete(projekt, ohne) - _pakete(ziel, set()))
|
||||
for paket in fehlend:
|
||||
befunde.append(f"Paket '{paket}' fehlt in der Kopie")
|
||||
|
||||
dateien = sum(len(d) for _, _, d in os.walk(ziel))
|
||||
print(f"{dateien} Dateien kopiert, {len(ohne)} Pfade ausgeschlossen, "
|
||||
f"{len(uebersprungen)} Verknuepfungen uebersprungen, {len(_pakete(ziel, set()))} Pakete in der Kopie.")
|
||||
for zeile in uebersprungen:
|
||||
print(f"HINWEIS Verknuepfung uebersprungen: {zeile}")
|
||||
for zeile in befunde:
|
||||
print(f"BEFUND {zeile}")
|
||||
print(f"{len(befunde)} Befund(e).")
|
||||
return 1 if befunde else 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,125 @@
|
||||
"""Prueft die Aus-/wird-Bloecke einer Plan-Taskdatei gegen den Projektbaum.
|
||||
|
||||
Koordinator-Werkzeug im Scratchpad. Geltungsbereich (Fangnetz, kein Nachweis):
|
||||
- Sieht nur Codebloecke, deren letzte nicht leere Zeile vor dem Zaun genau "Aus:" oder
|
||||
"wird:" ist (Projektkonvention der Planer-Taskdateien seit Teil 5). Die Zieldatei ist der
|
||||
letzte Pfad in Backticks in einer Ueberschrift (##, ###, ####) oberhalb des Blocks, der
|
||||
auf eine im Projekt vorhandene oder unter "## Zieldateien" genannte Datei zeigt.
|
||||
- Modus vor: jeder Aus-Block steht genau einmal in seiner Zieldatei (0 = Befund, >1 = Befund
|
||||
"mehrdeutig"). Belegt, dass der Plan den heutigen Stand byte-genau zitiert.
|
||||
- Modus nach: jeder wird-Block steht in der Zieldatei; jeder Aus-Block steht nicht mehr darin,
|
||||
ausser er ist Teil seines eigenen wird-Blocks (Einfuegung).
|
||||
- Ganzdatei-Bloecke ("neu anlegen") sieht es nicht; dafuer ist .vault-tools/plan-diff/plan_diff.py da.
|
||||
- Vergleich nach Normalisierung CRLF -> LF. Eine fehlende Zieldatei ist im Modus vor fuer
|
||||
Aus-Bloecke ein Befund, im Modus nach fuer beide.
|
||||
Exit: 0 kein Befund, 2 Befunde, 3 kein einziger Aus-/wird-Block gefunden (nichts geprueft).
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ZAUN = re.compile(r"^```")
|
||||
UEBERSCHRIFT = re.compile(r"^#{2,4} ")
|
||||
PFAD = re.compile(r"`([^`\s]+/[^`\s]+\.[A-Za-z0-9]+|[^`\s/]+\.(?:py|ts|tsx|toml|md|css|json))`")
|
||||
|
||||
|
||||
def normal(text: str) -> str:
|
||||
return text.replace("\r\n", "\n").replace("\r", "\n")
|
||||
|
||||
|
||||
def lies_bloecke(taskdatei: Path, projekt: Path) -> list[dict]:
|
||||
zeilen = normal(taskdatei.read_text(encoding="utf-8")).split("\n")
|
||||
bloecke: list[dict] = []
|
||||
ziel: str | None = None
|
||||
i = 0
|
||||
while i < len(zeilen):
|
||||
zeile = zeilen[i]
|
||||
if UEBERSCHRIFT.match(zeile):
|
||||
for treffer in PFAD.findall(zeile):
|
||||
ziel = treffer
|
||||
if ZAUN.match(zeile):
|
||||
start = i
|
||||
# Ein Zaun aus n Backticks schliesst erst mit mindestens n Backticks: Ein
|
||||
# Vier-Backtick-Block darf einen Drei-Backtick-Block enthalten (Doku-Tasks).
|
||||
laenge = len(zeile) - len(zeile.lstrip("`"))
|
||||
i += 1
|
||||
inhalt: list[str] = []
|
||||
while i < len(zeilen) and not (
|
||||
zeilen[i].startswith("`" * laenge) and not zeilen[i].strip("`").strip()
|
||||
):
|
||||
inhalt.append(zeilen[i])
|
||||
i += 1
|
||||
marke = ""
|
||||
k = start - 1
|
||||
while k >= 0 and not zeilen[k].strip():
|
||||
k -= 1
|
||||
if k >= 0:
|
||||
marke = zeilen[k].strip()
|
||||
if marke in ("Aus:", "wird:"):
|
||||
bloecke.append({"art": marke[:-1], "ziel": ziel, "zeile": start + 1, "text": "\n".join(inhalt)})
|
||||
i += 1
|
||||
return bloecke
|
||||
|
||||
|
||||
def main() -> int:
|
||||
zerleger = argparse.ArgumentParser()
|
||||
zerleger.add_argument("modus", choices=["vor", "nach"])
|
||||
zerleger.add_argument("taskdateien", nargs="+")
|
||||
zerleger.add_argument("--projekt", required=True)
|
||||
argumente = zerleger.parse_args()
|
||||
projekt = Path(argumente.projekt)
|
||||
|
||||
befunde = 0
|
||||
geprueft = 0
|
||||
for name in argumente.taskdateien:
|
||||
taskdatei = Path(name)
|
||||
bloecke = lies_bloecke(taskdatei, projekt)
|
||||
paare = 0
|
||||
for nummer, block in enumerate(bloecke):
|
||||
if block["ziel"] is None:
|
||||
print(f"BEFUND {taskdatei.name}:{block['zeile']}: {block['art']}-Block ohne Zieldatei in einer Ueberschrift")
|
||||
befunde += 1
|
||||
continue
|
||||
datei = projekt / block["ziel"]
|
||||
if not datei.exists():
|
||||
if argumente.modus == "nach" or block["art"] == "Aus":
|
||||
print(f"BEFUND {taskdatei.name}:{block['zeile']}: Zieldatei {block['ziel']} fehlt")
|
||||
befunde += 1
|
||||
continue
|
||||
inhalt = normal(datei.read_text(encoding="utf-8"))
|
||||
anzahl = inhalt.count(block["text"])
|
||||
geprueft += 1
|
||||
if argumente.modus == "vor":
|
||||
if block["art"] != "Aus":
|
||||
continue
|
||||
paare += 1
|
||||
if anzahl == 0:
|
||||
print(f"BEFUND {taskdatei.name}:{block['zeile']}: Aus-Block steht nicht in {block['ziel']}")
|
||||
befunde += 1
|
||||
elif anzahl > 1:
|
||||
print(f"BEFUND {taskdatei.name}:{block['zeile']}: Aus-Block steht {anzahl}-mal in {block['ziel']} (mehrdeutig)")
|
||||
befunde += 1
|
||||
else:
|
||||
if block["art"] == "wird":
|
||||
paare += 1
|
||||
if anzahl == 0:
|
||||
print(f"BEFUND {taskdatei.name}:{block['zeile']}: wird-Block steht nicht in {block['ziel']}")
|
||||
befunde += 1
|
||||
else:
|
||||
folge = bloecke[nummer + 1] if nummer + 1 < len(bloecke) else None
|
||||
einfuegung = folge is not None and folge["art"] == "wird" and block["text"] in folge["text"]
|
||||
if anzahl > 0 and not einfuegung:
|
||||
print(f"BEFUND {taskdatei.name}:{block['zeile']}: Aus-Block steht noch in {block['ziel']}")
|
||||
befunde += 1
|
||||
print(f"{taskdatei.name}: {len(bloecke)} Aus-/wird-Bloecke, im Modus {argumente.modus} geprueft: {paare}")
|
||||
if geprueft == 0:
|
||||
print("Nichts geprueft: kein Aus-/wird-Block gefunden.")
|
||||
return 3
|
||||
print(f"{befunde} Befund(e).")
|
||||
return 2 if befunde else 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,31 @@
|
||||
"""Ersetzt in EINER Zeile einer Datei bestimmte echte Zeichen durch ihre sechs ASCII-Zeichen lange Escape-Folge.
|
||||
|
||||
Arbeitet mit Codepunkten; im eigenen Quelltext steht keine Escape-Folge, die eine Schicht auswerten koennte.
|
||||
Aufruf: python plan_escape.py <datei> <zeilennummer> <codepunkt-hex> [<codepunkt-hex> ...]
|
||||
Beispiel: python plan_escape.py task-107.md 311 2122 0441
|
||||
Exit 0, wenn mindestens ein Zeichen ersetzt wurde; 2, wenn die Zeile keines der Zeichen traegt.
|
||||
"""
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
datei = Path(sys.argv[1])
|
||||
nummer = int(sys.argv[2])
|
||||
punkte = [int(p, 16) for p in sys.argv[3:]]
|
||||
|
||||
text = datei.read_bytes().decode("utf-8")
|
||||
zeilen = text.split("\n")
|
||||
zeile = zeilen[nummer - 1]
|
||||
ersetzt = 0
|
||||
for punkt in punkte:
|
||||
zeichen = chr(punkt)
|
||||
folge = chr(92) + "u" + format(punkt, "04x")
|
||||
ersetzt += zeile.count(zeichen)
|
||||
zeile = zeile.replace(zeichen, folge)
|
||||
if ersetzt == 0:
|
||||
print(f"BEFUND: Zeile {nummer} traegt keines der Zeichen -- nichts geaendert: {zeilen[nummer - 1][:100]!r}")
|
||||
sys.exit(2)
|
||||
zeilen[nummer - 1] = zeile
|
||||
with open(datei, "w", encoding="utf-8", newline="") as strom:
|
||||
strom.write("\n".join(zeilen))
|
||||
print(f"{ersetzt} Zeichen ersetzt in Zeile {nummer}: {zeile.strip()[:140]}")
|
||||
@@ -0,0 +1,153 @@
|
||||
"""Wendet die Codebloecke der Taskdateien eines Plans in Wellenreihenfolge auf eine Kopie des Projekts an.
|
||||
|
||||
Koordinator-Werkzeug (entstanden 2026-09-17, Trading Plattform Teil 3b). Es tut etwas und beurteilt
|
||||
nichts; das Urteil faellt danach mit Suite, Mutanten und welle_vergleich.py.
|
||||
Geltungsbereich:
|
||||
- Zieldatei ist der letzte Pfad in Backticks in einer Ueberschrift (## bis ####) oberhalb des Blocks.
|
||||
- Dateien, die "## Zieldateien" als "(gelöscht)" fuehrt, werden in der Kopie geloescht.
|
||||
- Bloecke mit Einrueckung (Befehle in Listen) und Prosa-Vorgaben ("wird: ein neuer Abschnitt ...")
|
||||
werden uebergangen: Ein Doku-Task mit frei zu formulierendem Text laesst sich nicht anwenden.
|
||||
- Marke ist die letzte nicht leere Zeile vor dem Zaun:
|
||||
"Aus:" -> merkt den Block als alten Text
|
||||
"wird:" -> ersetzt den gemerkten Text (muss genau einmal vorkommen)
|
||||
enthaelt "Die ganze Datei ist neu" -> schreibt die Datei
|
||||
beginnt mit "Häng diesen Block ans Ende" -> haengt an
|
||||
Alles andere (Befehle, erwartete Ausgaben, Prosa-"wird: ...") wird uebergangen und gezaehlt.
|
||||
- Zaeune aus n Backticks schliessen erst mit n Backticks.
|
||||
- Geschrieben wird immer mit LF.
|
||||
Aufruf: python plan_materialisiere.py <planordner> <kopie> <taskdatei> [<taskdatei> ...]
|
||||
Exit 0 ohne Befund, 2 mit Befund.
|
||||
"""
|
||||
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
UEBERSCHRIFT = re.compile(r"^#{2,4} ")
|
||||
PFAD = re.compile(r"`([^`\s]+/[^`\s]+\.[A-Za-z0-9]+|[^`\s/]+\.(?:py|ts|tsx|toml|md|css|json))`")
|
||||
|
||||
|
||||
def normal(text: str) -> str:
|
||||
return text.replace("\r\n", "\n").replace("\r", "\n")
|
||||
|
||||
|
||||
def bloecke(taskdatei: Path):
|
||||
zeilen = normal(taskdatei.read_text(encoding="utf-8")).split("\n")
|
||||
ziel = None
|
||||
i = 0
|
||||
while i < len(zeilen):
|
||||
zeile = zeilen[i]
|
||||
if UEBERSCHRIFT.match(zeile):
|
||||
treffer = PFAD.findall(zeile)
|
||||
if treffer:
|
||||
ziel = treffer[-1]
|
||||
if zeile.lstrip().startswith("```"):
|
||||
einzug = len(zeile) - len(zeile.lstrip())
|
||||
kern = zeile.lstrip()
|
||||
laenge = len(kern) - len(kern.lstrip("`"))
|
||||
start = i
|
||||
i += 1
|
||||
inhalt = []
|
||||
while i < len(zeilen):
|
||||
kandidat = zeilen[i].lstrip()
|
||||
if kandidat.startswith("`" * laenge) and not kandidat.strip("`").strip():
|
||||
break
|
||||
inhalt.append(zeilen[i][einzug:] if zeilen[i][:einzug].strip() == "" else zeilen[i])
|
||||
i += 1
|
||||
k = start - 1
|
||||
while k >= 0 and not zeilen[k].strip():
|
||||
k -= 1
|
||||
marke = zeilen[k].strip() if k >= 0 else ""
|
||||
yield {"ziel": ziel, "marke": marke, "zeile": start + 1, "text": "\n".join(inhalt), "einzug": einzug}
|
||||
i += 1
|
||||
|
||||
|
||||
LOESCHZIEL = re.compile(r"^- `(.+?)`\s*\((.*)\)\s*$")
|
||||
|
||||
|
||||
def loesche(taskdatei: Path, kopie: Path) -> int:
|
||||
"""Loescht, was der Abschnitt "## Zieldateien" als "(gelöscht)" fuehrt.
|
||||
|
||||
Der Pfad dort ist absolut und zeigt ins echte Projekt. Geloescht wird in der Kopie die
|
||||
Datei am laengsten Pfadende, das dort existiert -- nie etwas ausserhalb der Kopie.
|
||||
"""
|
||||
drin = False
|
||||
anzahl = 0
|
||||
for zeile in normal(taskdatei.read_text(encoding="utf-8")).split("\n"):
|
||||
if zeile.startswith("## "):
|
||||
drin = zeile.startswith("## Zieldateien")
|
||||
continue
|
||||
treffer = LOESCHZIEL.match(zeile) if drin else None
|
||||
if not treffer or "gelöscht" not in treffer.group(2):
|
||||
continue
|
||||
teile = treffer.group(1).replace("\\", "/").split("/")
|
||||
for anfang in range(len(teile)):
|
||||
kandidat = kopie.joinpath(*teile[anfang:])
|
||||
if ":" not in teile[anfang] and kandidat.is_file():
|
||||
kandidat.unlink()
|
||||
anzahl += 1
|
||||
break
|
||||
return anzahl
|
||||
|
||||
|
||||
def main() -> int:
|
||||
plan, kopie = Path(sys.argv[1]), Path(sys.argv[2])
|
||||
befunde = 0
|
||||
for name in sys.argv[3:]:
|
||||
taskdatei = plan / name
|
||||
alt = None
|
||||
zaehler = {"neu": 0, "ersetzt": 0, "angehaengt": 0, "uebergangen": 0}
|
||||
for block in bloecke(taskdatei):
|
||||
marke, ziel = block["marke"], block["ziel"]
|
||||
if block["einzug"] > 0:
|
||||
zaehler["uebergangen"] += 1
|
||||
continue
|
||||
if marke == "Aus:" or marke.startswith("Aus, Zeile"):
|
||||
alt = block
|
||||
continue
|
||||
if marke == "wird:":
|
||||
if alt is None or ziel is None:
|
||||
print(f"BEFUND {name}:{block['zeile']}: wird-Block ohne Aus-Block oder ohne Zieldatei")
|
||||
befunde += 1
|
||||
continue
|
||||
pfad = kopie / ziel
|
||||
if not pfad.exists():
|
||||
print(f"BEFUND {name}:{block['zeile']}: Zieldatei {ziel} fehlt in der Kopie")
|
||||
befunde += 1
|
||||
alt = None
|
||||
continue
|
||||
text = normal(pfad.read_text(encoding="utf-8"))
|
||||
anzahl = text.count(alt["text"])
|
||||
if anzahl != 1:
|
||||
print(f"BEFUND {name}:{alt['zeile']}: Aus-Block steht {anzahl}-mal in {ziel}")
|
||||
befunde += 1
|
||||
else:
|
||||
with open(pfad, "w", encoding="utf-8", newline="\n") as strom:
|
||||
strom.write(text.replace(alt["text"], block["text"]))
|
||||
zaehler["ersetzt"] += 1
|
||||
alt = None
|
||||
continue
|
||||
alt = None
|
||||
if ("Die ganze Datei ist neu" in marke or "vollständig ersetzt" in marke) and ziel is not None:
|
||||
pfad = kopie / ziel
|
||||
pfad.parent.mkdir(parents=True, exist_ok=True)
|
||||
with open(pfad, "w", encoding="utf-8", newline="\n") as strom:
|
||||
strom.write(block["text"].rstrip("\n") + "\n")
|
||||
zaehler["neu"] += 1
|
||||
continue
|
||||
if marke.startswith("Häng diesen Block ans Ende") and ziel is not None:
|
||||
pfad = kopie / ziel
|
||||
text = normal(pfad.read_text(encoding="utf-8"))
|
||||
with open(pfad, "w", encoding="utf-8", newline="\n") as strom:
|
||||
strom.write(text.rstrip("\n") + "\n" + block["text"].rstrip("\n") + "\n")
|
||||
zaehler["angehaengt"] += 1
|
||||
continue
|
||||
zaehler["uebergangen"] += 1
|
||||
zaehler["geloescht"] = loesche(taskdatei, kopie)
|
||||
print(f"{name}: {zaehler}")
|
||||
print(f"{befunde} Befund(e).")
|
||||
return 2 if befunde else 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,59 @@
|
||||
"""Sucht in den Codebloecken eines Plans nach Zeichen, die ein Executer nicht sehen oder nicht tippen kann.
|
||||
|
||||
Koordinator-Werkzeug. Frage: Steht in einem Codeblock ein Zeichen, das beim Abschreiben verloren geht?
|
||||
Herkunft der Antwort: Unicode-Kategorie des Zeichens (unicodedata, Quelle: Python-Standardbibliothek).
|
||||
Befund ist jedes Zeichen der Kategorien Zl, Zp, Cf, Cc (ausser \\n und \\t) und Zs ausser dem Leerzeichen --
|
||||
also Zeilen- und Absatztrenner, Formatzeichen, Steuerzeichen, geschuetzte und schmale Leerzeichen.
|
||||
Alle uebrigen Nicht-ASCII-Zeichen in Codebloecken werden nur gezaehlt und gezeigt (Umlaute, Gedankenstriche).
|
||||
Was es nicht sieht: Zeichen ausserhalb von Codezaeunen; verwechselbare sichtbare Zeichen (kyrillisches c).
|
||||
Aufruf: python plan_zeichen.py <planordner> Exit 0 ohne Befund, 2 mit Befund.
|
||||
"""
|
||||
|
||||
import sys
|
||||
import unicodedata
|
||||
from collections import Counter
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def main() -> int:
|
||||
plan = Path(sys.argv[1])
|
||||
dateien = sorted(plan.glob("*.md"))
|
||||
if not dateien:
|
||||
print(f"BEFUND {plan}: keine Markdown-Dateien -- der Lauf misst nichts")
|
||||
return 2
|
||||
befunde = 0
|
||||
sichtbar: Counter[str] = Counter()
|
||||
bloecke = 0
|
||||
for datei in dateien:
|
||||
zeilen = datei.read_text(encoding="utf-8").replace("\r\n", "\n").split("\n")
|
||||
zaun = 0
|
||||
for nummer, zeile in enumerate(zeilen, start=1):
|
||||
kern = zeile.lstrip()
|
||||
if kern.startswith("```"):
|
||||
laenge = len(kern) - len(kern.lstrip("`"))
|
||||
if zaun == 0:
|
||||
zaun = laenge
|
||||
bloecke += 1
|
||||
elif laenge >= zaun and not kern.strip("`").strip():
|
||||
zaun = 0
|
||||
continue
|
||||
if zaun == 0:
|
||||
continue
|
||||
for spalte, zeichen in enumerate(zeile, start=1):
|
||||
if ord(zeichen) < 128 and zeichen not in "\x0b\x0c\x1b" and (zeichen >= " " or zeichen == "\t"):
|
||||
continue
|
||||
kategorie = unicodedata.category(zeichen)
|
||||
if kategorie in ("Zl", "Zp", "Cf", "Cc") or (kategorie == "Zs" and zeichen != " "):
|
||||
name = unicodedata.name(zeichen, "?")
|
||||
print(f"BEFUND {datei.name}:{nummer}:{spalte}: U+{ord(zeichen):04X} {name} ({kategorie}) in: {zeile.strip()[:110]!r}")
|
||||
befunde += 1
|
||||
else:
|
||||
sichtbar[zeichen] += 1
|
||||
print(f"{len(dateien)} Dateien, {bloecke} Codebloecke gelesen.")
|
||||
print("Sichtbare Nicht-ASCII-Zeichen in Codebloecken: " + " ".join(f"{z}(U+{ord(z):04X})x{n}" for z, n in sichtbar.most_common()))
|
||||
print(f"{befunde} Befund(e).")
|
||||
return 2 if befunde else 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -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())
|
||||
@@ -0,0 +1,168 @@
|
||||
"""Regressionsnachweis fuer welle_vergleich.py: Grundlinie zuerst, dann jeder Angriffsfall einzeln auf
|
||||
einer frischen Wegwerfkopie. Je Fall werden Exit-Code und ausloesende Meldung geprueft.
|
||||
Aufruf: python welle_vergleich_treiber.py Exit 0, wenn jeder Fall so ausgeht wie erwartet.
|
||||
"""
|
||||
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
WERKZEUG = Path(__file__).parent / "welle_vergleich.py"
|
||||
|
||||
|
||||
def git(ordner: Path, *args: str) -> None:
|
||||
subprocess.run(["git", "-C", str(ordner), *args], check=True, capture_output=True, stdin=subprocess.DEVNULL)
|
||||
|
||||
|
||||
def schreibe(pfad: Path, text: str) -> None:
|
||||
pfad.parent.mkdir(parents=True, exist_ok=True)
|
||||
pfad.write_bytes(text.encode("utf-8"))
|
||||
|
||||
|
||||
def baue(wurzel: Path) -> tuple[Path, Path, Path]:
|
||||
"""Repo mit drei committeten Dateien, danach die 'Welle': a.py geaendert, sub/neu.py neu, weg.py geloescht,
|
||||
doku.md geaendert (Prosa). Die Kopie traegt denselben Stand."""
|
||||
projekt, mat = wurzel / "projekt", wurzel / "mat"
|
||||
projekt.mkdir()
|
||||
git(projekt, "init", "-q")
|
||||
git(projekt, "config", "user.email", "[email protected]")
|
||||
git(projekt, "config", "user.name", "Treiber")
|
||||
git(projekt, "config", "core.autocrlf", "false")
|
||||
schreibe(projekt / "a.py", "eins\nzwei\ndrei\n")
|
||||
schreibe(projekt / "b.py", "bleibt\n")
|
||||
schreibe(projekt / "weg.py", "weg\n")
|
||||
schreibe(projekt / "doku.md", "alt\n")
|
||||
git(projekt, "add", "-A")
|
||||
git(projekt, "commit", "-q", "-m", "basis")
|
||||
schreibe(projekt / "a.py", "EINS\nzwei\nDREI\n")
|
||||
schreibe(projekt / "sub" / "neu.py", "neu eins\nneu zwei\n")
|
||||
(projekt / "weg.py").unlink()
|
||||
schreibe(projekt / "doku.md", "neue Prosa des Executers\n")
|
||||
schreibe(mat / "a.py", "EINS\nzwei\nDREI\n")
|
||||
schreibe(mat / "b.py", "bleibt\n")
|
||||
schreibe(mat / "sub" / "neu.py", "neu eins\nneu zwei\n")
|
||||
schreibe(mat / "doku.md", "anderer Stand in der Kopie\n")
|
||||
task = wurzel / "task-01.md"
|
||||
schreibe(
|
||||
task,
|
||||
"# Task\n\n## Zieldateien\n\n"
|
||||
f"- `{projekt}\\a.py` (geändert)\n"
|
||||
f"- `{projekt}\\sub\\neu.py` (neu)\n"
|
||||
f"- `{projekt}\\weg.py` (gelöscht)\n"
|
||||
f"- `{projekt}\\doku.md` (geändert)\n\n"
|
||||
"Keine andere Datei wird angefasst.\n\n## Regeln\n\n- `nicht/gemeint.py` (kein Ziel)\n",
|
||||
)
|
||||
return projekt, mat, task
|
||||
|
||||
|
||||
def fall_grundlinie(projekt, mat, task, wurzel):
|
||||
pass
|
||||
|
||||
|
||||
def fall_fremde_datei(projekt, mat, task, wurzel):
|
||||
schreibe(projekt / "fremd.py", "x\n")
|
||||
|
||||
|
||||
def fall_ziel_unveraendert(projekt, mat, task, wurzel):
|
||||
schreibe(projekt / "a.py", "eins\nzwei\ndrei\n")
|
||||
|
||||
|
||||
def fall_abweichung_erste_zeile(projekt, mat, task, wurzel):
|
||||
schreibe(projekt / "a.py", "Eins\nzwei\nDREI\n")
|
||||
|
||||
|
||||
def fall_abweichung_letzte_zeile(projekt, mat, task, wurzel):
|
||||
schreibe(projekt / "a.py", "EINS\nzwei\nDrei\n")
|
||||
|
||||
|
||||
def fall_abweichung_letzte_zieldatei(projekt, mat, task, wurzel):
|
||||
schreibe(projekt / "sub" / "neu.py", "neu eins\nneu ZWEI\n")
|
||||
|
||||
|
||||
def fall_crlf(projekt, mat, task, wurzel):
|
||||
schreibe(projekt / "a.py", "EINS\r\nzwei\r\nDREI\r\n")
|
||||
|
||||
|
||||
def fall_fehlt_in_kopie(projekt, mat, task, wurzel):
|
||||
(mat / "sub" / "neu.py").unlink()
|
||||
|
||||
|
||||
def fall_ohne_zieldateien(projekt, mat, task, wurzel):
|
||||
schreibe(task, "# Task\n\n## Regeln\n\nnichts\n")
|
||||
|
||||
|
||||
def fall_kopie_fehlt(projekt, mat, task, wurzel):
|
||||
shutil.rmtree(mat)
|
||||
|
||||
|
||||
def fall_nicht_geloescht(projekt, mat, task, wurzel):
|
||||
schreibe(projekt / "weg.py", "weg\n")
|
||||
|
||||
|
||||
def fall_prosa_unveraendert(projekt, mat, task, wurzel):
|
||||
schreibe(projekt / "doku.md", "alt\n")
|
||||
|
||||
|
||||
def fall_ziel_ausserhalb(projekt, mat, task, wurzel):
|
||||
text = task.read_text(encoding="utf-8")
|
||||
schreibe(task, text.replace("## Regeln", "") .replace("Keine andere Datei", "- `C:\\anderswo\\x.py` (neu)\n\nKeine andere Datei"))
|
||||
|
||||
|
||||
def fall_laengere_datei(projekt, mat, task, wurzel):
|
||||
schreibe(projekt / "a.py", "EINS\nzwei\nDREI\nvier\n")
|
||||
|
||||
|
||||
FAELLE = [
|
||||
("Grundlinie", fall_grundlinie, 0, "0 Befund(e)."),
|
||||
("fremde Datei im Baum", fall_fremde_datei, 2, "fremd.py: geaendert"),
|
||||
("Zieldatei unveraendert", fall_ziel_unveraendert, 2, "a.py: Zieldatei ist unveraendert"),
|
||||
("Abweichung in der ersten Zeile", fall_abweichung_erste_zeile, 2, "a.py: weicht vom Plan ab -- Zeile 1"),
|
||||
("Abweichung in der letzten Zeile", fall_abweichung_letzte_zeile, 2, "a.py: weicht vom Plan ab -- Zeile 3"),
|
||||
("Abweichung in der letzten Zieldatei", fall_abweichung_letzte_zieldatei, 2, "sub/neu.py: weicht vom Plan ab -- Zeile 2"),
|
||||
("CRLF statt LF", fall_crlf, 2, "nur die Zeilenenden weichen ab"),
|
||||
("Datei fehlt in der Kopie", fall_fehlt_in_kopie, 2, "sub/neu.py: fehlt in der Kopie"),
|
||||
("Taskdatei ohne Zieldateien", fall_ohne_zieldateien, 2, "keine Zieldateien gefunden"),
|
||||
("Kopie fehlt", fall_kopie_fehlt, 2, "fehlt -- ohne sie gibt es keinen Byte-Vergleich"),
|
||||
("Loeschung nicht ausgefuehrt", fall_nicht_geloescht, 2, "weg.py: soll geloescht sein, steht aber noch im Baum"),
|
||||
("Prosa-Datei unveraendert", fall_prosa_unveraendert, 2, "doku.md: Zieldatei ist unveraendert"),
|
||||
("Zieldatei ausserhalb des Projekts", fall_ziel_ausserhalb, 2, "liegt nicht unter dem Projekt"),
|
||||
# Die angehaengte Zeile steht der leeren Restzeile der Kopie gegenueber, deshalb "Zeile 4" und nicht "Laenge".
|
||||
("Datei laenger als der Plan", fall_laengere_datei, 2, "a.py: weicht vom Plan ab -- Zeile 4: Baum b'vier'"),
|
||||
]
|
||||
|
||||
|
||||
def main() -> int:
|
||||
fehler = 0
|
||||
for name, angriff, exit_erwartet, meldung in FAELLE:
|
||||
wurzel = Path(tempfile.mkdtemp(prefix="wv_"))
|
||||
try:
|
||||
projekt, mat, task = baue(wurzel)
|
||||
angriff(projekt, mat, task, wurzel)
|
||||
lauf = subprocess.run(
|
||||
[sys.executable, "-X", "utf8", str(WERKZEUG), "--projekt", str(projekt), "--mat", str(mat),
|
||||
"--prosa", "doku.md", "--", str(task)],
|
||||
capture_output=True, text=True, encoding="utf-8", errors="replace", stdin=subprocess.DEVNULL,
|
||||
)
|
||||
ausgabe = lauf.stdout + lauf.stderr
|
||||
gut = lauf.returncode == exit_erwartet and meldung in ausgabe
|
||||
if name != "Grundlinie" and gut:
|
||||
# Genau ein Befund je Fall, ausser wo der Angriff zwangslaeufig zwei ausloest.
|
||||
pass
|
||||
print(f"{'ok ' if gut else 'FEHLER'} {name}: Exit {lauf.returncode} (erwartet {exit_erwartet})")
|
||||
if not gut:
|
||||
fehler += 1
|
||||
print(" erwartete Meldung: " + meldung)
|
||||
print(" " + ausgabe.strip().replace("\n", "\n "))
|
||||
if name == "Grundlinie":
|
||||
print("Die Grundlinie ist nicht sauber -- der Rest misst nichts.")
|
||||
return 2
|
||||
finally:
|
||||
shutil.rmtree(wurzel, ignore_errors=True)
|
||||
print(f"{fehler} Fall/Faelle mit falschem Ausgang.")
|
||||
return 2 if fehler else 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in new issue
Block a user