Abgleich mit dem Vault-Stand a201514

16 Datei(en) geändert.
Erzeugt von .vault-tools/auto-push/werkzeuge_spiegel.py.
This commit is contained in:
Pascal Michel committed 2026-10-18 04:00:03 +02:00
1 parent fb8bc625e3
commit 4d90e6be60
16 files changed
+781 -36

No files matched your search

+26 -6
View File
@@ -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-13
updated: 2026-10-17
---
# Planwerkzeuge für die Kopie mit angewendetem Plan
@@ -15,10 +15,10 @@ 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.
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").
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".
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.
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.
7. **Rot-Nachweis je Task (seit 2026-09-18):** `rot_nachweis.py <basis-kopie> <planordner> <arbeitsordner> <spec.json> [--fixturen <ordner>] [--node-modules <pfad>] [--conftest-als-test] [--venv <wsl-pfad>] [--frontend <ordner>] [--projektname <ordnername>]`. **Für ein anderes Projekt als die Trading Plattform braucht es die drei letzten Schalter (seit 2026-09-24):** Ihre Vorgaben sind die der Trading Plattform (`~/venvs/trading`, `dashboard`, `trading-plattform`). Jedes andere Projekt nennt seine Werte, für den Nexhorizons Hub `--venv '~/venvs/hub' --frontend frontend --projektname 'active - Nexhorizons Hub'`, das Venv in einfachen Anführungszeichen, sonst macht die Git-Bash aus `~` einen Windows-Pfad. Das Skript druckt die benutzten Werte in der ersten Zeile. Die Vorgabe `trading-plattform` passt nur auf Pläne mit Arbeitsbaum-Pfaden; ältere Trading-Pläne mit absoluten Pfaden unter `active - Trading Plattform` brauchen `--projektname 'active - Trading Plattform'`, sonst endet der Lauf mit einem `ValueError`. Weiter fest verdrahtet bleiben die Distribution `Debian`, die Ordner `tests/` und `tests/fixtures/`, die Endungen `.test.ts`/`.test.tsx` und `npx.cmd vitest`. **Vorbedingung:** Das Projekt ist im Venv nicht editierbar installiert (`pip install -e`); sonst löst der Finder von setuptools ein fehlendes Untermodul gegen das echte Projekt auf, und ein Test ohne Umsetzung läuft grün. `tests/conftest.py` gilt dabei als Produktivseite und wird zurückgesetzt; nur wenn die neue Fixture eines Tasks dort liegt, lässt `--conftest-als-test` sie stehen (Beleg unten, 2026-09-22). `plan_materialisiere.py` wendet eine Taskdatei immer ganz an; „erst der Test, dann die Umsetzung" lässt sich damit nicht beobachten. Das Skript stellt den Zustand dazwischen auf Wegwerfkopien her — Basis plus Voraussetzungstasks plus der Task, danach die produktiven Zieldateien zurück auf den Stand vor dem Task — und fährt die Testdateien des Tasks (pytest in WSL, vitest unter Windows über eine Junction, die es vor dem Löschen löst). Es meldet je Task `ROT` mit der ersten Fehlerzeile oder `GRUEN`; ein Test, der ohne Umsetzung grün ist, misst sie nicht. Die Basis-Kopie zieht man **vor** der ersten Welle. `spec.json` nennt je Task die Voraussetzungstasks. Erster Einsatz am 2026-09-18 an vier Plänen der Trading Plattform: 35 Tasks, alle rot an der gemeinten Stelle, keiner grün ohne Umsetzung. Die Executer brauchen den Tanz mit dem Beiseitelegen der Produktivdatei dann nicht mehr; ihr Auftrag sagt das ausdrücklich.
@@ -62,7 +62,27 @@ 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-09-18 nimmt `welle_vergleich.py` unter `## Zieldateien` auch Pfade relativ zum Projekt (ohne Laufwerk und ohne Wurzel), weil Pläne für einen Git-Worktree keine absoluten Pfade nennen; ein `..` darin ist ein Befund wie ein fremder absoluter Pfad. Jede Zeile braucht weiter die Klammer hinter dem Pfad (`(neu)`, `(geändert)`, `(gelöscht)`). `plan_materialisiere.py` erkennt eine Zieldatei ohne Schrägstrich nur mit den Endungen py, ts, tsx, toml, md, css und json — für `compose.yaml` in der Projektwurzel schreibt die Überschrift `./compose.yaml`. Der Treiber fährt dazu drei weitere Fälle (relative Grundlinie, relative Abweichung, `..` nach außen), zusammen 17.
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.
- Nennt eine Überschrift kein Ziel, aber einen Token, der nach einer Datei aussieht, ist jeder schreibende Block danach ein Befund mit Exit 2. Das gilt auch unter Überschriften ohne Pfad, bis eine Überschrift mit Ziel kommt. Nach einer Datei sehen aus: ein relativer Pfad mit Schrägstrich, ein Name ohne Schrägstrich, der direkt in der Kopie (bei `plan_bloecke.py` im Projekt) als Datei liegt, und der Dateiname eines Eintrags unter Zieldateien. `plan_materialisiere.py` schreibt den Block nicht; `plan_bloecke.py` prüft ihn nicht gegen das vorige Ziel.
- Kein Befund sind Ordner (`config/`), absolute Pfade (`/proc`), Token mit Leerraum (`GET /api/paper`) und Bezeichner ohne gleichnamige Datei (`main`, `Order`). Bestehende Pläne nennen sie in Überschriften über Blöcken, die das vorige Ziel meinen.
- Verworfen ist, jeden Pfad ohne Endung als Ziel zu erkennen. Das hätte jede Überschrift, die nur einen Ordner ohne Schrägstrich am Ende (`tests/.work/r1-01`) oder einen API-Pfad (`glattstellen/beginn`) nennt, zum Ziel aller Blöcke danach gemacht. Der Eintrag unter Zieldateien zeigt, dass der Plan die Datei meint.
- Folge in den Workflows: Ein Befund ist Exit 2. `plankritik.js` und `welle-mit-pruefung.js` verwerfen die Kopie dann, `rot_nachweis.py` meldet den Task als `ANWENDEN GESCHEITERT`.
Belegt am selben Tag in zwei Stufen, erst der Befund, dann die Erkennung über Zieldateien (Rohdaten: `AI/secondbrain/edge-stack-traefik-crowdsec-memory/2026-10-17-plan-ziel-roh/`, die zweite Stufe im Unterordner `nachtrag/`):
- `plan_ziel_treiber.py [--werkzeuge <ordner>]` fährt 33 Fälle, die Grundlinie zuerst, und prüft je Fall Exit, Meldung, Zahl der Befunde und jede Datei der Kopie.
- Angriffe: R1-Form an letzter und erster Stelle, Aus/wird, anhängen, über eine Überschrift ohne Pfad hinweg, zwei Stellen. Dazu eine Datei ohne Endung mit Eintrag an letzter und erster Stelle, mit Aus/wird, als `./Dockerfile` und als absoluter Eintrag; `Dockerfile` ohne Eintrag an letzter und erster Stelle; der Dateiname eines Eintrags ohne Pfad. Für `plan_bloecke.py` in den Modi vor und nach.
- Gegenfälle ohne Befund: Ordner, absoluter Pfad, PFAD-Treffer neben einer Datei ohne Endung, Token mit Leerraum, Bezeichner, Pfad ohne Endung über einem Befehl und vor einem Ziel. Ein Muster unter Zieldateien bleibt Befund.
- Ursprünglicher Stand 22 von 33 Fällen rot, Stand nach Stufe 1 noch 11 (genau die Angriffe der Stufe 2), jetzt 33 grün.
- Eingebaute Fehler, jeder an seinem Fall gefangen: 12 gegen Stufe 1, 21 gegen die jetzige Fassung (Liste in `nachtrag/mutanten.txt`). Von den 21 sind 10 die Fehler der Stufe 1 an ihrer neuen Stelle, 11 treffen die neue Regel: Zieldateien-Erkennung aus, Zieldateien vor PFAD, absolute Einträge übergangen, `./` nicht normiert, Muster zählt als Ziel, Wurzel vor `docs/plans` nicht erkannt, Datei in der Kopie zählt nicht, Dateiname eines Eintrags zählt nicht, Kopie oder Projekt nicht übergeben, keine Wurzel in `plan_bloecke.py`.
- Vorher/nachher über alle Plandateien im Vault, je Projekt in eigenen Kopien aus `git archive HEAD` oder einer Ordnerkopie, Ausgaben je Datei und Bäume byte-genau, der Kopiepfad in Absturzmeldungen normiert. Stufe 1 gegen den ursprünglichen Stand, 2 205 Dateien: einzige Abweichung R1 `task-01-logrotate.md`, Befund statt überschriebener `test-units.sh`.
- Stufe 2 gegen Stufe 1, 2 223 Dateien: R1 `task-01-logrotate.md` schreibt Stelle 30 nach `host/logrotate/edge-traefik`. Trading Plattform Task 90 schreibt Schritt 2 nach `.gitignore` statt still nach `trading/data/nachrichten.py` (vorher dort „Aus-Block steht 0-mal"). Sonst keine Abweichung; die Bäume unterscheiden sich nur in diesen beiden Dateien. `plan_bloecke.py` weicht nur an Task 90 ab: gegen den Projektordner geprüft jetzt gegen `.gitignore` ohne Befund, vorher „Aus-Block steht nicht in trading/data/nachrichten.py".
- Zehn Abstürze in allen Ständen gleich (fehlende Zieldatei beim Anhängen, siehe „Offen seit 2026-09-23").
- R1 auf `git archive 67cbe0b` des Edge-Stack, Task 01 allein und die Welle 01 bis 03: ursprünglich Exit 0 und der Kopf der logrotate-Datei in `test-units.sh`; nach Stufe 1 Exit 2 und ein Befund; jetzt Exit 0, `host/logrotate/edge-traefik` byte-gleich mit dem Block aus Stelle 30, `test-units.sh` unberührt.
- Lücken: Eine neue Datei ohne Endung, ohne Schrägstrich und ohne Eintrag unter Zieldateien bleibt still, ebenso ein absoluter Pfad ohne Endung in der Überschrift. Nennt eine Überschrift einen PFAD-Treffer und dazu eine Datei ohne Endung, gilt der PFAD-Treffer. Ein absoluter Eintrag außerhalb des Projekts zählt nicht, etwa in einem Arbeitsbaum-Plan, der den Hauptordner nennt. Wird in keiner übergebenen Taskdatei ein Block gegen eine Datei geprüft, endet `plan_bloecke.py` wie bisher mit 3, auch mit Befunden. `plan_bloecke.py` braucht `plan_materialisiere.py` im selben Ordner.
Seit 2026-09-18 nimmt `welle_vergleich.py` unter `## Zieldateien` auch Pfade relativ zum Projekt (ohne Laufwerk und ohne Wurzel), weil Pläne für einen Git-Worktree keine absoluten Pfade nennen; ein `..` darin ist ein Befund wie ein fremder absoluter Pfad. Jede Zeile braucht weiter die Klammer hinter dem Pfad (`(neu)`, `(geändert)`, `(gelöscht)`). `plan_materialisiere.py` erkennt eine Zieldatei ohne Schrägstrich nur mit den Endungen py, ts, tsx, toml, md, css und json — für `compose.yaml` in der Projektwurzel schreibt die Überschrift `./compose.yaml`. Eine Datei ganz ohne Endung braucht ihren Eintrag unter `## Zieldateien` (seit 2026-10-17, oben). Der Treiber fährt dazu drei weitere Fälle (relative Grundlinie, relative Abweichung, `..` nach außen).
Seit 2026-09-24 hat `rot_nachweis.py` die Schalter `--venv`, `--frontend` und `--projektname`; vorher waren `~/venvs/trading`, `dashboard` und `trading-plattform/` fest verdrahtet. Ein Hub-Lauf im Trading-Venv wäre für die Tasks mit `argon2-cffi` und `pyotp` rot aus einem fehlenden Paket gewesen, nicht aus der fehlenden Umsetzung. Fehlt das Venv in WSL, steht beim Task jetzt `VORAUSSETZUNG FEHLT` statt `ROT`, und die Schlusszeile zählt diese Läufe getrennt. Belegt am selben Tag: Der Vorgabe-Befehl ist byte-gleich mit der alten Fassung, falsche Schalterwerte enden mit Exit 3, und der erste echte Lauf über 38 Tasks des Hub meldete keinen grün ohne Umsetzung. Im selben Lauf zeigte sich eine zweite Lücke: Beim Zurücksetzen blieben Ordner stehen, die erst der Task angelegt hatte. Ein leeres `hub/auth/` liest Python als Namespace-Paket, und die Fehlerzeile nannte `hub.auth.passwort` statt `hub.auth`. Jetzt gehen solche Ordner mit, sobald sie leer sind; derselbe Lauf für die Tasks 7, 9 und 11 zeigt danach genau die Zeile aus dem Plan. Die unabhängige Prüfung desselben Tages fand eine dritte Lücke: Ein vergessenes `--venv` fiel still auf das Trading-Venv zurück, und Task 7 stand als `ROT … No module named 'argon2'` da. Seitdem wird eine erste Fehlerzeile, die ein Modul außerhalb des Projekts und außerhalb der Zieldateien vermisst, als `VORAUSSETZUNG FEHLT` gemeldet. Das trifft auch den Ende-zu-Ende-Task des Hub, dessen Tests `playwright` brauchen. Mit den Vorgaben lieferte das Skript für die sieben Tasks des Trading-Pakets K vor und nach allen Änderungen dieselben Zeilen. Zwei weitere Lücken desselben Tages, beide ein ROT, das nichts misst: **Ein Arbeitsordner mit Windows-Kurznamen** (`C:/Users/PASCAL~1/…`) ließ vitest dort starten, die Dateien aber über den langen Namen auflösen, und jede Oberflächen-Testdatei endete als `Cannot find module '/src/…'`. Das Skript löst den Arbeitsordner jetzt auf den langen Namen auf; danach zeigte Trading-Task 395 wieder `9 failed | 52 passed` statt `no tests`, und die Hub-Tasks 24, 25, 29 und 37 die Zeile aus dem Plan (`Error: Failed to resolve import "./kontrast" …`). Die erste `Error:`-Zeile steht seitdem mit in der Ergebniszeile, weil die `FAIL`-Zeile allein den Grund nicht zeigt. **Ein Test, der über Vite eine Datei aus `node_modules` lädt** (`?raw`), scheitert hinter der Junction mit `Denied ID`, weil Vite nichts außerhalb seines Wurzelordners ausliefert; das Skript meldet das als `VORAUSSETZUNG FEHLT`. Für solche Tasks läuft der Nachweis in einer Kopie mit echtem `node_modules` (242 MB, gut 20 Sekunden).
@@ -70,12 +90,12 @@ Seit 2026-09-22 behandelt `rot_nachweis.py` `tests/conftest.py` als Produktivsei
Seit 2026-09-20 quotet `rot_nachweis.py` den Arbeitsordner für den WSL-Aufruf (`shlex.quote`). Ein Temp-Pfad mit Leerzeichen — der Benutzername steckt im Scratchpad-Pfad — ließ vorher das `cd` scheitern: Alle sieben Tasks eines Plans der Trading Plattform standen als „ROT pytest: keine Summenzeile" da, ohne dass ein einziger Test lief. Nach der Korrektur derselbe Lauf mit echten Summenzeilen (`3 failed, 17 passed`, `1 error`, …). Wer „keine Summenzeile" bei **jedem** Task sieht, hat kein Rot gemessen, sondern ein kaputtes Werkzeug.
`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. `kopie_bauen.py` ist am 2026-09-17 an drei kaputten Eingaben rot gesehen: Ziel existiert schon (Exit 2, nichts kopiert), Ausschluss `node_modules` ohne Gegenstück im Wurzelordner (Befund), und ein aus der Kopie entferntes Paket `trading/data` (die Paketprüfung nennt es; nach dem Zurücklegen leer). Einen Treiber dazu gibt es noch nicht — die drei Fälle sind von Hand gefahren.
`welle_vergleich_treiber.py` fährt 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). Seit 2026-10-17 kommen acht Fälle zu Zieldateien dazu, die git ignoriert: gleich, Abweichung an erster und an letzter Stelle der Liste, fehlt im Baum, fehlt in der Kopie, gelöscht, Löschung nicht ausgeführt und eine getrackte Datei unter einem ignorierten Muster, die weiter als unverändert gilt. Sechs davon waren auf der alten Fassung rot; fünf Mutanten der neuen Zeilen macht der Treiber je rot. Am Stand von Welle 2 aus Paket R1 im Edge-Stack meldete die alte Fassung 12 Fehlalarme für `tests/.work/`, die neue 0 Befunde mit 20 byte-gleichen Zieldateien, 12 davon ignoriert; eine verfälschte ignorierte Datei meldet sie als Abweichung. `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. `kopie_bauen.py` ist am 2026-09-17 an drei kaputten Eingaben rot gesehen: Ziel existiert schon (Exit 2, nichts kopiert), Ausschluss `node_modules` ohne Gegenstück im Wurzelordner (Befund), und ein aus der Kopie entferntes Paket `trading/data` (die Paketprüfung nennt es; nach dem Zurücklegen leer). Einen Treiber dazu gibt es noch nicht — die drei Fälle sind von Hand gefahren.
Offen seit 2026-09-23: `plan_materialisiere.py` stirbt mit `FileNotFoundError`, wenn die Zieldatei eines „Häng diesen Block ans Ende"-Blocks in der Kopie fehlt (Trading Plattform, Task 384 hängte einen Nachtrag an die Planübersicht an; die Kopie war ohne `docs/plans` gebaut). Ein Absturz mitten im Task lässt die davor angewendeten Blöcke halb drin. Bis das ein Befund ist: Kopien für Pläne, die Plandateien selbst anfassen, mit dem Planordner bauen — und eine Kopie nie nach dem Start der Welle ziehen (sie trägt dann den halben Stand des Arbeitsbaums).
## 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.
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.
Das ältere `.vault-tools/plan-diff/plan_diff.py` vergleicht Ganzdatei-Blöcke einer früheren Plankonvention und bleibt dafür in Gebrauch.
+37 -8
View File
@@ -2,9 +2,19 @@
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.
"wird:" ist (Projektkonvention der Planer-Taskdateien seit Teil 5). Welche Datei Ziel ist, regelt
ordne() in plan_materialisiere.py (importiert, die Regel steht dort im Kopf): der letzte PFAD-Treffer
einer Ueberschrift (##, ###, ####) oberhalb des Blocks, ohne ihn ein Token, das genau so unter
"## Zieldateien" steht (absolute Eintraege relativ zu --projekt). Ob die Zieldatei im Projekt liegt,
prueft erst der Modus. Bis 2026-10-17 stand hier, das Ziel muesse unter Zieldateien stehen; der Code
tat das nie.
- Befund statt stillem Ziel (seit 2026-10-17, Plankritik Paket R1 im Edge-Stack): Nennt eine
Ueberschrift kein Ziel, aber einen Token, der nach einer Datei aussieht (relativer Pfad mit
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.
- Exit 3 hat Vorrang: Wurde in keiner Taskdatei ein Block gegen eine Datei geprueft, endet es mit 3,
auch wenn Befunde ausgegeben sind.
- 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,
@@ -18,11 +28,15 @@ Exit: 0 kein Befund, 2 Befunde, 3 kein einziger Aus-/wird-Block gefunden (nichts
import argparse
import re
import sys
from pathlib import Path
from pathlib import Path, PureWindowsPath
# Die Zielregel (PFAD, Zieldateien, offene Token) steht 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
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:
@@ -32,13 +46,20 @@ def normal(text: str) -> str:
def lies_bloecke(taskdatei: Path, projekt: Path) -> list[dict]:
zeilen = normal(taskdatei.read_text(encoding="utf-8")).split("\n")
bloecke: list[dict] = []
# Absolute Eintraege unter "## Zieldateien" zaehlen relativ zu --projekt, wie in welle_vergleich.py
geplant = zieldateien(zeilen, [PureWindowsPath(projekt.absolute()), PureWindowsPath(projekt.resolve())])
ziel: str | None = None
# (Zeile, Token) der letzten Ueberschrift ohne Ziel, die nach einer Datei aussieht; None, sobald ein Ziel kommt
ungeklaert: tuple[int, list[str]] | None = None
i = 0
while i < len(zeilen):
zeile = zeilen[i]
if UEBERSCHRIFT.match(zeile):
for treffer in PFAD.findall(zeile):
ziel = treffer
neu, offen = ordne(zeile, geplant, projekt)
if neu:
ziel, ungeklaert = neu, None
elif offen:
ungeklaert = (i + 1, offen)
if ZAUN.match(zeile):
start = i
# Ein Zaun aus n Backticks schliesst erst mit mindestens n Backticks: Ein
@@ -58,7 +79,8 @@ def lies_bloecke(taskdatei: Path, projekt: Path) -> list[dict]:
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)})
bloecke.append({"art": marke[:-1], "ziel": ziel, "zeile": start + 1, "text": "\n".join(inhalt),
"ungeklaert": ungeklaert})
i += 1
return bloecke
@@ -78,6 +100,13 @@ def main() -> int:
bloecke = lies_bloecke(taskdatei, projekt)
paare = 0
for nummer, block in enumerate(bloecke):
if block["ungeklaert"]:
zeile_u, pfade = block["ungeklaert"]
print(f"BEFUND {taskdatei.name}:{block['zeile']}: {block['art']}-Block unter Ueberschrift in Zeile {zeile_u}, "
f"die nennt {', '.join(f'`{p}`' for p in pfade)} und keine erkannte Zieldatei (fehlt sie unter ## Zieldateien?); "
f"geprueft wuerde gegen {block['ziel'] or 'nichts'}")
befunde += 1
continue
if block["ziel"] is None:
print(f"BEFUND {taskdatei.name}:{block['zeile']}: {block['art']}-Block ohne Zieldatei in einer Ueberschrift")
befunde += 1
+125 -8
View File
@@ -3,7 +3,24 @@
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.
- Zieldatei ist der letzte Pfad in Backticks in einer Ueberschrift (## bis ####) oberhalb des Blocks, den
PFAD erkennt: mit Schraegstrich und Endung, ohne Schraegstrich nur mit py, ts, tsx, toml, md, css, json.
Erkennt PFAD in der Ueberschrift nichts, ist Ziel der letzte Token, der (ohne "./") genau so unter
"## Zieldateien" derselben Taskdatei steht (seit 2026-10-17): So wird eine Datei ohne Endung
(`host/logrotate/edge-traefik`, `Dockerfile`, `.gitignore`) planbar. Ein absoluter Eintrag zaehlt
relativ zum Ordner vor "docs/plans" im Pfad des Planordners; ohne diesen zaehlen nur relative Eintraege.
Ordner, Muster mit "*" und Pfade mit ".." unter Zieldateien sind kein Ziel.
- Befund statt stillem Ziel (seit 2026-10-17): Nennt eine Ueberschrift kein Ziel, aber einen Token, der
nach einer Datei aussieht, ist jeder schreibende Block danach ein Befund, auch unter Ueberschriften
ohne Pfad, bis eine Ueberschrift mit Ziel kommt; nichts wird geschrieben. Nach einer Datei sehen aus:
ein relativer Pfad mit Schraegstrich und ein Name ohne Schraegstrich, der direkt in der Kopie als Datei
liegt oder Dateiname eines Eintrags unter Zieldateien ist. Vorher ging der Block still ins vorige Ziel.
Kein Befund sind Ordner (hinten "/"), absolute Pfade (vorn "/"), Token mit Leerraum und Bezeichner
ohne gleichnamige Datei: Bestehende Plaene nennen sie in Ueberschriften als Kontext und meinen das
vorige Ziel. Nicht gesehen: eine neue Datei ohne Endung, ohne Schraegstrich und ohne Eintrag unter
Zieldateien, ein absoluter Pfad ohne Endung und eine Ueberschrift, die neben einem PFAD-Treffer eine
Datei ohne Endung nennt (dann gilt der PFAD-Treffer). Gleiche Regel in plan_bloecke.py (dort mit
--projekt statt Kopie und Planordner); Regressionsnachweis: plan_ziel_treiber.py.
- 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.
@@ -21,26 +38,117 @@ Exit 0 ohne Befund, 2 mit Befund.
import re
import sys
from pathlib import Path
from pathlib import Path, PureWindowsPath
UEBERSCHRIFT = re.compile(r"^#{2,4} ")
PFAD = re.compile(r"`([^`\s]+/[^`\s]+\.[A-Za-z0-9]+|[^`\s/]+\.(?:py|ts|tsx|toml|md|css|json))`")
TOKEN = re.compile(r"`([^`\s]+)`")
# Eintrag unter "## Zieldateien", dieselbe Form wie in welle_vergleich.py
ZIEL = re.compile(r"^- `(.+?)`(?:\s*\((.*?)\))?")
def wurzeln(planordner: Path) -> list[PureWindowsPath]:
"""Der Projektordner vor "docs/plans" im Pfad des Planordners, wie uebergeben und aufgeloest."""
aus = []
for pfad in (planordner.absolute(), planordner.resolve()):
teile = pfad.parts
for i in range(len(teile) - 2, 0, -1):
if teile[i] == "docs" and teile[i + 1] == "plans":
aus.append(PureWindowsPath(*teile[:i]))
break
return aus
def zieldateien(zeilen: list[str], wurzeln: list[PureWindowsPath]) -> set[str]:
"""Relative Pfade unter "## Zieldateien"; ein absoluter zaehlt relativ zu einer der wurzeln.
Ordner (hinten "/"), Muster mit "*" und Pfade mit ".." sind kein Ziel.
"""
drin, menge = False, set()
for zeile in zeilen:
if zeile.startswith("## "):
drin = zeile.startswith("## Zieldateien")
continue
treffer = ZIEL.match(zeile) if drin else None
if not treffer or treffer.group(1).endswith(("/", "\\")) or "*" in treffer.group(1):
continue
pfad = PureWindowsPath(treffer.group(1))
if pfad.drive or pfad.root:
relativ = None
for wurzel in wurzeln:
try:
relativ = pfad.relative_to(wurzel)
break
except ValueError:
continue
if relativ is None:
continue
pfad = relativ
if ".." not in pfad.parts:
menge.add(pfad.as_posix())
return menge
def ist_datei(ordner: Path | None, name: str) -> bool:
"""Liegt `name` als Datei direkt im ordner? Nie fuer einen Namen mit Laufwerk oder Wurzel."""
if ordner is None or PureWindowsPath(name).drive or PureWindowsPath(name).root:
return False
try:
return (ordner / name).is_file()
except (OSError, ValueError):
return False
def ordne(zeile: str, geplant: set[str], ordner: Path | None) -> tuple[str | None, list[str]]:
"""Zieldatei einer Ueberschrift und die Token, die nach einer Datei aussehen, ohne erkannt zu sein.
Ziel ist der letzte PFAD-Treffer, ohne ihn der letzte Token, der (ohne "./") unter "## Zieldateien"
steht. Ohne Ziel gelten als offen: relative Pfade mit Schraegstrich (kein Ordner, nicht absolut) und
Namen ohne Schraegstrich, die direkt im ordner als Datei liegen oder Dateiname eines Eintrags sind.
Nachbesserung 2026-10-17 (Plankritik Paket R1 im Edge-Stack, Befunde 1 und 5): "Stelle 30 —
`host/logrotate/edge-traefik`" setzte kein Ziel, der Block "Die ganze Datei ist neu" ueberschrieb
`tests/systemd/test-units.sh` mit Exit 0; `Dockerfile` ging ebenso still ins vorige Ziel. `config/`
und `/proc` stehen in bestehenden Plaenen ueber Bloecken, die das vorige Ziel meinen.
"""
treffer = PFAD.findall(zeile)
if treffer:
return treffer[-1], []
token = TOKEN.findall(zeile)
ziele = [PureWindowsPath(t).as_posix() for t in token if PureWindowsPath(t).as_posix() in geplant]
if ziele:
return ziele[-1], []
namen = {eintrag.rsplit("/", 1)[-1] for eintrag in geplant}
offen = [t for t in token if ("/" in t and not t.startswith("/") and not t.endswith("/"))
or ("/" not in t and (t in namen or ist_datei(ordner, t)))]
return None, offen
def schreibt(marke: str) -> bool:
"""Marken, nach denen main() in die Zieldatei schreibt; bei einer neuen Marke hier mitziehen."""
return (marke == "wird:" or "Die ganze Datei ist neu" in marke or "vollständig ersetzt" in marke
or marke.startswith("Häng diesen Block ans Ende"))
def normal(text: str) -> str:
return text.replace("\r\n", "\n").replace("\r", "\n")
def bloecke(taskdatei: Path):
def bloecke(taskdatei: Path, kopie: Path | None = None):
zeilen = normal(taskdatei.read_text(encoding="utf-8")).split("\n")
geplant = zieldateien(zeilen, wurzeln(taskdatei.parent))
ziel = None
# (Zeile, Token) der letzten Ueberschrift ohne Ziel, die nach einer Datei aussieht; None, sobald ein Ziel kommt
ungeklaert = None
i = 0
while i < len(zeilen):
zeile = zeilen[i]
if UEBERSCHRIFT.match(zeile):
treffer = PFAD.findall(zeile)
if treffer:
ziel = treffer[-1]
# Der Generator laeuft verzahnt mit main(): Die Kopie traegt hier schon die Bloecke davor
neu, offen = ordne(zeile, geplant, kopie)
if neu:
ziel, ungeklaert = neu, None
elif offen:
ungeklaert = (i + 1, offen)
if zeile.lstrip().startswith("```"):
einzug = len(zeile) - len(zeile.lstrip())
kern = zeile.lstrip()
@@ -58,7 +166,8 @@ def bloecke(taskdatei: Path):
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}
yield {"ziel": ziel, "marke": marke, "zeile": start + 1, "text": "\n".join(inhalt), "einzug": einzug,
"ungeklaert": ungeklaert}
i += 1
@@ -114,7 +223,7 @@ def main() -> int:
taskdatei = plan / name
alt = None
zaehler = {"neu": 0, "ersetzt": 0, "angehaengt": 0, "uebergangen": 0}
for block in bloecke(taskdatei):
for block in bloecke(taskdatei, kopie):
marke, ziel = block["marke"], block["ziel"]
if block["einzug"] > 0:
zaehler["uebergangen"] += 1
@@ -122,6 +231,14 @@ def main() -> int:
if marke == "Aus:" or marke.startswith("Aus, Zeile"):
alt = block
continue
if block["ungeklaert"] and schreibt(marke):
zeile_u, pfade = block["ungeklaert"]
print(f"BEFUND {name}:{block['zeile']}: Ueberschrift in Zeile {zeile_u} nennt "
f"{', '.join(f'`{p}`' for p in pfade)} und keine erkannte Zieldatei (fehlt sie unter ## Zieldateien?); "
f"der Block ginge nach {ziel or 'keinem Ziel'}, nichts geschrieben")
befunde += 1
alt = None
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")
@@ -0,0 +1,246 @@
"""Regressionsnachweis fuer die Zielzuordnung von plan_materialisiere.py und plan_bloecke.py.
Anlass 2026-10-17 (Plankritik von Paket R1 im Edge-Stack, Befunde 1 und 5): Die Ueberschrift
"#### Stelle 30 — `host/logrotate/edge-traefik`" nennt einen Pfad ohne Endung. PFAD erkannte ihn nicht,
das Ziel blieb beim vorigen (`tests/systemd/test-units.sh`), und der Block "Die ganze Datei ist neu"
ueberschrieb es mit Exit 0 und "0 Befund(e)". Die Faelle unten bauen diese Form nach, an erster und an
letzter Stelle, ueber eine pfadlose Unterueberschrift hinweg und fuer jede schreibende Marke, dazu die
Ueberschriften aus bestehenden Plaenen, die kein Befund sein duerfen (Ordner, absoluter Pfad, erkannte
Zieldatei neben einem Pfad ohne Endung, Token mit Leerraum).
Seit dem Nachtrag vom selben Tag ist eine Datei ohne Endung Ziel, wenn sie genau so unter "## Zieldateien"
steht, auch als absoluter Eintrag; ein Name ohne Schraegstrich, der in der Kopie liegt oder der Dateiname
eines Eintrags ist, ist ein Befund (vorher ging `Dockerfile` still ins vorige Ziel). Die Faelle dazu stehen
am Ende der Liste; "{PROJEKT}" in einer Taskdatei ist der absolute Projektordner des Falls.
Grundlinie zuerst, dann jeder Fall einzeln in einem frischen Wegwerfordner. Je Fall werden Exit-Code,
ausloesende Meldung und bei plan_materialisiere.py der Inhalt jeder Datei der Kopie geprueft.
Aufruf: python plan_ziel_treiber.py [--werkzeuge <ordner>] Vorgabe: der Ordner dieses Skripts.
Exit 0, wenn jeder Fall so ausgeht wie erwartet, sonst 2.
"""
import argparse
import shutil
import subprocess
import sys
import tempfile
from pathlib import Path
# Projekt jedes Falls: eine Datei mit Endung, eine ohne, eine Doku, zwei ohne Endung im Wurzelordner
BASIS = {
"tests/a.sh": "eins\nzwei\ndrei\n",
"host/logrotate/edge-traefik": "alt\n",
"README.md": "# Projekt\n\n- host/ Ordner\n",
"Dockerfile": "FROM alt\n",
".gitignore": "data/\n",
}
Z = "```"
def aus_wird(ueberschrift: str, alt: str, neu: str) -> str:
return f"{ueberschrift}\n\nAus:\n\n{Z}text\n{alt}\n{Z}\n\nwird:\n\n{Z}text\n{neu}\n{Z}\n\n"
def ganz(ueberschrift: str, text: str) -> str:
return f"{ueberschrift}\n\nDie ganze Datei ist neu:\n\n{Z}text\n{text}\n{Z}\n\n"
def anhaengen(ueberschrift: str, text: str) -> str:
return f"{ueberschrift}\n\nHäng diesen Block ans Ende der Datei:\n\n{Z}text\n{text}\n{Z}\n\n"
def befehl(ueberschrift: str) -> str:
return f"{ueberschrift}\n\nAufruf:\n\n{Z}bash\nbash tests/.work/r1-01/umgebung.sh\n{Z}\n\n"
def zieldateien(*pfade: str) -> str:
return "# Task 01\n\n## Zieldateien\n\n" + "".join(f"- `{p}` (geändert)\n" for p in pfade) + "\n## Prüfstand\n\n"
KOPF = "# Task 01\n\n## Prüfstand\n\n"
STELLE_A = aus_wird("#### Stelle 1 — `tests/a.sh`", "zwei", "ZWEI")
OHNE = "`host/logrotate/edge-traefik`"
MELDUNG = f"nennt {OHNE} und keine erkannte Zieldatei"
GEPLANT = zieldateien("tests/a.sh", "host/logrotate/edge-traefik")
# Name, Werkzeug, Taskdatei, erwarteter Exit, erwartete Meldung, Zahl der BEFUND-Zeilen,
# erwartete Dateien nach dem Lauf (nur "mat"; jede nicht genannte Datei bleibt wie in BASIS)
FAELLE = [
("Grundlinie", "mat", KOPF + STELLE_A, 0, "0 Befund(e).", 0,
{"tests/a.sh": "eins\nZWEI\ndrei\n"}),
# Die Form aus R1: Pfad ohne Endung nach einem erkannten Ziel (Schaden an letzter Stelle)
("R1-Form: ganze Datei, letzte Stelle", "mat",
KOPF + STELLE_A + ganz(f"#### Stelle 2 — {OHNE}", "neu"), 2, MELDUNG, 1,
{"tests/a.sh": "eins\nZWEI\ndrei\n"}),
# Dieselbe Form als erste Stelle: vorher kein Ziel, der Block ging still als "uebergangen" durch
("ganze Datei, erste Stelle", "mat",
KOPF + ganz(f"#### Stelle 1 — {OHNE}", "neu") + aus_wird("#### Stelle 2 — `tests/a.sh`", "zwei", "ZWEI"), 2, MELDUNG, 1,
{"tests/a.sh": "eins\nZWEI\ndrei\n"}),
("Aus/wird unter Pfad ohne Endung", "mat",
KOPF + aus_wird("#### Stelle 1 — `tests/a.sh`", "eins", "EINS") + aus_wird(f"#### Stelle 2 — {OHNE}", "zwei", "ZWEI"),
2, MELDUNG, 1, {"tests/a.sh": "EINS\nzwei\ndrei\n"}),
("anhaengen unter Pfad ohne Endung", "mat",
KOPF + STELLE_A + anhaengen(f"#### Stelle 2 — {OHNE}", "vier"), 2, MELDUNG, 1,
{"tests/a.sh": "eins\nZWEI\ndrei\n"}),
# Das Ziel bestimmt die letzte Ueberschrift mit Pfad; eine pfadlose Unterueberschrift aendert daran nichts
("ueber pfadlose Unterueberschrift", "mat",
KOPF + STELLE_A + f"### Stelle 2 — {OHNE}\n\n" + ganz("#### Neue Fassung", "neu"), 2, MELDUNG, 1,
{"tests/a.sh": "eins\nZWEI\ndrei\n"}),
("zwei Stellen, zwei Befunde", "mat",
KOPF + ganz(f"#### Stelle 1 — {OHNE}", "neu") + STELLE_A + ganz(f"#### Stelle 3 — {OHNE}", "neu"), 2, "2 Befund(e).", 2,
{"tests/a.sh": "eins\nZWEI\ndrei\n"}),
# Kein Befund: eine Ueberschrift mit erkannter Zieldatei hebt den Pfad ohne Endung davor auf
("Pfad ohne Endung, danach erkanntes Ziel", "mat",
KOPF + f"### Hinweis zu {OHNE}\n\nProsa.\n\n" + STELLE_A, 0, "0 Befund(e).", 0,
{"tests/a.sh": "eins\nZWEI\ndrei\n"}),
("Pfad ohne Endung ueber einem Befehl", "mat",
KOPF + befehl("#### Messordner `tests/.work/r1-01`") + STELLE_A, 0, "0 Befund(e).", 0,
{"tests/a.sh": "eins\nZWEI\ndrei\n"}),
# Kein Befund: Ueberschriften aus bestehenden Plaenen (Paket E2 Task 367, S3 Task 298, NF Task 703)
("Ordner mit Schraegstrich", "mat",
KOPF + STELLE_A + aus_wird("#### Änderung 2 — dass `host/` nicht der einzige Ort ist", "drei", "DREI"), 0, "0 Befund(e).", 0,
{"tests/a.sh": "eins\nZWEI\nDREI\n"}),
("absoluter Pfad", "mat",
KOPF + STELLE_A + aus_wird("#### Änderung 2 — ein Wegwerf-`/proc` statt zweier Dateien", "drei", "DREI"), 0, "0 Befund(e).", 0,
{"tests/a.sh": "eins\nZWEI\nDREI\n"}),
("erkannte Zieldatei neben Pfad ohne Endung", "mat",
KOPF + STELLE_A + aus_wird("#### Änderung in `README.md`: `glattstellen/beginn` ohne Schlüssel", "host/ Ordner", "host/ Ordner, neu"),
0, "0 Befund(e).", 0, {"tests/a.sh": "eins\nZWEI\ndrei\n", "README.md": "# Projekt\n\n- host/ Ordner, neu\n"}),
("Token mit Leerraum", "mat",
KOPF + STELLE_A + aus_wird("#### Endpunkt `GET /api/paper`", "drei", "DREI"), 0, "0 Befund(e).", 0,
{"tests/a.sh": "eins\nZWEI\nDREI\n"}),
# plan_bloecke.py, Modus vor: Der Aus-Block steht zufaellig auch im vorigen Ziel, deshalb vorher still.
# Je Stelle zwei Befunde (Aus und wird); keiner davon darf zusaetzlich gegen das vorige Ziel laufen.
("bloecke: Grundlinie", "vor", KOPF + STELLE_A, 0, "0 Befund(e).", 0, None),
("bloecke: letzte Stelle", "vor",
KOPF + STELLE_A + aus_wird(f"#### Stelle 2 — {OHNE}", "drei", "DREI"), 2, MELDUNG, 2, None),
("bloecke: erste Stelle", "vor",
KOPF + aus_wird(f"#### Stelle 1 — {OHNE}", "drei", "DREI") + STELLE_A, 2, MELDUNG, 2, None),
("bloecke: ueber pfadlose Unterueberschrift", "vor",
KOPF + STELLE_A + f"### Stelle 2 — {OHNE}\n\n" + aus_wird("#### Änderung", "drei", "DREI"), 2, MELDUNG, 2, None),
("bloecke: Ordner, absolut, Leerraum, erkannte Zieldatei", "vor",
KOPF + STELLE_A + aus_wird("#### Änderung 2 — dass `host/` nicht der einzige Ort ist", "drei", "DREI")
+ aus_wird("#### Änderung 3 — Wegwerf-`/proc`", "eins", "EINS")
+ aus_wird("#### Endpunkt `GET /api/paper`", "eins", "EINS")
+ aus_wird("#### Änderung in `README.md`: `glattstellen/beginn`", "host/ Ordner", "host/ Ordner, neu"),
0, "0 Befund(e).", 0, None),
# Modus nach: Der Baum traegt schon den wird-Text im vorigen Ziel
("bloecke nach: letzte Stelle", "nach",
KOPF + aus_wird("#### Stelle 1 — `tests/a.sh`", "eins", "EINS") + aus_wird(f"#### Stelle 2 — {OHNE}", "drei", "DREI"),
2, MELDUNG, 2, None),
# Nachtrag: Datei ohne Endung unter "## Zieldateien" ist Ziel (R1 Stelle 30 mit Eintrag), letzte und erste Stelle
("Zieldateien: ganze Datei ohne Endung, letzte Stelle", "mat",
GEPLANT + STELLE_A + ganz(f"#### Stelle 2 — {OHNE}", "neu"), 0, "0 Befund(e).", 0,
{"tests/a.sh": "eins\nZWEI\ndrei\n", "host/logrotate/edge-traefik": "neu\n"}),
("Zieldateien: ganze Datei ohne Endung, erste Stelle", "mat",
GEPLANT + ganz(f"#### Stelle 1 — {OHNE}", "neu") + aus_wird("#### Stelle 2 — `tests/a.sh`", "zwei", "ZWEI"),
0, "0 Befund(e).", 0, {"tests/a.sh": "eins\nZWEI\ndrei\n", "host/logrotate/edge-traefik": "neu\n"}),
("Zieldateien: Aus/wird in Datei ohne Endung", "mat",
GEPLANT + STELLE_A + aus_wird(f"#### Stelle 2 — {OHNE}", "alt", "ALT"), 0, "0 Befund(e).", 0,
{"tests/a.sh": "eins\nZWEI\ndrei\n", "host/logrotate/edge-traefik": "ALT\n"}),
# "./Dockerfile" in der Ueberschrift, "Dockerfile" unter Zieldateien
("Zieldateien: ./Dockerfile", "mat",
zieldateien("tests/a.sh", "Dockerfile") + STELLE_A + ganz("#### Stelle 2 — `./Dockerfile`", "FROM neu"),
0, "0 Befund(e).", 0, {"tests/a.sh": "eins\nZWEI\ndrei\n", "Dockerfile": "FROM neu\n"}),
# Absoluter Eintrag: relativ zum Ordner vor docs/plans (Trading Plattform, Task 90: `.gitignore`)
("Zieldateien: absoluter Eintrag", "mat",
zieldateien("tests/a.sh", "{PROJEKT}\\.gitignore") + STELLE_A + aus_wird("### Schritt 2 — `.gitignore`", "data/", "data/\nartefakte/"),
0, "0 Befund(e).", 0, {"tests/a.sh": "eins\nZWEI\ndrei\n", ".gitignore": "data/\nartefakte/\n"}),
# Vorrang: Ein PFAD-Treffer in derselben Ueberschrift bleibt Ziel, auch neben einer Zieldatei ohne Endung
("Zieldateien: PFAD-Treffer hat Vorrang", "mat",
GEPLANT + STELLE_A + aus_wird(f"#### Änderung in `README.md`, Zeile {OHNE}", "host/ Ordner", "host/ Ordner, neu"),
0, "0 Befund(e).", 0, {"tests/a.sh": "eins\nZWEI\ndrei\n", "README.md": "# Projekt\n\n- host/ Ordner, neu\n"}),
# Ein Muster unter Zieldateien ist kein Ziel; der Pfad bleibt ein Befund
("Zieldateien: Muster ist kein Ziel", "mat",
zieldateien("tests/a.sh", "host/logrotate/*") + STELLE_A + ganz("#### Stelle 2 — `host/logrotate/*`", "neu"),
2, "nennt `host/logrotate/*` und keine erkannte Zieldatei", 1, {"tests/a.sh": "eins\nZWEI\ndrei\n"}),
# Name ohne Schraegstrich: liegt in der Kopie oder ist Dateiname eines Eintrags -> Befund statt stillem Ziel
("Dockerfile ohne Eintrag, letzte Stelle", "mat",
KOPF + STELLE_A + ganz("#### Stelle 2 — `Dockerfile`", "FROM neu"), 2, "nennt `Dockerfile` und keine erkannte Zieldatei", 1,
{"tests/a.sh": "eins\nZWEI\ndrei\n"}),
("Dockerfile ohne Eintrag, erste Stelle", "mat",
KOPF + ganz("#### Stelle 1 — `Dockerfile`", "FROM neu") + aus_wird("#### Stelle 2 — `tests/a.sh`", "zwei", "ZWEI"),
2, "nennt `Dockerfile` und keine erkannte Zieldatei", 1, {"tests/a.sh": "eins\nZWEI\ndrei\n"}),
("Dateiname eines Eintrags ohne Pfad", "mat",
GEPLANT + STELLE_A + ganz("#### Stelle 2 — `edge-traefik`", "neu"), 2, "nennt `edge-traefik` und keine erkannte Zieldatei", 1,
{"tests/a.sh": "eins\nZWEI\ndrei\n"}),
# Kein Befund: ein Bezeichner ohne Datei dieses Namens (bestehende Plaene: `main`, `pytest`, `Order`)
("Bezeichner ohne Datei", "mat",
KOPF + STELLE_A + aus_wird("#### Änderung 2 — `main` ruft `pruefe` auf", "drei", "DREI"), 0, "0 Befund(e).", 0,
{"tests/a.sh": "eins\nZWEI\nDREI\n"}),
("bloecke: Zieldateien, Datei ohne Endung", "vor",
GEPLANT + STELLE_A + aus_wird(f"#### Stelle 2 — {OHNE}", "alt", "ALT"), 0, "0 Befund(e).", 0, None),
("bloecke: Zieldateien, absoluter Eintrag", "vor",
zieldateien("tests/a.sh", "{PROJEKT}\\.gitignore") + STELLE_A + aus_wird("### Schritt 2 — `.gitignore`", "data/", "data/\nartefakte/"),
0, "0 Befund(e).", 0, None),
("bloecke: Dockerfile ohne Eintrag", "vor",
KOPF + STELLE_A + aus_wird("#### Stelle 2 — `Dockerfile`", "drei", "DREI"), 2, "nennt `Dockerfile` und keine erkannte Zieldatei", 2, None),
]
def schreibe(pfad: Path, text: str) -> None:
pfad.parent.mkdir(parents=True, exist_ok=True)
pfad.write_bytes(text.encode("utf-8"))
def fahre(werkzeuge: Path, art: str, wurzel: Path, plan: Path) -> subprocess.CompletedProcess:
if art == "mat":
befehl = [str(werkzeuge / "plan_materialisiere.py"), str(plan), str(wurzel / "kopie"), "task-01.md"]
else:
befehl = [str(werkzeuge / "plan_bloecke.py"), art, str(plan / "task-01.md"), "--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 main() -> int:
zerleger = argparse.ArgumentParser()
zerleger.add_argument("--werkzeuge", default=str(Path(__file__).resolve().parent))
werkzeuge = Path(zerleger.parse_args().werkzeuge)
fehler = 0
for name, art, task, exit_erwartet, meldung, anzahl, dateien in FAELLE:
wurzel = Path(tempfile.mkdtemp(prefix="pz_"))
try:
basis = dict(BASIS)
if art == "nach":
# Stand nach der Welle: Stelle 1 angewendet, Stelle 2 im vorigen Ziel statt in edge-traefik
basis["tests/a.sh"] = "EINS\nzwei\nDREI\n"
for rel, text in basis.items():
schreibe(wurzel / "kopie" / rel, text)
# {PROJEKT}: fuer plan_bloecke der Ordner hinter --projekt; fuer plan_materialisiere ein eigener
# Projektordner, unter dessen docs/plans die Taskdatei liegt (die Kopie liegt woanders)
plan = wurzel / "plan"
if "{PROJEKT}" in task:
projekt = wurzel / ("projekt" if art == "mat" else "kopie")
plan = projekt / "docs" / "plans" / "p" if art == "mat" else plan
task = task.replace("{PROJEKT}", str(projekt))
schreibe(plan / "task-01.md", task)
lauf = fahre(werkzeuge, art, wurzel, plan)
ausgabe = lauf.stdout + lauf.stderr
gruende = []
if lauf.returncode != exit_erwartet:
gruende.append(f"Exit {lauf.returncode}, erwartet {exit_erwartet}")
if meldung not in ausgabe:
gruende.append(f"Meldung fehlt: {meldung}")
if ausgabe.count("BEFUND ") != anzahl:
gruende.append(f"{ausgabe.count('BEFUND ')} BEFUND-Zeile(n), erwartet {anzahl}")
# Jede Datei der Kopie: erwarteter Inhalt, sonst unveraendert wie BASIS
for rel, text in basis.items() if dateien is not None else ():
soll = dateien.get(rel, text)
ist = (wurzel / "kopie" / rel).read_bytes().decode("utf-8", errors="replace")
if ist != soll:
gruende.append(f"{rel}: {ist!r}, erwartet {soll!r}")
print(f"{'ok ' if not gruende else 'FEHLER'} {name}: Exit {lauf.returncode}")
if gruende:
fehler += 1
for grund in gruende:
print(" " + grund)
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 von {len(FAELLE)}.")
return 2 if fehler else 0
if __name__ == "__main__":
sys.exit(main())
+31 -5
View File
@@ -5,9 +5,15 @@ Koordinator-Werkzeug im Scratchpad. Fragen und Herkunft der Antwort:
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.
4. Welche Zieldateien blendet git aus? Quelle: `git check-ignore -z --stdin` im Projekt. Getrackte
Dateien zaehlen nicht, auch wenn ein Muster passt (git-check-ignore(1)).
Seit 2026-10-17: Eine ignorierte Zieldatei steht nie in `git status`. Frueher hiess das "unveraendert gegenueber
HEAD" und der Byte-Vergleich fiel aus (Paket R1 im Edge-Stack: 8 und 12 Fehlalarme fuer tests/.work/).
Jetzt wird sie wie jede andere gegen die Kopie verglichen; ob sie vor der Welle schon so aussah, sieht das
Werkzeug bei ihr nicht, und eine geloeschte gilt als geloescht, sobald sie im Baum fehlt.
Was es NICHT sieht (Fangnetz, kein Nachweis):
- Dateien, die .gitignore ausblendet (data/, artifacts/, node_modules/): Ein Executer, der dorthin
schreibt, bleibt unsichtbar.
- Dateien, die .gitignore ausblendet und die keine Zieldatei sind (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.
@@ -99,6 +105,25 @@ def geaendert(projekt: Path, befunde: list[str]) -> dict[str, str]:
return aus
def ignoriert(projekt: Path, pfade: list[str], befunde: list[str]) -> set[str]:
"""Die Zieldateien unter pfade, die git ignoriert. Exit 1 von check-ignore heisst: keine; alles ueber 1 ein Befund."""
if not pfade:
return set()
try:
ergebnis = subprocess.run(
["git", "-C", str(projekt), "check-ignore", "-z", "--stdin"],
input=("\0".join(pfade) + "\0").encode("utf-8"),
capture_output=True,
)
except OSError as fehler:
befunde.append(f"git check-ignore laesst sich nicht starten ({fehler})")
return set()
if ergebnis.returncode not in (0, 1):
befunde.append(f"git check-ignore endet mit {ergebnis.returncode}: {ergebnis.stderr.decode('utf-8', 'replace').strip()}")
return set()
return {p for p in ergebnis.stdout.decode("utf-8", "replace").split("\0") if p}
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)"
@@ -122,6 +147,7 @@ def pruefe(args: argparse.Namespace, befunde: list[str]) -> None:
for name in args.taskdateien:
ziele.update(zieldateien(Path(name), projekt, befunde))
status = geaendert(projekt, befunde)
ign = ignoriert(projekt, sorted(ziele), befunde)
for pfad in sorted(set(status) - set(ziele)):
befunde.append(f"{pfad}: geaendert ({status[pfad]!r}), liegt aber ausserhalb der Zieldateien dieser Welle")
@@ -136,10 +162,10 @@ def pruefe(args: argparse.Namespace, befunde: list[str]) -> None:
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, ""):
if not echt.exists() and pfad not in ign 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:
if pfad not in status and pfad not in ign:
befunde.append(f"{pfad}: Zieldatei ist unveraendert gegenueber HEAD")
continue
if not echt.is_file():
@@ -155,7 +181,7 @@ def pruefe(args: argparse.Namespace, befunde: list[str]) -> None:
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.")
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, {len(ign)} von git ignoriert.")
def main() -> int:
@@ -151,6 +151,64 @@ def fall_relativ_nach_aussen(projekt, mat, task, wurzel):
schreibe(task, text.replace("Keine andere Datei", "- `../anderswo/x.py` (neu)\n\nKeine andere Datei"))
def _ignoriert(projekt, mat, task, erst="neu", letzt="neu"):
"""Seit 2026-10-17: Zieldateien unter einem Pfad, den .gitignore ausblendet (Paket R1 im Edge-Stack: Prüfdateien
unter tests/.work/). .gitignore kommt in den Basis-Commit; work/erst.py steht als erste, work/letzt.py als letzte
Zieldatei in der Liste, beide in Baum und Kopie gleich. erst/letzt: "neu" oder "gelöscht" (dann in keinem Baum)."""
schreibe(projekt / ".gitignore", "work/\n")
git(projekt, "add", ".gitignore")
git(projekt, "commit", "-q", "-m", "ignoriert")
for name, art in (("erst.py", erst), ("letzt.py", letzt)):
if art == "neu":
schreibe(projekt / "work" / name, f"{name} eins\n{name} zwei\n")
schreibe(mat / "work" / name, f"{name} eins\n{name} zwei\n")
text = task.read_text(encoding="utf-8")
text = text.replace("## Zieldateien\n\n", f"## Zieldateien\n\n- `work/erst.py` ({erst})\n", 1)
text = text.replace("\n\nKeine andere Datei", f"\n- `work/letzt.py` ({letzt})\n\nKeine andere Datei", 1)
schreibe(task, text)
def fall_ignoriert_gleich(projekt, mat, task, wurzel):
_ignoriert(projekt, mat, task)
def fall_ignoriert_erste_weicht_ab(projekt, mat, task, wurzel):
_ignoriert(projekt, mat, task)
schreibe(projekt / "work" / "erst.py", "erst.py EINS\nerst.py zwei\n")
def fall_ignoriert_letzte_weicht_ab(projekt, mat, task, wurzel):
_ignoriert(projekt, mat, task)
schreibe(projekt / "work" / "letzt.py", "letzt.py eins\nletzt.py ZWEI\n")
def fall_ignoriert_fehlt_im_baum(projekt, mat, task, wurzel):
_ignoriert(projekt, mat, task)
(projekt / "work" / "letzt.py").unlink()
def fall_ignoriert_fehlt_in_kopie(projekt, mat, task, wurzel):
_ignoriert(projekt, mat, task)
(mat / "work" / "erst.py").unlink()
def fall_ignoriert_geloescht(projekt, mat, task, wurzel):
_ignoriert(projekt, mat, task, letzt="gelöscht")
def fall_ignoriert_nicht_geloescht(projekt, mat, task, wurzel):
_ignoriert(projekt, mat, task, letzt="gelöscht")
schreibe(projekt / "work" / "letzt.py", "bleibt\n")
def fall_ignoriert_getrackt(projekt, mat, task, wurzel):
"""Eine getrackte Datei unter einem ignorierten Muster (git add -f) ist kein ignoriertes Ziel: Unveraendert
gegenueber HEAD bleibt ein Befund."""
_ignoriert(projekt, mat, task)
git(projekt, "add", "-f", "work/erst.py")
git(projekt, "commit", "-q", "-m", "erzwungen")
FAELLE = [
("Grundlinie", fall_grundlinie, 0, "0 Befund(e)."),
("fremde Datei im Baum", fall_fremde_datei, 2, "fremd.py: geaendert"),
@@ -174,6 +232,15 @@ FAELLE = [
# Seit 2026-09-18: Die Klammer hinter dem Pfad ist wahlfrei, Text dahinter erlaubt.
("Zieldateien ohne Klammer", fall_ohne_klammer_grundlinie, 0, "0 Befund(e)."),
("Zieldatei ohne Klammer weicht ab", fall_ohne_klammer_abweichung, 2, "a.py: weicht vom Plan ab -- Zeile 1"),
# Seit 2026-10-17: Zieldateien, die git ignoriert, werden byte-genau verglichen statt als unveraendert gemeldet.
("ignorierte Zieldateien gleich", fall_ignoriert_gleich, 0, "4 byte-gleich mit der Kopie, 1 nur als Menge geprueft, 2 von git ignoriert."),
("ignorierte erste Zieldatei weicht ab", fall_ignoriert_erste_weicht_ab, 2, "work/erst.py: weicht vom Plan ab -- Zeile 1"),
("ignorierte letzte Zieldatei weicht ab", fall_ignoriert_letzte_weicht_ab, 2, "work/letzt.py: weicht vom Plan ab -- Zeile 2"),
("ignorierte Zieldatei fehlt im Baum", fall_ignoriert_fehlt_im_baum, 2, "work/letzt.py: Zieldatei fehlt im Baum"),
("ignorierte Zieldatei fehlt in der Kopie", fall_ignoriert_fehlt_in_kopie, 2, "work/erst.py: fehlt in der Kopie"),
("ignorierte Zieldatei geloescht", fall_ignoriert_geloescht, 0, "0 Befund(e)."),
("ignorierte Loeschung nicht ausgefuehrt", fall_ignoriert_nicht_geloescht, 2, "work/letzt.py: soll geloescht sein, steht aber noch im Baum"),
("getrackte Datei unter ignoriertem Muster unveraendert", fall_ignoriert_getrackt, 2, "work/erst.py: Zieldatei ist unveraendert"),
]