Automatischer Push: 11 Datei(en) geändert
- .claude/agents/planer.md - .claude/workflows/plankritik.js - .claude/workflows/quorum.js - .claude/workflows/welle-mit-pruefung.js - .vault-tools/plan-kopie/LIESMICH.md - .vault-tools/plan-kopie/plan_bloecke.py - .vault-tools/plan-kopie/plan_materialisiere.py - .vault-tools/plan-kopie/plan_zaun_treiber.py - .vault-tools/workflow-pruefung/LIESMICH.md - .vault-tools/workflow-pruefung/faelle_workflows.mjs - .vault-tools/workflow-pruefung/mutanten_workflows.py 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
7f484f9be5
commit
22761eb186
11 files changed
+1520
-65
No files matched your search
@@ -5,7 +5,7 @@ title: "Planwerkzeuge: Plan auf eine Kopie anwenden und jede Welle dagegen vergl
|
||||
summary: "Skripte für die Standardschleife bei Plänen mit ausgeschriebenem Code — schlanke Kopie bauen, Plan auf die 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-10-17
|
||||
updated: 2026-11-02
|
||||
---
|
||||
|
||||
# Planwerkzeuge für die Kopie mit angewendetem Plan
|
||||
@@ -14,8 +14,8 @@ Die Regel dazu steht in [[AI/Regelwerk/codeworkflow|codeworkflow.md]] („Die Ko
|
||||
|
||||
## 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. **Beides erledigt `kopie_bauen.py <projekt> <ziel> --ohne <relpfad> …`:** Es nimmt Ausschlüsse nur als Pfade vom Wurzelordner, meldet einen Ausschluss ohne Gegenstück („ein Name statt eines Pfads?"), überspringt Verknüpfungen und Junctions, lässt `__pycache__` weg, weigert sich bei einem vorhandenen Ziel und vergleicht am Ende die Python-Pakete beider Bäume. Für die Trading Plattform: `--ohne .git dashboard/node_modules dashboard/dist data artifacts config/secrets.toml docs/plans`. 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. Eine Datei ganz ohne Endung (`host/logrotate/edge-traefik`, `Dockerfile`, `.gitignore`) ist Ziel, wenn derselbe Pfad unter `## Zieldateien` steht. Fehlt der Eintrag, ist ihr Block ein Befund mit Exit 2, außer bei einer neuen Datei ohne Schrägstrich (unten, „Seit 2026-10-17").
|
||||
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. **Beides erledigt `kopie_bauen.py <projekt> <ziel> --ohne <relpfad> …`:** Es nimmt Ausschlüsse nur als Pfade vom Wurzelordner, meldet einen Ausschluss ohne Gegenstück („ein Name statt eines Pfads?"), überspringt Verknüpfungen und Junctions, lässt `__pycache__` weg, weigert sich bei einem vorhandenen Ziel und vergleicht am Ende die Python-Pakete beider Bäume. Für die Trading Plattform: `--ohne .git dashboard/node_modules dashboard/dist data artifacts config/secrets.toml docs/plans`. 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. Auch Vitest schreibt hinter die Junction: Vite legt beim Laden der Konfiguration `node_modules/.vite-temp` an und löscht die Datei danach, unabhängig von `cacheDir` (gemessen 2026-11-02 im Nexhorizons Hub, Vite 8.3.0); `cacheDir` in einer Wegwerf-Konfiguration hält nur den eigentlichen Cache aus dem fremden `node_modules` heraus.
|
||||
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. Vor dem ersten Schreibvorgang prüft es die Zäune aller übergebenen Taskdateien; ein kaputter Zaun ist ein Befund mit Exit 2, und der Lauf schreibt nichts (unten, „Seit 2026-11-02“). Eine Datei ganz ohne Endung (`host/logrotate/edge-traefik`, `Dockerfile`, `.gitignore`) ist Ziel, wenn derselbe Pfad unter `## Zieldateien` steht. Fehlt der Eintrag, ist ihr Block ein Befund mit Exit 2, außer bei einer neuen Datei ohne Schrägstrich (unten, „Seit 2026-10-17").
|
||||
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". Eine Zieldatei, die git ignoriert (etwa Prüfdateien unter `tests/.work/`), vergleicht es seit 2026-10-17 ebenfalls byte-genau mit der Kopie und zählt sie in der Schlusszeile als „von git ignoriert"; vorher meldete es sie als „unveraendert gegenueber HEAD" und verglich sie nicht.
|
||||
@@ -62,6 +62,30 @@ Nachbesserung vom 2026-10-09 (Paket RB3 der Trading Plattform, Plan `docs/plans/
|
||||
|
||||
Seit 2026-10-11 schreibt `plan_materialisiere.py` nur noch innerhalb der Kopie. Vorher schlug ein absoluter Pfad in einer Überschrift unter Windows die Kopie (`kopie / "D:/…"` ist der absolute Pfad): Das Skript ersetzte dann die echte Datei, mit Exit 0 und „0 Befund(e)“. Gemessen an einer Probe-Datei im Scratchpad. Entdeckt beim Planen von Paket E9 der Trading Plattform, dessen Tasks 725 und 726 Dateien unter `D:/Claude/.vault-tools/` ändern. Jetzt ist ein Ziel außerhalb der Kopie (absolut oder mit `..`) ein Befund mit Exit 2, und nichts wird geschrieben. Belegt am selben Tag mit drei Fällen (Aus/wird absolut, ganze Datei absolut, `../`): je Befund, die Dateien draußen unverändert; und mit einer Gegenprobe: Die Tasks 723 und 724 von E9 ergeben in einer frischen Kopie dieselben Zählungen und byte-gleiche Dateien. Pläne für Dateien außerhalb des Projekts nennen sie deshalb relativ zu einem Wurzelordner (etwa `.vault-tools/trading-plattform-pruefung/abendkontrolle.py` gegen eine Kopie, die diesen Ordner enthält).
|
||||
|
||||
Seit 2026-11-02 prüfen `plan_materialisiere.py` und `plan_bloecke.py` die Zäune einer Taskdatei, bevor sie einen Block anfassen. Anlass war Auftrag 13 der Kette 050 (Trading Plattform) am 2026-11-01: In `task-1020` klebte der schließende Zaun zweimal an `])`. Der Block lief bis in den nächsten Task, das erste Aus/wird-Paar dort ging ohne eigene Meldung verloren, und das Werkzeug sagte nur „Aus-Block steht 0-mal“. Das sah nach einem falschen Anker aus. Unter „Die ganze Datei ist neu“ schrieb derselbe Fehler Planprosa in die Zieldatei, mit Exit 0 und „0 Befund(e)“. Die Regel jetzt, `n` ist die Zaunlänge des Blocks:
|
||||
- Ein Block schließt mit mindestens `n` Backticks in einer Zeile ohne weiteren Text, höchstens drei Spalten tiefer als der Öffner (CommonMark 0.31.2). Tiefer ist es Inhalt. Vorher schloss jeder Einzug, auch ein Zaun in einer eingerückten Zeichenkette, und auch zwei Zäune auf einer Zeile.
|
||||
- Befund ist eine Zeile im Block, die auf mindestens `n` Backticks endet und nicht schließt (klebender Zaun, auch ein zu tief eingerückter).
|
||||
- Befund ist eine Zeile im Block, die mit mindestens `n` Backticks beginnt und nicht schließt: Der Schlusszaun fehlt, oder der Außenzaun ist zu kurz.
|
||||
- Befund ist ein Block, der bis zum Dateiende nicht schließt, und eine Taskdatei, die sich nicht lesen lässt.
|
||||
- `plan_materialisiere.py` prüft alle übergebenen Taskdateien vor dem ersten Schreibvorgang. Bei einem Befund schreibt und löscht der ganze Lauf nichts und endet mit Exit 2, auch die heilen Taskdateien davor bleiben ungeschrieben. Die Zählzeile je Task entfällt dann.
|
||||
- `plan_bloecke.py` meldet denselben Befund und prüft die Blöcke dieser Taskdatei nicht, die übrigen Taskdateien prüft es weiter. Ein Zaunbefund endet immer mit Exit 2, auch wenn sonst nichts geprüft wurde. Die Blocksuche ist jetzt dieselbe wie in `plan_materialisiere.py` (`zaun()` und `schliesst()`), vorher sah es nur Zäune in Spalte 0.
|
||||
- Eine Zeile mit weiterem Backtick hinter dem Zaun öffnet nichts, sie ist Inline-Code (CommonMark 0.31.2: „Info strings for backtick code blocks cannot contain backticks“). Vorher schluckte so eine Zeile den nächsten Block.
|
||||
- Bewusst entschieden: Ein Text im Block, etwa eine Zeichenkette in einem Test, der auf `n` Backticks endet oder mit ihnen beginnt, ist derselbe Befund. Von einem klebenden Zaun ist er nicht zu unterscheiden. Abhilfe ist ein längerer Zaun um den Block. Beim Text am Zeilenende steht oft ein Folgebefund weiter hinten, weil der echte Schlusszaun für die Suche einen Block öffnet; der längere Zaun löst beide.
|
||||
- Nicht entscheidbar: Ein nackter Zaun als eigene Zeile im Text, höchstens drei Spalten tiefer als der Öffner, schließt den Block. Geht die Zahl der Zäune auf, wird der Rest Prosa, und es kommt kein Befund. Text mit Zäunen braucht deshalb immer einen längeren Zaun um den Block. Der Treiberfall „Luecke: nackter Zaun als Zeile im Text“ hält das fest.
|
||||
- Kein Befund sind Markdown-Vorlagen in längeren Zäunen mit `##`-Überschriften und kürzeren Zäunen darin, eingerückte Blöcke, Inline-Code und Zaunfolgen mitten in der Zeile oder in Anführungszeichen, Prosa außerhalb von Blöcken.
|
||||
- `welle_vergleich.py` liest keine Blöcke, nur `## Zieldateien`, und bleibt unverändert. `plan_zeichen.py` und `plan-diff/plan_diff.py` haben je eine eigene Blocksuche mit derselben Lücke. Sie schreiben nichts und bleiben unverändert; was ein kaputter Zaun dort bewirkt, ist nicht gefahren, nur am Quelltext gelesen.
|
||||
|
||||
Belegt am 2026-11-02 (Rohdaten und Messskripte: `AI/secondbrain/regelwerk-memory/2026-11-01-zaunpruefung/`):
|
||||
- `plan_zaun_treiber.py [--werkzeuge <ordner>] [--mutanten]` fährt 114 Fälle, die Grundlinie zuerst, und prüft je Fall Exit, jede Befundzeile samt Zeilennummer, die Zahl der Befunde, die Summenzeile und jede Datei der Kopie. Die Angriffe aus `paar()` haben ein geheiltes Gegenstück, das schreiben muss; sonst wäre „nichts geschrieben“ auch grün, wenn die Fixture nichts anwendet. Gefahren unter Windows und in WSL Debian.
|
||||
- Alte Fassung 65 von 114 Fällen rot, neue 0. `plan_ziel_treiber.py` bleibt bei 33 von 33.
|
||||
- `--mutanten` baut 39 Fehler ein, jeder ist an seinem Fall gefangen.
|
||||
- `task-1020` vor der Korrektur: genau zwei Befunde an den Zeilen 1678 und 1729, die Kopie bleibt unverändert. Die alte Fassung meldete dort 80 Folgefehler und schrieb fünf Dateien.
|
||||
- Alle Taskdateien im Vault und in den Jobordnern (2 253 verschiedene, 59 790 Blöcke): `bloecke()` alt und neu liefern dasselbe. Die Zaunprüfung meldet acht Zeilen in drei alten Doku-Tasks (`task-89` und `task-94` der Trading Plattform, `task-08` von After Install). Alle acht sind echt: Der Außenzaun war zu kurz, der Block endete zu früh. `plan_bloecke.py vor` weicht in genau diesen drei von 188 Planordnern ab.
|
||||
- Kette 050: alle 50 Lieferungen, angewendet wie in `kette_050.py` (Basis plus transitive Vorgänger), mit alter und neuer Fassung: gleiche Meldungen, gleiche Bäume. Die eingesetzte Fassung meldet in den 52 Taskdateien der Lieferungen keinen Zaunbefund, `zaun_scan.py` kein `KLEBT` und kein `OFFEN`. Seine `STRUKTUR`-Treffer sind Markdown-Überschriften als Inhalt geschlossener `markdown`- und `text`-Blöcke und bleiben ohne Befund.
|
||||
- Zustandssuche über die echten Funktionen (`messung/zaun_suche.py`, 40 000 zufällige heile Dateien, je ein Zaun beschädigt): keine heile Datei mit Befund, wobei der Generator in Dreierblöcken keine inneren Zäune erzeugt; alle 4 915 klebenden Zäune gefunden.
|
||||
- Ein `pruefer` fand keinen blockierenden Befund. Eingearbeitet sind der Einzug, die zwei Zäune auf einer Zeile, die weitere Regel für den Zeilenanfang und die Lücken seiner Treiberfälle.
|
||||
- Lücken: Ein nackter Zaun als Öffner ist von einem Schlusszaun nicht zu unterscheiden. Fehlt der Schlusszaun und kommt als Nächstes ein nackter Zaun derselben oder größerer Länge, schließt der den Block, und die Rollen tauschen bis zum nächsten Öffner mit Sprache oder bis zum Dateiende; zwei solche Fehler heben sich auf. In der Zufallssuche blieben rund 2 % der fehlenden Schlusszäune still, rund 13 % der fehlenden oder klebenden Öffner. Dasselbe gilt für einen Backtick in der Sprachangabe, für Zäune hinter Listenpunkt oder Zitatzeichen und für Backtick-Zäune in Tilde-Blöcken.
|
||||
|
||||
Seit 2026-10-17 geht ein Block nicht mehr still in die falsche Datei, wenn seine Überschrift eine Datei ohne Endung nennt. Beide Werkzeuge nutzen dafür `ordne()` in `plan_materialisiere.py`; `plan_bloecke.py` importiert die Funktion. Anlass war die Plankritik von Paket R1 im Edge-Stack (Befunde 1 und 5): Die Überschrift „Stelle 30 — `host/logrotate/edge-traefik`" setzte kein Ziel, das Ziel blieb bei `tests/systemd/test-units.sh` aus Stelle 29, und der Block „Die ganze Datei ist neu" überschrieb diese Datei, mit Exit 0 und „0 Befund(e)". Die Regel jetzt:
|
||||
- Ziel ist der letzte PFAD-Treffer der Überschrift. Ohne ihn ist Ziel der letzte Token, der (ohne `./`) genau so unter `## Zieldateien` derselben Taskdatei steht. So wird eine Datei ohne Endung planbar: `host/logrotate/edge-traefik`, `Dockerfile`, `.gitignore`.
|
||||
- Ein absoluter Eintrag unter Zieldateien zählt relativ zum Projekt. Das ist in `plan_bloecke.py` `--projekt`, wie in `welle_vergleich.py`, und in `plan_materialisiere.py` der Ordner vor `docs/plans` im Pfad des Planordners. Ordner, Muster mit `*` und Pfade mit `..` unter Zieldateien sind kein Ziel.
|
||||
@@ -96,6 +120,6 @@ Offen seit 2026-09-23: `plan_materialisiere.py` stirbt mit `FileNotFoundError`,
|
||||
|
||||
## Was die Skripte nicht sehen
|
||||
|
||||
Dateien, die `.gitignore` ausblendet und die keine Zieldatei sind; bei einer ignorierten Zieldatei, ob sie vor der Welle schon so aussah; 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 „с"); eine neue Zieldatei ohne Endung und ohne Schrägstrich, die nicht unter `## Zieldateien` steht — ihr Block geht still ins vorige Ziel. Sie sind ein Fangnetz, kein Nachweis — den liefert der Lauf mit echten Daten.
|
||||
Dateien, die `.gitignore` ausblendet und die keine Zieldatei sind; bei einer ignorierten Zieldatei, ob sie vor der Welle schon so aussah; 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 „с"); eine neue Zieldatei ohne Endung und ohne Schrägstrich, die nicht unter `## Zieldateien` steht — ihr Block geht still ins vorige Ziel; ein nackter Zaun als Zeile im Text, ein fehlender Schlusszaun vor einem nackten Zaun und ein fehlender Öffner. 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.
|
||||
@@ -13,8 +13,14 @@ Koordinator-Werkzeug im Scratchpad. Geltungsbereich (Fangnetz, kein Nachweis):
|
||||
Schraegstrich; Name, der direkt in --projekt als Datei liegt oder Dateiname eines Eintrags ist), ist
|
||||
jeder Aus- und wird-Block danach ein Befund, bis eine Ueberschrift mit Ziel kommt; vorher wurde still
|
||||
gegen das vorige Ziel geprueft. Luecken wie in plan_materialisiere.py; Nachweis: plan_ziel_treiber.py.
|
||||
- Zaunpruefung (seit 2026-11-02, Regel und Luecken im Kopf von plan_materialisiere.py, Nachweis: plan_zaun_treiber.py):
|
||||
Klebt ein Zaun, fehlt einer oder schliesst ein Block nie, ist das ein Befund, und die Bloecke dieser Taskdatei
|
||||
werden nicht geprueft: Hinter dem Zaun stimmt die Datei nicht mehr, und ein still geschluckter Aus-Block fiele
|
||||
sonst nur als "steht nicht in ..." auf. Vorher lief hier eine eigene Blocksuche nur in Spalte 0; seitdem gilt
|
||||
dieselbe wie in plan_materialisiere.py (zaun() und schliesst()), Aus- und wird-Bloecke zaehlen weiter nur mit
|
||||
Zaun in Spalte 0.
|
||||
- Exit 3 hat Vorrang: Wurde in keiner Taskdatei ein Block gegen eine Datei geprueft, endet es mit 3,
|
||||
auch wenn Befunde ausgegeben sind.
|
||||
auch wenn Befunde ausgegeben sind. Ausnahme ist ein Befund der Zaunpruefung, dort bleibt es bei 2.
|
||||
- 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,
|
||||
@@ -30,12 +36,11 @@ import re
|
||||
import sys
|
||||
from pathlib import Path, PureWindowsPath
|
||||
|
||||
# Die Zielregel (PFAD, Zieldateien, offene Token) steht einmal, in plan_materialisiere.py im selben Ordner;
|
||||
# Zielregel und Zaunregel stehen einmal, in plan_materialisiere.py im selben Ordner;
|
||||
# der Ordner kommt ausdruecklich in den Suchpfad, sonst scheitert der Import unter "python -I"
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||
from plan_materialisiere import ordne, zieldateien # noqa: E402
|
||||
from plan_materialisiere import ordne, schliesst, zaun, zaunbefunde, zieldateien # noqa: E402
|
||||
|
||||
ZAUN = re.compile(r"^```")
|
||||
UEBERSCHRIFT = re.compile(r"^#{2,4} ")
|
||||
|
||||
|
||||
@@ -60,16 +65,15 @@ def lies_bloecke(taskdatei: Path, projekt: Path) -> list[dict]:
|
||||
ziel, ungeklaert = neu, None
|
||||
elif offen:
|
||||
ungeklaert = (i + 1, offen)
|
||||
if ZAUN.match(zeile):
|
||||
laenge = zaun(zeile)
|
||||
if laenge:
|
||||
start = i
|
||||
einzug = len(zeile) - len(zeile.lstrip())
|
||||
# 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()
|
||||
):
|
||||
while i < len(zeilen) and not schliesst(zeilen[i], laenge, einzug):
|
||||
inhalt.append(zeilen[i])
|
||||
i += 1
|
||||
marke = ""
|
||||
@@ -78,7 +82,7 @@ def lies_bloecke(taskdatei: Path, projekt: Path) -> list[dict]:
|
||||
k -= 1
|
||||
if k >= 0:
|
||||
marke = zeilen[k].strip()
|
||||
if marke in ("Aus:", "wird:"):
|
||||
if marke in ("Aus:", "wird:") and zeile.startswith("`"):
|
||||
bloecke.append({"art": marke[:-1], "ziel": ziel, "zeile": start + 1, "text": "\n".join(inhalt),
|
||||
"ungeklaert": ungeklaert})
|
||||
i += 1
|
||||
@@ -95,8 +99,16 @@ def main() -> int:
|
||||
|
||||
befunde = 0
|
||||
geprueft = 0
|
||||
zaunfehler = False
|
||||
for name in argumente.taskdateien:
|
||||
taskdatei = Path(name)
|
||||
kaputt = zaunbefunde(taskdatei)
|
||||
if kaputt:
|
||||
for zeile, meldung in kaputt:
|
||||
print(f"BEFUND {taskdatei.name}:{zeile}: {meldung}; Bloecke dieser Taskdatei nicht geprueft")
|
||||
befunde += len(kaputt)
|
||||
zaunfehler = True
|
||||
continue
|
||||
bloecke = lies_bloecke(taskdatei, projekt)
|
||||
paare = 0
|
||||
for nummer, block in enumerate(bloecke):
|
||||
@@ -143,7 +155,7 @@ def main() -> int:
|
||||
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:
|
||||
if geprueft == 0 and not zaunfehler:
|
||||
print("Nichts geprueft: kein Aus-/wird-Block gefunden.")
|
||||
return 3
|
||||
print(f"{befunde} Befund(e).")
|
||||
|
||||
@@ -30,7 +30,40 @@ Geltungsbereich:
|
||||
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.
|
||||
- Zaeune aus n Backticks schliessen erst mit n Backticks, in einer Zeile ohne weiteren Text und hoechstens drei
|
||||
Spalten tiefer als der Oeffner; tiefer ist es Inhalt. Eine Zeile mit weiterem Backtick hinter dem Zaun oeffnet
|
||||
nichts, sie ist Inline-Code. Beides nach CommonMark 0.31.2, Abschnitt Fenced code blocks, nachgelesen 2026-11-01
|
||||
("may be preceded by up to three spaces of indentation", "Info strings for backtick code blocks cannot contain
|
||||
backticks"). Abweichend davon oeffnet ein Zaun bei jeder Einrueckung, und Tilde-Zaeune kennt das Werkzeug nicht.
|
||||
- Zaunpruefung (seit 2026-11-02, Anlass in zaunbefunde()): Vor dem ersten Schreibvorgang liest zaunbefunde() alle
|
||||
uebergebenen Taskdateien. Ein Befund in einer davon, und der ganze Lauf schreibt und loescht nichts, auch nicht
|
||||
die heilen Dateien davor. Befund sind, mit n als Zaunlaenge des Blocks:
|
||||
1. eine Zeile im Block, die auf mindestens n Backticks endet, ohne zu schliessen (klebender Zaun, auch ein
|
||||
Zaun mehr als drei Spalten tiefer als der Oeffner),
|
||||
2. eine Zeile im Block, die mit mindestens n Backticks beginnt, ohne zu schliessen (Schlusszaun fehlt,
|
||||
Aussenzaun zu kurz),
|
||||
3. ein Block, der bis zum Dateiende nicht schliesst,
|
||||
4. eine Taskdatei, die sich nicht lesen laesst.
|
||||
Herkunft: 1 und 3 stehen im Auftrag zur Haertung. 2 ist Projektvorgabe, denselben stillen Schaden richtet ein
|
||||
fehlender Zaun an. 4 ist Projektvorgabe nach AI/Regelwerk/pruefwerkzeuge.md, vorher stuerzte der Lauf mittendrin ab.
|
||||
Bewusst entschieden: Ein Text im Block, etwa eine Zeichenkette in einem Test, die am Zeilenende auf n Backticks
|
||||
endet oder mit ihnen beginnt, ist derselbe Befund. Von einem klebenden Zaun ist er nicht zu unterscheiden, und die
|
||||
Folgen sind ungleich: Der Fehlalarm kostet einen laengeren Zaun um den Block, der uebersehene Zaun schreibt
|
||||
Planprosa in die Zieldatei. Gemessen am 2026-11-02 ueber alle verschiedenen Taskdateien im Vault und in den
|
||||
Jobordnern: 1 und 3 ohne Treffer, 2 acht Zeilen in drei alten Doku-Tasks, alle echt (Aussenzaun zu kurz, der
|
||||
Block endete zu frueh).
|
||||
Nicht entscheidbar ist ein nackter Zaun als eigene Zeile im Text, hoechstens drei Spalten tiefer als der Oeffner:
|
||||
Er schliesst den Block. Geht die Zahl der Zaeune auf, wird der Rest Prosa, der Block endet dort, und es kommt
|
||||
kein Befund (Treiberfall "Luecke:"). Text mit Zaeunen braucht deshalb immer einen laengeren Zaun um den Block.
|
||||
Kein Befund: laengere Zaeune um Markdown-Vorlagen mit ##-Ueberschriften und kuerzeren Zaeunen darin, Inline-Code
|
||||
und Zaunfolgen mitten in der Zeile oder in Anfuehrungszeichen, Prosa ausserhalb von Bloecken.
|
||||
Nicht gesehen (Fangnetz, kein Nachweis): Ein nackter Zaun als Oeffner ist von einem Schlusszaun nicht zu
|
||||
unterscheiden. Fehlt der Schlusszaun und kommt als naechstes ein nackter Zaun derselben oder groesserer Laenge,
|
||||
schliesst der den Block, und die Rollen tauschen bis zum naechsten Oeffner mit Sprache (das meldet 2) oder bis
|
||||
zum Dateiende (das meldet 3 am letzten Zaun, zwei solche Fehler heben sich auf). Ebenso bei einem Oeffner, der
|
||||
fehlt, an der Zeile davor klebt oder einen Backtick in der Sprachangabe traegt, bei Zaeunen hinter Listenpunkt
|
||||
oder Zitatzeichen und bei Backtick-Zaeunen in Tilde-Bloecken. Nachweis: plan_zaun_treiber.py. plan_bloecke.py
|
||||
nutzt dieselben Funktionen.
|
||||
- Geschrieben wird immer mit LF.
|
||||
Aufruf: python plan_materialisiere.py <planordner> <kopie> <taskdatei> [<taskdatei> ...]
|
||||
Exit 0 ohne Befund, 2 mit Befund.
|
||||
@@ -133,6 +166,65 @@ def normal(text: str) -> str:
|
||||
return text.replace("\r\n", "\n").replace("\r", "\n")
|
||||
|
||||
|
||||
def zaun(zeile: str) -> int:
|
||||
"""Laenge des Zauns, der in dieser Zeile einen Block oeffnet, sonst 0."""
|
||||
kern = zeile.lstrip()
|
||||
laenge = len(kern) - len(kern.lstrip("`"))
|
||||
return laenge if laenge >= 3 and "`" not in kern[laenge:] else 0
|
||||
|
||||
|
||||
def schliesst(zeile: str, laenge: int, einzug: int) -> bool:
|
||||
"""Schliesst die Zeile einen Block mit Zaun der Laenge laenge und Einzug einzug?
|
||||
|
||||
Mindestens so viele Backticks, sonst nichts in der Zeile, hoechstens drei Spalten tiefer als der Oeffner.
|
||||
Nachbesserung 2026-11-02 (Pruefung der Zaunpruefung): Vorher schloss jeder Einzug und "``` ```". Ein eingerueckter
|
||||
Zaun in einer Zeichenkette kappte den Block mit 0 Befunden, und plan_bloecke.py hatte das bis dahin gesehen.
|
||||
"""
|
||||
kern = zeile.lstrip()
|
||||
return kern.startswith("`" * laenge) and not kern.rstrip().strip("`") and len(zeile) - len(kern) - einzug <= 3
|
||||
|
||||
|
||||
def zaunbefunde(taskdatei: Path) -> list[tuple[int, str]]:
|
||||
"""(Zeile, Meldung) je Stelle, an der die Bloecke der Taskdatei nicht stimmen; leer heisst: bloecke() liest sie richtig.
|
||||
|
||||
Nachbesserung 2026-11-01 (Kette 050 der Trading Plattform, Auftrag 13): In task-1020 klebte der Zaun an `])`,
|
||||
der Block lief bis in den naechsten Task, es kam nur "Aus-Block steht 0-mal", und ein Aus/wird-Paar ging ohne
|
||||
Meldung verloren. Unter "Die ganze Datei ist neu" schrieb derselbe Fehler Planprosa mit 0 Befunden in die Datei.
|
||||
Fuer die Meldung endet ein Block am klebenden Zaun, und ein Zaun am Zeilenanfang im Block beginnt einen neuen: So
|
||||
steht jeder Fehler einmal da, und die Bloecke dahinter stimmen wieder. War es doch ein Text mit Zaunfolge, oeffnet
|
||||
der echte Schlusszaun fuer diese Suche einen Block, und ein Folgebefund steht weiter hinten; der laengere Zaun um
|
||||
den Block loest beide.
|
||||
"""
|
||||
try:
|
||||
zeilen = normal(taskdatei.read_text(encoding="utf-8")).split("\n")
|
||||
except (OSError, UnicodeDecodeError) as fehler:
|
||||
return [(0, f"Taskdatei nicht lesbar ({fehler})")]
|
||||
befunde = []
|
||||
i = 0
|
||||
while i < len(zeilen):
|
||||
laenge = zaun(zeilen[i])
|
||||
start = i
|
||||
i += 1
|
||||
if not laenge:
|
||||
continue
|
||||
spalte = len(zeilen[start]) - len(zeilen[start].lstrip())
|
||||
while i < len(zeilen) and not schliesst(zeilen[i], laenge, spalte):
|
||||
if zeilen[i].rstrip().endswith("`" * laenge):
|
||||
befunde.append((i + 1, f"Zaun klebt an der Codezeile, Block ab Zeile {start + 1}; der Schlusszaun "
|
||||
"gehoert auf eine eigene Zeile, ein Text mit Zaunfolge in einen laengeren Zaun"))
|
||||
break
|
||||
kern = zeilen[i].lstrip()
|
||||
if kern.startswith("`" * laenge):
|
||||
befunde.append((i + 1, f"hier oeffnet ein Block, der Block ab Zeile {start + 1} ist aber nicht geschlossen; "
|
||||
"es fehlt sein Schlusszaun, oder der Text braucht einen laengeren Zaun"))
|
||||
start, laenge, spalte = i, len(kern) - len(kern.lstrip("`")), len(zeilen[i]) - len(kern)
|
||||
i += 1
|
||||
if i >= len(zeilen):
|
||||
befunde.append((start + 1, f"Block schliesst nie (Zaun aus {laenge} Backticks)"))
|
||||
i += 1
|
||||
return befunde
|
||||
|
||||
|
||||
def bloecke(taskdatei: Path, kopie: Path | None = None):
|
||||
zeilen = normal(taskdatei.read_text(encoding="utf-8")).split("\n")
|
||||
geplant = zieldateien(zeilen, wurzeln(taskdatei.parent))
|
||||
@@ -149,17 +241,13 @@ def bloecke(taskdatei: Path, kopie: Path | None = None):
|
||||
ziel, ungeklaert = neu, None
|
||||
elif offen:
|
||||
ungeklaert = (i + 1, offen)
|
||||
if zeile.lstrip().startswith("```"):
|
||||
laenge = zaun(zeile)
|
||||
if laenge:
|
||||
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
|
||||
while i < len(zeilen) and not schliesst(zeilen[i], laenge, einzug):
|
||||
inhalt.append(zeilen[i][einzug:] if zeilen[i][:einzug].strip() == "" else zeilen[i])
|
||||
i += 1
|
||||
k = start - 1
|
||||
@@ -219,6 +307,14 @@ def loesche(taskdatei: Path, kopie: Path) -> int:
|
||||
def main() -> int:
|
||||
plan, kopie = Path(sys.argv[1]), Path(sys.argv[2])
|
||||
befunde = 0
|
||||
# Erst alle Zaeune aller Taskdateien, dann der erste Schreibvorgang: Hinter einem kaputten Zaun stimmt die Datei nicht mehr
|
||||
for name in sys.argv[3:]:
|
||||
for zeile, meldung in zaunbefunde(plan / name):
|
||||
print(f"BEFUND {name}:{zeile}: {meldung}; nichts geschrieben")
|
||||
befunde += 1
|
||||
if befunde:
|
||||
print(f"{befunde} Befund(e).")
|
||||
return 2
|
||||
for name in sys.argv[3:]:
|
||||
taskdatei = plan / name
|
||||
alt = None
|
||||
|
||||
@@ -0,0 +1,377 @@
|
||||
"""Regressionsnachweis fuer die Zaunpruefung von plan_materialisiere.py und plan_bloecke.py.
|
||||
|
||||
Anlass 2026-11-01 (Kette 050 der Trading Plattform, Auftrag 13): In task-1020 klebte der schliessende Zaun
|
||||
zweimal an der Codezeile. Der Block lief weiter, schluckte das naechste Aus/wird-Paar ohne eigene Meldung, und
|
||||
das Werkzeug sagte nur "Aus-Block steht 0-mal". Unter "Die ganze Datei ist neu" schrieb es Planprosa in die
|
||||
Zieldatei und meldete 0 Befunde. Die Faelle unten bauen diese Form nach, an erster und an letzter Stelle, je
|
||||
schreibender Marke, im eingerueckten und im laengeren Block, mit CRLF, ueber zwei Taskdateien, dazu den Block,
|
||||
der nie schliesst, und den fehlenden Zaun vor dem naechsten Block.
|
||||
Die Angriffe aus paar() haben ein geheiltes Gegenstueck, das sich nur im Zaun unterscheidet und schreiben muss:
|
||||
Sonst waere "nichts geschrieben" auch dann gruen, wenn die Fixture von vornherein nichts anwendet. Die Gegenfaelle
|
||||
ohne Befund sind echte Inhalte: Markdown-Vorlagen in laengeren Zaeunen mit ##-Ueberschriften, eingerueckte Bloecke,
|
||||
Inline-Code, Zeichenketten mit Zaunfolge. Faelle mit "Luecke:" im Namen halten fest, was das Werkzeug bewusst nicht
|
||||
sieht: Wird einer rot, ist die Luecke zu und der Kopfkommentar von plan_materialisiere.py nachzuziehen.
|
||||
Grundlinie zuerst, dann jeder Fall einzeln in einem frischen Wegwerfordner. Je Fall werden Exit-Code, jede
|
||||
erwartete Befundzeile samt Zeilennummer (aus dem Text der Taskdatei gerechnet), die Zahl der BEFUND-Zeilen, die
|
||||
Summenzeile und bei plan_materialisiere.py jede Datei der Kopie geprueft.
|
||||
--mutanten baut die Fehler aus MUTANTEN je in eine Kopie der Werkzeuge ein und verlangt, dass mindestens ein Fall
|
||||
rot wird; ein Mutant, dessen Zeile nicht genau einmal dasteht, ist selbst ein Fehler.
|
||||
Aufruf: python plan_zaun_treiber.py [--werkzeuge <ordner>] [--mutanten] Vorgabe: der Ordner dieses Skripts.
|
||||
Exit 0, wenn jeder Fall so ausgeht wie erwartet und jeder Mutant gefangen ist, sonst 2.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
BASIS = {
|
||||
"tests/a.sh": "eins\nzwei\ndrei\n",
|
||||
"tests/b.sh": "vier\nfuenf\n",
|
||||
"tests/c.py": "@pytest.mark.parametrize(\"pfad\", [\n pytest.param(pfad)\n])\n",
|
||||
"README.md": "# Projekt\n",
|
||||
}
|
||||
Z = "```"
|
||||
KLEBT = "Zaun klebt"
|
||||
FEHLT = "nicht geschlossen"
|
||||
OFFEN = "schliesst nie"
|
||||
LESEN = "nicht lesbar"
|
||||
KOPF = "# Task 01\n\n## Prüfstand\n\n"
|
||||
A = "#### Stelle 1 — `tests/a.sh`"
|
||||
B = "#### Stelle 2 — `tests/b.sh`"
|
||||
C = "#### Stelle 1 — `tests/c.py`"
|
||||
N = "#### Stelle 1 — `docs/neu.md`"
|
||||
NICHTS = {}
|
||||
GANZ_A = {"tests/a.sh": "eins\nZWEI\ndrei\n"}
|
||||
GANZ_B = {"tests/b.sh": "VIER\nfuenf\n"}
|
||||
|
||||
|
||||
def b(wort: str, wortlaut: str | None, welche: int = 1, datei: str = "task-01.md") -> tuple:
|
||||
"""Erwarteter Befund: Wort, Text der Zeile (welche-tes Vorkommen, -1 das letzte), Taskdatei."""
|
||||
return wort, wortlaut, welche, datei
|
||||
|
||||
|
||||
def aus_wird(ueberschrift: str, alt: str, neu: str, schluss_aus: str = "\n" + Z, schluss_wird: str = "\n" + Z) -> str:
|
||||
return f"{ueberschrift}\n\nAus:\n\n{Z}text\n{alt}{schluss_aus}\n\nwird:\n\n{Z}text\n{neu}{schluss_wird}\n\n"
|
||||
|
||||
|
||||
def ganz(ueberschrift: str, text: str, schluss: str = "\n" + Z) -> str:
|
||||
return f"{ueberschrift}\n\nDie ganze Datei ist neu:\n\n{Z}text\n{text}{schluss}\n\n"
|
||||
|
||||
|
||||
def anhaengen(ueberschrift: str, text: str, schluss: str = "\n" + Z) -> str:
|
||||
return f"{ueberschrift}\n\nHäng diesen Block ans Ende der Datei:\n\n{Z}text\n{text}{schluss}\n\n"
|
||||
|
||||
|
||||
def befehl(text: str, schluss: str = "\n" + Z) -> str:
|
||||
return f"Aufruf:\n\n{Z}bash\n{text}{schluss}\n\n"
|
||||
|
||||
|
||||
STELLE_A = aus_wird(A, "zwei", "ZWEI")
|
||||
STELLE_B = aus_wird(B, "vier", "VIER")
|
||||
|
||||
# Name, Art (mat, vor, nach), Taskdatei(en), Exit, erwartete Befunde (Wort, Text der Zeile), Dateien der Kopie
|
||||
# (nur mat; nur Abweichungen von BASIS, None heisst geloescht)
|
||||
FAELLE = [
|
||||
("Grundlinie", "mat", KOPF + STELLE_A + STELLE_B, 0, [], {**GANZ_A, **GANZ_B}),
|
||||
("bloecke: Grundlinie", "vor", KOPF + STELLE_A + STELLE_B, 0, [], None),
|
||||
]
|
||||
|
||||
|
||||
def paar(name: str, vorlage: str, klebt: str, heil: str, erwartet: list, dateien: dict, bloecke: bool = True) -> None:
|
||||
"""Haengt den Angriff und sein Gegenstueck an; {S} in der Vorlage ist die Stelle des Zauns."""
|
||||
FAELLE.append((name, "mat", vorlage.replace("{S}", klebt), 2, erwartet, NICHTS))
|
||||
FAELLE.append((f"{name}: geheilt", "mat", vorlage.replace("{S}", heil), 0, [], dateien))
|
||||
if bloecke:
|
||||
FAELLE.append((f"bloecke vor: {name}", "vor", vorlage.replace("{S}", klebt), 2, erwartet, None))
|
||||
FAELLE.append((f"bloecke nach: {name}", "nach", vorlage.replace("{S}", klebt), 2, erwartet, None))
|
||||
|
||||
|
||||
NAH = "\n" + Z
|
||||
# Die Form aus task-1020: beide Zaeune kleben an "])", das naechste Paar waere geschluckt
|
||||
paar("T27-Form: Aus und wird kleben", KOPF + aus_wird(C, " pytest.param(pfad)\n])", " pytest.param(PFAD)\n])", "{S}", "{S}") + STELLE_B,
|
||||
Z, NAH, [b(KLEBT, "])" + Z), b(KLEBT, "])" + Z, 2)], {"tests/c.py": BASIS["tests/c.py"].replace("(pfad)", "(PFAD)"), **GANZ_B})
|
||||
paar("nur Aus klebt, erste Stelle", KOPF + aus_wird(A, "zwei", "ZWEI", "{S}") + STELLE_B, Z, NAH, [b(KLEBT, "zwei" + Z)], {**GANZ_A, **GANZ_B})
|
||||
paar("nur wird klebt, Folgepaar", KOPF + aus_wird(A, "zwei", "ZWEI", NAH, "{S}") + STELLE_B, Z, NAH, [b(KLEBT, "ZWEI" + Z)], {**GANZ_A, **GANZ_B})
|
||||
paar("nur wird klebt, letzte Stelle", KOPF + STELLE_B + aus_wird(A, "zwei", "ZWEI", NAH, "{S}"), Z, NAH, [b(KLEBT, "ZWEI" + Z)], {**GANZ_A, **GANZ_B})
|
||||
paar("ganze Datei klebt, Folgepaar", KOPF + ganz(N, "neu", "{S}") + STELLE_A, Z, NAH, [b(KLEBT, "neu" + Z)], {"docs/neu.md": "neu\n", **GANZ_A}, bloecke=False)
|
||||
paar("ganze Datei klebt, letzte Stelle", KOPF + STELLE_A + ganz(N, "neu", "{S}"), Z, NAH, [b(KLEBT, "neu" + Z)], {"docs/neu.md": "neu\n", **GANZ_A}, bloecke=False)
|
||||
paar("anhaengen klebt", KOPF + STELLE_A + anhaengen(B, "sechs", "{S}") + "Schluss.\n", Z, NAH, [b(KLEBT, "sechs" + Z)],
|
||||
{**GANZ_A, "tests/b.sh": "vier\nfuenf\nsechs\n"}, bloecke=False)
|
||||
paar("Befehlsblock klebt, Folgepaar", KOPF + befehl("ls", "{S}") + STELLE_A, Z, NAH, [b(KLEBT, "ls" + Z)], GANZ_A)
|
||||
paar("eingerueckter Block klebt", KOPF + f"1. Schritt:\n\n {Z}bash\n ls{{S}}\n\n" + STELLE_A, Z, "\n " + Z, [b(KLEBT, " ls" + Z)], GANZ_A)
|
||||
paar("Leerzeichen hinter dem klebenden Zaun", KOPF + STELLE_B + ganz(N, "neu", "{S}"), Z + " ", NAH, [b(KLEBT, "neu" + Z + " ")], {"docs/neu.md": "neu\n", **GANZ_B}, bloecke=False)
|
||||
paar("klebender Zaun laenger als der Block", KOPF + aus_wird(A, "zwei", "ZWEI", "{S}") + STELLE_B, Z + "``", NAH, [b(KLEBT, "zwei" + Z + "``")], {**GANZ_A, **GANZ_B})
|
||||
VIER = Z + "`"
|
||||
paar("Vierer-Zaun klebt", KOPF + f"{N}\n\nDie ganze Datei ist neu:\n\n{VIER}markdown\nneu{{S}}\n\n" + STELLE_A, VIER, "\n" + VIER, [b(KLEBT, "neu" + VIER)],
|
||||
{"docs/neu.md": "neu\n", **GANZ_A}, bloecke=False)
|
||||
# Bewusst entschieden: Auch ein Text, der auf einen Zaun endet, ist ein Befund; Abhilfe ist der laengere Zaun (Fall unten)
|
||||
FAELLE.append(("Zeichenkette im Test endet auf den Zaun", "mat",
|
||||
KOPF + ganz(N, f'TEXT = """Aus:\nzwei{Z}\n"""') + STELLE_A, 2, [b(KLEBT, "zwei" + Z), b(FEHLT, Z + "text", 2)], NICHTS))
|
||||
# Block, der nie schliesst: letzte, einzige und allerletzte Zeile, Schlusszaun zu kurz; die heile Form schreibt
|
||||
paar("Block schliesst nie, letzte Stelle", KOPF + STELLE_A + f"{N}\n\nDie ganze Datei ist neu:\n\n{Z}text\nneu\n{{S}}", "", Z + "\n",
|
||||
[b(OFFEN, Z + "text", -1)], {"docs/neu.md": "neu\n", **GANZ_A}, bloecke=False)
|
||||
paar("Block schliesst nie, einziger Block", KOPF + f"{N}\n\nDie ganze Datei ist neu:\n\n{Z}text\nneu\n\nProsa.\n{{S}}", "", Z + "\n",
|
||||
[b(OFFEN, Z + "text")], {"docs/neu.md": "neu\n\nProsa.\n"}, bloecke=False)
|
||||
paar("Aus-Block schliesst nie", KOPF + f"{A}\n\nAus:\n\n{Z}text\nzwei\n{{S}}", "", Z + "\n\nwird:\n\n" + Z + "text\nZWEI\n" + Z + "\n", [b(OFFEN, Z + "text")], GANZ_A)
|
||||
paar("Zaun auf der allerletzten Zeile", KOPF + STELLE_A + "Aufruf:\n\n" + Z + "{S}", "", "bash\nls\n" + Z, [b(OFFEN, Z, -1)], GANZ_A)
|
||||
paar("Schlusszaun kuerzer als der Oeffner", KOPF + f"{N}\n\nDie ganze Datei ist neu:\n\n{VIER}text\nneu\n{{S}}\n\nProsa.\n", Z, VIER, [b(OFFEN, VIER + "text")], {"docs/neu.md": "neu\n"}, bloecke=False)
|
||||
# Zaun fehlt, der naechste Block oeffnet: Der Block schloesse mit dem Zaun des naechsten
|
||||
paar("Zaun fehlt, Folgepaar", KOPF + ganz(N, "neu", "{S}") + STELLE_A, "", NAH, [b(FEHLT, Z + "text", 2)], {"docs/neu.md": "neu\n", **GANZ_A}, bloecke=False)
|
||||
paar("Zaun fehlt im Aus-Block", KOPF + aus_wird(A, "zwei", "ZWEI", "{S}") + STELLE_B, "", NAH, [b(FEHLT, Z + "text", 2)], {**GANZ_A, **GANZ_B})
|
||||
paar("Zaun fehlt, naechster Block ist laenger", KOPF + ganz(N, "neu", "{S}") + f"{A}\n\nDie ganze Datei ist neu:\n\n{VIER}markdown\n{Z}text\nx\n{Z}\n{VIER}\n", "", NAH,
|
||||
[b(FEHLT, VIER + "markdown")], {"docs/neu.md": "neu\n", "tests/a.sh": f"{Z}text\nx\n{Z}\n"}, bloecke=False)
|
||||
# Zwei Fehler in einer Datei: beide stehen da, keiner verdeckt den anderen
|
||||
FAELLE.append(("klebt und schliesst nie", "mat", KOPF + aus_wird(A, "zwei", "ZWEI", Z) + ganz(N, "neu", ""), 2,
|
||||
[b(KLEBT, "zwei" + Z), b(OFFEN, Z + "text", -1)], NICHTS))
|
||||
paar("CRLF: beide kleben", (KOPF + aus_wird(A, "zwei", "ZWEI", "{S}", "{S}") + STELLE_B).replace("\n", "\r\n"), Z, "\r\n" + Z,
|
||||
[b(KLEBT, "zwei" + Z), b(KLEBT, "ZWEI" + Z)], {**GANZ_A, **GANZ_B})
|
||||
# Mehrere Taskdateien: Der ganze Lauf schreibt nichts, auch die heile Datei davor nicht
|
||||
ZWEI_DATEIEN = [("zweite Taskdatei klebt", {"task-01.md": KOPF + STELLE_A, "task-02.md": KOPF + ganz(N, "neu", Z)}, [b(KLEBT, "neu" + Z, 1, "task-02.md")]),
|
||||
("erste Taskdatei klebt", {"task-01.md": KOPF + ganz(N, "neu", Z), "task-02.md": KOPF + STELLE_A}, [b(KLEBT, "neu" + Z)])]
|
||||
for name, dateien, erwartet in ZWEI_DATEIEN:
|
||||
FAELLE.append((name, "mat", dateien, 2, erwartet, NICHTS))
|
||||
FAELLE.append(("zwei Taskdateien: geheilt", "mat", {"task-01.md": KOPF + STELLE_A, "task-02.md": KOPF + ganz(N, "neu")}, 0, [], {"docs/neu.md": "neu\n", **GANZ_A}))
|
||||
# Loeschung unter Zieldateien bleibt aus, solange die Taskdatei einen Zaunbefund traegt
|
||||
LOESCHEN = "# Task 01\n\n## Zieldateien\n\n- `tests/b.sh` (gelöscht)\n\n## Prüfstand\n\n" + aus_wird(A, "zwei", "ZWEI", "{S}")
|
||||
paar("Loeschung unter Zieldateien", LOESCHEN, Z, NAH, [b(KLEBT, "zwei" + Z)], {**GANZ_A, "tests/b.sh": None}, bloecke=False)
|
||||
# Unlesbar: ein Befund, kein Traceback, auch die heile Datei davor bleibt ungeschrieben
|
||||
FAELLE.append(("Taskdatei fehlt", "mat", {"task-01.md": KOPF + STELLE_A, "task-02.md": None}, 2, [b(LESEN, None, 1, "task-02.md")], NICHTS))
|
||||
FAELLE.append(("Taskdatei ist kein UTF-8", "mat", {"task-01.md": KOPF + STELLE_A, "task-02.md": b"\xff\xfe\x00kaputt"}, 2, [b(LESEN, None, 1, "task-02.md")], NICHTS))
|
||||
FAELLE.append(("bloecke: Taskdatei fehlt", "vor", {"task-01.md": KOPF + STELLE_A, "task-02.md": None}, 2, [b(LESEN, None, 1, "task-02.md")], None))
|
||||
|
||||
# Nachbesserung nach der Pruefung vom 2026-11-02: Einzug, zwei Zaeune auf einer Zeile, Zeile beginnt mit dem Zaun
|
||||
TIEF = " " + Z
|
||||
ZEICHENKETTE = f'def test():\n t = """\n{TIEF}\n code\n{TIEF}\n """'
|
||||
FAELLE.extend([
|
||||
("Zaun vier Spalten tiefer ist Inhalt, kein Schluss", "mat", KOPF + ganz(N, ZEICHENKETTE) + STELLE_A, 2, [b(KLEBT, TIEF)], NICHTS),
|
||||
("Zaun vier Spalten tiefer im laengeren Zaun", "mat", KOPF + f"{N}\n\nDie ganze Datei ist neu:\n\n{VIER}python\n{ZEICHENKETTE}\n{VIER}\n\n" + STELLE_A, 0, [],
|
||||
{"docs/neu.md": ZEICHENKETTE + "\n", **GANZ_A}),
|
||||
("bloecke: Zaun vier Spalten tiefer im Aus-Block", "vor", KOPF + aus_wird(A, f"eins\n{TIEF}\n zwei\n{TIEF}\ndrei", "X") + STELLE_B, 2, [b(KLEBT, TIEF)], None),
|
||||
("Zaun drei Spalten tiefer schliesst", "mat", KOPF + ganz(N, "neu", "\n " + Z) + STELLE_A, 0, [], {"docs/neu.md": "neu\n", **GANZ_A}),
|
||||
("eingerueckter Block: Schlusszaun drei Spalten tiefer", "mat", KOPF + f"1. Schritt:\n\n {Z}bash\n ls\n {Z}\n\n" + STELLE_A, 0, [], GANZ_A),
|
||||
("eingerueckter Block: Zaun vier Spalten tiefer", "mat", KOPF + f"1. Schritt:\n\n {Z}bash\n ls\n {Z}\n {Z}\n\n" + STELLE_A, 2,
|
||||
[b(KLEBT, " " + Z), b(FEHLT, Z + "text")], NICHTS),
|
||||
("zwei Zaeune auf einer Zeile schliessen nicht", "mat", KOPF + ganz(N, "neu", f"\n{Z} {Z}") + STELLE_A, 2, [b(KLEBT, f"{Z} {Z}")], NICHTS),
|
||||
("Blockzeile beginnt mit dem Zaun und endet anders", "mat", KOPF + ganz(N, f"neu\n{Z} {Z}text\nzwei") + STELLE_A, 2, [b(FEHLT, f"{Z} {Z}text")], NICHTS),
|
||||
("eingerueckter Oeffner im Block", "mat", KOPF + f"1. Schritt:\n\n {Z}bash\n ls\n\n2. Danach:\n\n {Z}text\n pwd\n {Z}\n\n" + STELLE_A, 2, [b(FEHLT, " " + Z + "text")], NICHTS),
|
||||
("zwei Bloecke ohne Schluss", "mat", KOPF + ganz(N, "neu", "") + ganz(A, "x", ""), 2, [b(FEHLT, Z + "text", 2), b(OFFEN, Z + "text", 2)], NICHTS),
|
||||
("Meldung nennt den Block des klebenden Zauns", "mat", f"Prosa.\n\n{Z}text\nzwei{Z}\n", 2, [b("Block ab Zeile 3", "zwei" + Z)], NICHTS),
|
||||
("Meldung nennt je den offenen Block", "mat", f"{Z}text\na\n{Z}text\nb\n{Z}text\nc\n{Z}\n", 2, [b("Block ab Zeile 1", Z + "text", 2), b("Block ab Zeile 3", Z + "text", 3)], NICHTS),
|
||||
("Prosa beginnt mit zwei Backticks", "mat", KOPF + "``nur zwei\n\n" + STELLE_A, 0, [], GANZ_A),
|
||||
("Zaun fehlt, naechster Block tiefer eingerueckt", "mat", KOPF + ganz(N, "neu", "") + f"1. Schritt:\n\n {Z}bash\n ls\n {Z}\n\n" + STELLE_A, 2,
|
||||
[b(FEHLT, " " + Z + "bash")], NICHTS),
|
||||
# plan_bloecke.py: Der Schlusszaun des eingerueckten Blocks muss schliessen, sonst ginge das Aus danach still verloren
|
||||
("bloecke: eingerueckter Block, Schlusszaun drei Spalten tiefer", "vor", KOPF + f"1. Schritt:\n\n {Z}bash\n ls\n {Z}\n\n" + aus_wird(A, "gibt es nicht", "x"), 2,
|
||||
[b("Aus-Block steht nicht", Z + "text")], None),
|
||||
# Bewusst nicht gesehen: Ein nackter Zaun in Spalte 0 schliesst, die Zahl der Zaeune geht auf, der Rest ist Prosa
|
||||
("Luecke: nackter Zaun als Zeile im Text", "mat", KOPF + ganz(N, f't = """\n{Z}\ncode\n{Z}\n"""') + STELLE_A, 0, [], {"docs/neu.md": 't = """\n', **GANZ_A}),
|
||||
])
|
||||
|
||||
# Echte Inhalte: kein Befund, und der Text kommt unveraendert an
|
||||
VORLAGE =f"## Abschnitt\n\n### `tests/b.sh`\n\nProsa mit `Code`.\n\n{Z}text\nx\n{Z}\n\n{Z}bash\ny\n{Z}"
|
||||
FAELLE.extend([
|
||||
("Markdown-Vorlage im Vierer-Zaun, ##-Ueberschriften aendern kein Ziel", "mat",
|
||||
KOPF + f"{N}\n\nDie ganze Datei ist neu:\n\n{VIER}markdown\n{VORLAGE}\n{VIER}\n\n" + STELLE_A, 0, [], {"docs/neu.md": VORLAGE + "\n", **GANZ_A}),
|
||||
("Vorlage: letzte Stelle", "mat", KOPF + STELLE_A + f"{N}\n\nDie ganze Datei ist neu:\n\n{VIER}markdown\n{VORLAGE}\n{VIER}\n", 0, [], {"docs/neu.md": VORLAGE + "\n", **GANZ_A}),
|
||||
("Eingerueckter Befehl in einer Liste", "mat", KOPF + f"1. Schritt:\n\n {Z}bash\n ls\n {Z}\n\n2. Danach:\n\n {Z}\n pwd\n {Z}\n\n" + STELLE_A, 0, [], GANZ_A),
|
||||
("Spalte-0-Block mit eingeruecktem Schlusszaun", "mat", KOPF + ganz(N, "neu", "\n " + Z) + STELLE_A, 0, [], {"docs/neu.md": "neu\n", **GANZ_A}),
|
||||
("Inline-Code am Ende einer Blockzeile", "mat", KOPF + ganz(N, "- `pfad`\n- ``doppelt``\n- ende``") + STELLE_A, 0, [],
|
||||
{"docs/neu.md": "- `pfad`\n- ``doppelt``\n- ende``\n", **GANZ_A}),
|
||||
("Zaunfolge am Zeilenende im Vierer-Zaun", "mat", KOPF + f"{N}\n\nDie ganze Datei ist neu:\n\n{VIER}text\nzwei{Z}\n{VIER}\n\n" + STELLE_A, 0, [],
|
||||
{"docs/neu.md": f"zwei{Z}\n", **GANZ_A}),
|
||||
("Zeichenkette im Test im laengeren Zaun", "mat", KOPF + f'{N}\n\nDie ganze Datei ist neu:\n\n{VIER}python\nTEXT = """Aus:\nzwei{Z}\n{Z}text\n"""\n{VIER}\n\n' + STELLE_A, 0, [],
|
||||
{"docs/neu.md": f'TEXT = """Aus:\nzwei{Z}\n{Z}text\n"""\n', **GANZ_A}),
|
||||
("Zaunfolge in Anfuehrungszeichen", "mat", KOPF + ganz(N, f'Z = "{Z}"\nzaun = f"{{Z}}text"\nassert x == "{Z}"') + STELLE_A, 0, [],
|
||||
{"docs/neu.md": f'Z = "{Z}"\nzaun = f"{{Z}}text"\nassert x == "{Z}"\n', **GANZ_A}),
|
||||
("Zaunfolge mitten in der Zeile", "mat", KOPF + ganz(N, f"a {Z} b\n{Z[:2]}x") + STELLE_A, 0, [], {"docs/neu.md": f"a {Z} b\n{Z[:2]}x\n", **GANZ_A}),
|
||||
("Prosa endet auf Zaunfolge", "mat", KOPF + f"Der Zaun besteht aus {Z}\n\n" + STELLE_A, 0, [], GANZ_A),
|
||||
("Inline-Code am Zeilenanfang oeffnet nichts", "mat", KOPF + f"{Z}foo{Z} ist Inline-Code.\n\n" + STELLE_A + STELLE_B, 0, [], {**GANZ_A, **GANZ_B}),
|
||||
("Inline-Code am Zeilenanfang, letzte Zeile", "mat", KOPF + STELLE_A + f"{Z}foo{Z} ist Inline-Code.\n", 0, [], GANZ_A),
|
||||
("Schlusszaun mit Leerzeichen dahinter", "mat", KOPF + ganz(N, "neu", "\n" + Z + " ") + STELLE_A, 0, [], {"docs/neu.md": "neu\n", **GANZ_A}),
|
||||
("Schlusszaun laenger als der Oeffner", "mat", KOPF + ganz(N, "neu", "\n" + VIER) + STELLE_A, 0, [], {"docs/neu.md": "neu\n", **GANZ_A}),
|
||||
("leerer Block", "mat", KOPF + f"Aufruf:\n\n{Z}\n{Z}\n\n" + STELLE_A, 0, [], GANZ_A),
|
||||
("Taskdatei ohne Block", "mat", KOPF + "Nur Prosa.\n", 0, [], NICHTS),
|
||||
("leere Taskdatei", "mat", "", 0, [], NICHTS),
|
||||
# plan_bloecke.py: dieselbe Blockstruktur wie plan_materialisiere.py
|
||||
("bloecke: Vorlage als Aus-Block im Vierer-Zaun", "vor", KOPF + STELLE_A + f"{A}\n\nAus:\n\n{VIER}text\nzwei\ndrei\n{VIER}\n\nwird:\n\n{VIER}markdown\n{VORLAGE}\n{VIER}\n\n", 0, [], None),
|
||||
("bloecke: Inline-Code am Zeilenanfang oeffnet nichts", "vor", KOPF + f"{Z}foo{Z} ist Inline-Code.\n\n" + STELLE_A + STELLE_B, 0, [], None),
|
||||
("bloecke: eingerueckter Block nach Aus zaehlt nicht", "vor", KOPF + f"{A}\n\nAus:\n\n {Z}text\n gibt es nicht\n {Z}\n\n" + STELLE_A, 0, [], None),
|
||||
("bloecke: eingerueckter Schlusszaun schliesst", "vor", KOPF + aus_wird(A, "zwei", "ZWEI", "\n " + Z) + STELLE_B, 0, [], None),
|
||||
# Die heile Datei daneben wird weiter geprueft; ihr Befund steht neben dem Zaunbefund
|
||||
("bloecke: zwei Taskdateien, die heile wird geprueft", "vor",
|
||||
{"task-01.md": KOPF + aus_wird(A, "zwei", "ZWEI", Z), "task-02.md": KOPF + aus_wird(A, "gibt es nicht", "x")}, 2,
|
||||
[b(KLEBT, "zwei" + Z), b("Aus-Block steht nicht", Z + "text", 1, "task-02.md")], None),
|
||||
])
|
||||
|
||||
MAT, BLO = "plan_materialisiere.py", "plan_bloecke.py"
|
||||
FEHLT_ZEILE = ' if kern.startswith("`" * laenge):'
|
||||
NEUBEGINN = ' start, laenge, spalte = i, len(kern) - len(kern.lstrip("`")), len(zeilen[i]) - len(kern)'
|
||||
SCHLUSS = ' return kern.startswith("`" * laenge) and not kern.rstrip().strip("`") and len(zeile) - len(kern) - einzug <= 3'
|
||||
# Name, Datei, Text (ganze Zeilen, genau einmal in der Datei), Ersatz, Fall, an dem der Mutant rot werden muss
|
||||
MUTANTEN = [
|
||||
("klebt: Pruefung aus", MAT, ' if zeilen[i].rstrip().endswith("`" * laenge):', " if False:", "T27-Form: Aus und wird kleben"),
|
||||
("klebt: Leerzeichen dahinter uebersehen", MAT, ' if zeilen[i].rstrip().endswith("`" * laenge):', ' if zeilen[i].endswith("`" * laenge):', "Leerzeichen hinter dem klebenden Zaun"),
|
||||
("klebt: feste Laenge drei", MAT, ' if zeilen[i].rstrip().endswith("`" * laenge):', f' if zeilen[i].rstrip().endswith("{Z}"):', "Zaunfolge am Zeilenende im Vierer-Zaun"),
|
||||
("klebt: Block endet nicht am Zaun", MAT, 'in einen laengeren Zaun"))\n break\n', 'in einen laengeren Zaun"))\n', "T27-Form: Aus und wird kleben"),
|
||||
("offen: Pruefung aus", MAT, " if i >= len(zeilen):", " if False:", "Block schliesst nie, letzte Stelle"),
|
||||
("fehlt: Pruefung aus", MAT, FEHLT_ZEILE, " if False:", "Zaun fehlt, Folgepaar"),
|
||||
("fehlt: nur laengere Zaeune", MAT, FEHLT_ZEILE, ' if kern.startswith("`" * (laenge + 1)):', "Zaun fehlt, Folgepaar"),
|
||||
("fehlt: nur gleich lange Zaeune", MAT, FEHLT_ZEILE, ' if len(kern) - len(kern.lstrip("`")) == laenge:', "Zaun fehlt, naechster Block ist laenger"),
|
||||
("fehlt: nur Zaeune mit Sprache", MAT, FEHLT_ZEILE, " if zaun(zeilen[i]) >= laenge:", "Blockzeile beginnt mit dem Zaun und endet anders"),
|
||||
("fehlt: kein neuer Block", MAT, NEUBEGINN, " pass", "Zaun fehlt, naechster Block ist laenger"),
|
||||
("fehlt: Zeile des Blocks bleibt", MAT, NEUBEGINN, ' laenge, spalte = len(kern) - len(kern.lstrip("`")), len(zeilen[i]) - len(kern)', "zwei Bloecke ohne Schluss"),
|
||||
("fehlt: Zaunlaenge bleibt", MAT, NEUBEGINN, " start, spalte = i, len(zeilen[i]) - len(kern)", "Zaun fehlt, naechster Block ist laenger"),
|
||||
("fehlt: Einzug bleibt", MAT, NEUBEGINN, ' start, laenge = i, len(kern) - len(kern.lstrip("`"))', "Zaun fehlt, naechster Block tiefer eingerueckt"),
|
||||
("main: Vorpruefung aus", MAT, " for zeile, meldung in zaunbefunde(plan / name):", " for zeile, meldung in []:", "ganze Datei klebt, Folgepaar"),
|
||||
("main: Befund ohne Abbruch", MAT, ' if befunde:\n print(f"{befunde} Befund(e).")\n return 2\n', "", "ganze Datei klebt, Folgepaar"),
|
||||
("main: nur die erste Taskdatei", MAT, " for name in sys.argv[3:]:\n for zeile, meldung", " for name in sys.argv[3:4]:\n for zeile, meldung", "zweite Taskdatei klebt"),
|
||||
("Inline-Code oeffnet einen Block", MAT, ' return laenge if laenge >= 3 and "`" not in kern[laenge:] else 0', " return laenge if laenge >= 3 else 0", "Inline-Code am Zeilenanfang oeffnet nichts"),
|
||||
("Zaun schon ab zwei Backticks", MAT, ' return laenge if laenge >= 3 and "`" not in kern[laenge:] else 0', ' return laenge if laenge >= 2 and "`" not in kern[laenge:] else 0', "Prosa beginnt mit zwei Backticks"),
|
||||
("Zaun erst ab vier Backticks", MAT, ' return laenge if laenge >= 3 and "`" not in kern[laenge:] else 0', ' return laenge if laenge >= 4 and "`" not in kern[laenge:] else 0', "Grundlinie"),
|
||||
("Schlusszaun nur in Spalte 0", MAT, ' kern = zeile.lstrip()\n return kern.startswith("`" * laenge)', ' kern = zeile\n return kern.startswith("`" * laenge)', "Eingerueckter Befehl in einer Liste"),
|
||||
("Schlusszaun mit Text dahinter", MAT, SCHLUSS, ' return kern.startswith("`" * laenge) and len(zeile) - len(kern) - einzug <= 3', "Zaun fehlt, Folgepaar"),
|
||||
("Schlusszaun: Leerzeichen dahinter schliessen nicht", MAT, SCHLUSS, ' return kern.startswith("`" * laenge) and not kern.strip("`") and len(zeile) - len(kern) - einzug <= 3', "Schlusszaun mit Leerzeichen dahinter"),
|
||||
("Schlusszaun: zwei Zaeune auf einer Zeile", MAT, SCHLUSS, ' return kern.startswith("`" * laenge) and not kern.strip("`").strip() and len(zeile) - len(kern) - einzug <= 3', "zwei Zaeune auf einer Zeile schliessen nicht"),
|
||||
("Schlusszaun: vier Spalten tiefer schliesst", MAT, SCHLUSS, ' return kern.startswith("`" * laenge) and not kern.rstrip().strip("`") and len(zeile) - len(kern) - einzug <= 4', "Zaun vier Spalten tiefer ist Inhalt, kein Schluss"),
|
||||
("Schlusszaun: drei Spalten tiefer schliesst nicht", MAT, SCHLUSS, ' return kern.startswith("`" * laenge) and not kern.rstrip().strip("`") and len(zeile) - len(kern) - einzug <= 2', "Zaun drei Spalten tiefer schliesst"),
|
||||
("Schlusszaun: Einzug des Oeffners zaehlt nicht", MAT, SCHLUSS, ' return kern.startswith("`" * laenge) and not kern.rstrip().strip("`") and len(zeile) - len(kern) <= 3', "eingerueckter Block: Schlusszaun drei Spalten tiefer"),
|
||||
("Suche: Einzug des Blocks fehlt", MAT, " spalte = len(zeilen[start]) - len(zeilen[start].lstrip())", " spalte = 0", "eingerueckter Block: Schlusszaun drei Spalten tiefer"),
|
||||
("bloecke(): Einzug des Blocks fehlt", MAT, " while i < len(zeilen) and not schliesst(zeilen[i], laenge, einzug):\n inhalt.append(zeilen[i][einzug:]", " while i < len(zeilen) and not schliesst(zeilen[i], laenge, 0):\n inhalt.append(zeilen[i][einzug:]", "eingerueckter Block: Schlusszaun drei Spalten tiefer"),
|
||||
("lesen: UTF-8-Fehler nicht abgefangen", MAT, " except (OSError, UnicodeDecodeError) as fehler:", " except OSError as fehler:", "Taskdatei ist kein UTF-8"),
|
||||
("lesen: fehlende Datei nicht abgefangen", MAT, " except (OSError, UnicodeDecodeError) as fehler:", " except UnicodeDecodeError as fehler:", "Taskdatei fehlt"),
|
||||
("bloecke(): eigene Suche nach dem Oeffner", MAT, " laenge = zaun(zeile)\n if laenge:\n einzug", ' laenge = len(zeile.lstrip()) - len(zeile.lstrip().lstrip("`"))\n if laenge >= 3:\n einzug', "Inline-Code am Zeilenanfang oeffnet nichts"),
|
||||
("plan_bloecke: Pruefung aus", BLO, " kaputt = zaunbefunde(taskdatei)", " kaputt = []", "bloecke vor: T27-Form: Aus und wird kleben"),
|
||||
("plan_bloecke: Summe zaehlt je Datei", BLO, " befunde += len(kaputt)", " befunde += 1", "bloecke vor: T27-Form: Aus und wird kleben"),
|
||||
("plan_bloecke: Einzug des Blocks fehlt", BLO, " while i < len(zeilen) and not schliesst(zeilen[i], laenge, einzug):", " while i < len(zeilen) and not schliesst(zeilen[i], laenge, 0):", "bloecke: eingerueckter Block, Schlusszaun drei Spalten tiefer"),
|
||||
("plan_bloecke: prueft trotz Befund", BLO, " zaunfehler = True\n continue\n", " zaunfehler = True\n", "bloecke vor: T27-Form: Aus und wird kleben"),
|
||||
("plan_bloecke: Exit 3 trotz Zaunbefund", BLO, " if geprueft == 0 and not zaunfehler:", " if geprueft == 0:", "bloecke vor: T27-Form: Aus und wird kleben"),
|
||||
("plan_bloecke: eingerueckter Block zaehlt", BLO, ' if marke in ("Aus:", "wird:") and zeile.startswith("`"):', ' if marke in ("Aus:", "wird:"):', "bloecke: eingerueckter Block nach Aus zaehlt nicht"),
|
||||
("plan_bloecke: eigene Suche nach dem Oeffner", BLO, " laenge = zaun(zeile)\n if laenge:\n start = i", f' laenge = len(zeile) - len(zeile.lstrip("`"))\n if zeile.startswith("{Z}"):\n start = i', "bloecke: Inline-Code am Zeilenanfang oeffnet nichts"),
|
||||
("plan_bloecke: Schlusszaun nur in Spalte 0", BLO, " while i < len(zeilen) and not schliesst(zeilen[i], laenge, einzug):", ' while i < len(zeilen) and not (zeilen[i].startswith("`" * laenge) and not zeilen[i].strip("`").strip()):', "bloecke: eingerueckter Schlusszaun schliesst"),
|
||||
]
|
||||
|
||||
|
||||
def schreibe(pfad: Path, text) -> None:
|
||||
pfad.parent.mkdir(parents=True, exist_ok=True)
|
||||
pfad.write_bytes(text if isinstance(text, bytes) else text.encode("utf-8"))
|
||||
|
||||
|
||||
def zeile_von(text, wortlaut: str, welche: int = 1) -> int:
|
||||
"""Zeilennummer des welche-ten Vorkommens von wortlaut als ganze Zeile; -1 zaehlt von hinten, wie bei Listen."""
|
||||
zeilen = text.replace("\r\n", "\n").split("\n")
|
||||
treffer = [nr for nr, zeile in enumerate(zeilen, start=1) if zeile == wortlaut]
|
||||
return treffer[welche - 1 if welche > 0 else welche]
|
||||
|
||||
|
||||
def fahre(werkzeuge: Path, art: str, wurzel: Path, plan: Path, namen: list[str]) -> subprocess.CompletedProcess:
|
||||
if art == "mat":
|
||||
befehl_ = [str(werkzeuge / "plan_materialisiere.py"), str(plan), str(wurzel / "kopie"), *namen]
|
||||
else:
|
||||
befehl_ = [str(werkzeuge / "plan_bloecke.py"), art, *[str(plan / n) for n in namen], "--projekt", str(wurzel / "kopie")]
|
||||
return subprocess.run([sys.executable, "-X", "utf8", "-B", *befehl_], capture_output=True, text=True,
|
||||
encoding="utf-8", errors="replace", stdin=subprocess.DEVNULL)
|
||||
|
||||
|
||||
def alle_dateien(ordner: Path) -> dict[str, bytes]:
|
||||
return {p.relative_to(ordner).as_posix(): p.read_bytes() for p in ordner.rglob("*") if p.is_file()}
|
||||
|
||||
|
||||
def fall(werkzeuge: Path, art: str, task, exit_erwartet: int, erwartet: list, dateien: dict | None) -> tuple[list[str], str]:
|
||||
"""Faehrt einen Fall in einem frischen Wegwerfordner; Rueckgabe (Gruende fuer Rot, Ausgabe)."""
|
||||
wurzel = Path(tempfile.mkdtemp(prefix="pz_"))
|
||||
try:
|
||||
for rel, text in BASIS.items():
|
||||
schreibe(wurzel / "kopie" / rel, text)
|
||||
tasks = task if isinstance(task, dict) else {"task-01.md": task}
|
||||
for name, text in tasks.items():
|
||||
if text is not None:
|
||||
schreibe(wurzel / "plan" / name, text)
|
||||
lauf = fahre(werkzeuge, art, wurzel, wurzel / "plan", list(tasks))
|
||||
ausgabe = lauf.stdout + lauf.stderr
|
||||
gruende = []
|
||||
if lauf.returncode != exit_erwartet:
|
||||
gruende.append(f"Exit {lauf.returncode}, erwartet {exit_erwartet}")
|
||||
if "Traceback" in ausgabe:
|
||||
gruende.append("Traceback in der Ausgabe")
|
||||
for wort, wortlaut, welche, datei in erwartet:
|
||||
kopf = f"BEFUND {datei}:"
|
||||
if wortlaut is not None:
|
||||
kopf += f"{zeile_von(tasks[datei], wortlaut, welche)}:"
|
||||
if not any(z.startswith(kopf) and wort in z for z in ausgabe.split("\n")):
|
||||
gruende.append(f"Meldung fehlt: {kopf} ... {wort}")
|
||||
if ausgabe.count("BEFUND ") != len(erwartet):
|
||||
gruende.append(f"{ausgabe.count('BEFUND ')} BEFUND-Zeile(n), erwartet {len(erwartet)}")
|
||||
if f"{len(erwartet)} Befund(e)." not in ausgabe.split("\n"):
|
||||
gruende.append(f"Summenzeile fehlt: {len(erwartet)} Befund(e).")
|
||||
if dateien is not None:
|
||||
soll = {rel: text.encode("utf-8") for rel, text in {**BASIS, **dateien}.items() if text is not None}
|
||||
ist = alle_dateien(wurzel / "kopie")
|
||||
for rel in sorted(set(soll) | set(ist)):
|
||||
if soll.get(rel) != ist.get(rel):
|
||||
gruende.append(f"{rel}: {ist.get(rel)!r}, erwartet {soll.get(rel)!r}")
|
||||
return gruende, ausgabe
|
||||
finally:
|
||||
shutil.rmtree(wurzel, ignore_errors=True)
|
||||
|
||||
|
||||
def alle_faelle(werkzeuge: Path, laut: bool) -> list[str]:
|
||||
"""Faehrt alle Faelle, Grundlinie zuerst; Rueckgabe: Namen der roten Faelle."""
|
||||
rot = []
|
||||
for name, art, task, exit_erwartet, erwartet, dateien in FAELLE:
|
||||
gruende, ausgabe = fall(werkzeuge, art, task, exit_erwartet, erwartet, dateien)
|
||||
if laut:
|
||||
print(f"{'ok ' if not gruende else 'FEHLER'} {name}")
|
||||
if gruende:
|
||||
rot.append(name)
|
||||
if laut:
|
||||
for grund in gruende:
|
||||
print(" " + grund[:300])
|
||||
print(" " + ausgabe.strip().replace("\n", "\n ")[:900])
|
||||
if name == "Grundlinie":
|
||||
if laut:
|
||||
print("Die Grundlinie ist nicht sauber -- der Rest misst nichts.")
|
||||
return rot
|
||||
return rot
|
||||
|
||||
|
||||
def mutanten_lauf(werkzeuge: Path) -> int:
|
||||
"""Baut je Mutant einen Fehler in eine Kopie der Werkzeuge ein; Rueckgabe: Mutanten ohne Rot an ihrem Fall."""
|
||||
fehler = 0
|
||||
for name, datei, alt, neu, soll in MUTANTEN:
|
||||
ordner = Path(tempfile.mkdtemp(prefix="pm_"))
|
||||
try:
|
||||
shutil.copy(werkzeuge / MAT, ordner / MAT)
|
||||
shutil.copy(werkzeuge / BLO, ordner / BLO)
|
||||
text = (ordner / datei).read_text(encoding="utf-8")
|
||||
if text.count(alt) != 1:
|
||||
print(f"FEHLER {name}: der Text steht {text.count(alt)}-mal in {datei}")
|
||||
fehler += 1
|
||||
continue
|
||||
(ordner / datei).write_text(text.replace(alt, neu), encoding="utf-8", newline="\n")
|
||||
rot = alle_faelle(ordner, False)
|
||||
print(f"{'gefangen ' if soll in rot else 'UEBERLEBT'} {name}: {len(rot)} rot, erwartet an: {soll}")
|
||||
fehler += 0 if soll in rot else 1
|
||||
finally:
|
||||
shutil.rmtree(ordner, ignore_errors=True)
|
||||
print(f"{fehler} Mutant(en) nicht an ihrem Fall gefangen, von {len(MUTANTEN)}.")
|
||||
return fehler
|
||||
|
||||
|
||||
def main() -> int:
|
||||
zerleger = argparse.ArgumentParser()
|
||||
zerleger.add_argument("--werkzeuge", default=str(Path(__file__).resolve().parent))
|
||||
zerleger.add_argument("--mutanten", action="store_true")
|
||||
argumente = zerleger.parse_args()
|
||||
werkzeuge = Path(argumente.werkzeuge)
|
||||
rot = alle_faelle(werkzeuge, True)
|
||||
print(f"{len(rot)} Fall/Faelle mit falschem Ausgang von {len(FAELLE)}.")
|
||||
if rot:
|
||||
return 2
|
||||
return 2 if argumente.mutanten and mutanten_lauf(werkzeuge) else 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in new issue
Block a user