Erste Fassung: Claude-Werkzeuge aus dem Vault
.vault-tools (ohne trading-plattform-pruefung und watermarks-remover), .claude/agents, .claude/skills, .claude/workflows, .claude/settings.json. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
commit
fa1c26b407
137 files changed
+43926
No files matched your search
@@ -0,0 +1,81 @@
|
||||
---
|
||||
type: resource
|
||||
status: active
|
||||
title: "Planwerkzeuge: Plan auf eine Kopie anwenden und jede Welle dagegen vergleichen"
|
||||
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
|
||||
---
|
||||
|
||||
# Planwerkzeuge für die Kopie mit angewendetem Plan
|
||||
|
||||
Die Regel dazu steht in [[AI/Regelwerk/codeworkflow|codeworkflow.md]] („Die Kopie mit dem angewendeten Plan bleibt stehen …"). Hier steht nur, wie man die Skripte benutzt. Alle laufen mit dem Windows-Python: `PYTHONIOENCODING=utf-8 python -X utf8 <skript> …`.
|
||||
|
||||
## Ablauf
|
||||
|
||||
1. **Kopie bauen — erst, wenn der Planer fertig gemeldet hat.** Den Projektbaum ohne `.git`, ohne Geheimnisdateien und ohne `node_modules` in einen Wegwerfordner kopieren. **Ausschlüsse als Pfade vom Wurzelordner angeben, nicht als Ordnernamen:** Ein Ausschluss von „jedem Ordner namens `data`" hat am 2026-09-17 auch das Paket `trading/data` getroffen, und die Suite der Kopie brach mit Sammelfehlern ab. Nach dem Kopieren die Paketliste gegen das Original halten. **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.
|
||||
3. **Auf der Kopie fahren:** Suite, Typprüfung, Mutanten. Ein Mutantenlauf gibt je Mutant die Fehlerzeile aus, nicht nur den Exitcode.
|
||||
4. **Vor jeder Welle und nach jeder Plankorrektur:** `plan_bloecke.py vor <tasks…> --projekt <projekt>` (jeder Aus-Block steht genau einmal im echten Baum) und `plan_zeichen.py <planordner>` (keine Zeilen- und Absatztrenner, Format- und Steuerzeichen in Codeblöcken).
|
||||
5. **Nach jeder Welle:** `welle_vergleich.py --projekt <projekt> --mat <kopie> [--prosa <rel> …] -- <taskdateien der Welle>`. Prüft, ob genau die Zieldateien geändert sind und ob jede byte-gleich mit der Kopie ist. Es zeigt je Datei nur die erste Abweichung — danach `diff` auf die ganze Datei. Eigene Plankorrekturen im Baum meldet es zu Recht als „außerhalb der Zieldateien".
|
||||
6. **Escape-Folgen setzen:** `plan_escape.py <datei> <zeile> <codepunkt-hex> …` ersetzt echte Zeichen in einer Zeile durch ihre Escape-Folge, ohne dass eine Schreibschicht mitredet.
|
||||
|
||||
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.
|
||||
|
||||
8. **Mutanten fahren (seit 2026-09-27):** `mutanten_lauf.py --wurzel <wegwerfkopie> --mutanten <planordner>/mutanten.toml [--nur ID,…] [--nur-muster] [--bericht <datei.json>] [--zeitlimit <s>]`, in WSL mit einem Python ab 3.11. Die Mutanten stehen nicht mehr als Prosa im Plan, sondern in `mutanten.toml` im Planordner. Das Format, die Status und die Abdichtung stehen in `mutanten_lauf_ENTWURF.md`. Das Skript verweigert eine Wurzel mit `.git` und jeden Pfad aus `ohne`. Seit 2026-10-06 sucht es die pytest-Konfiguration wie pytest selbst, aber nie oberhalb der Wurzel, und gibt sie mit `-c` vor; eine fremde `pyproject.toml` im Windows-Temp stört damit nicht mehr, und ein Bedienfehler von pytest steht mit dem Ende der pytest-Ausgabe im Befund statt als `FEHLT` je Test (Entwurf, Abschnitt 3, „Konfiguration“ und „Abbruch von pytest“). Ob die Suche pytest selbst entspricht, prüft `mutanten_lauf_probe_konfig.py <mutanten_lauf.py> <arbeitsordner>` mit dem Python eines Venvs, in dem pytest liegt (26 Aufbauten, Exit 0 ohne Abweichung). Es fährt zuerst die Grundlinie, dann je Mutant nur die genannten Tests, setzt die Datei zurück und prüft das per SHA-256. Je Test gibt es die Fehlermeldung mit aus; ob ein Test an der gemeinten Stelle fällt, liest man dort. `--nur-muster` prüft nur, ob jedes Muster genau einmal dasteht, ohne pytest. Die Kopie legt man in WSL unter `/tmp` an (`cp -a`, danach `__pycache__` löschen). Auf `/mnt/d` läuft es auch, nur langsamer. **Aus der Git-Bash ein Skript nie mit `wsl … bash /mnt/d/…` starten:** Die Git-Bash macht aus dem Pfad einen Windows-Pfad. Aus der PowerShell starten oder `MSYS_NO_PATHCONV=1` davorsetzen. Ein SIGTERM auf `mutanten_lauf.py` selbst (etwa `timeout -k` oder ein Hilfsagent, der abgebrochen wird) löst dort einen eigenen Handler aus: Der laufende Mutant wird per SHA-Probe zurückgesetzt, die Temp-Ordner werden geräumt, und die Ausgabe trägt `ABBRUCH: SIGTERM` (Treiber-Fall 35, `mutanten_lauf_treiber.py`).
|
||||
**Die Plankritik fährt die Mutanten selbst (seit 2026-09-27, nachgebessert 2026-09-28):** Liegt `mutanten.toml` im Planordner, startet `plankritik.js` in der Vorbereitung `mutanten_lauf_kopie.sh`. Das Skript kopiert die Kopie mit angewendetem Plan nach `/tmp` in WSL, fährt dort den Läufer in einem eigenen Netz-Namensraum (`unshare --map-current-user --net`, gemessen in WSL Debian als Benutzer `claude`: UID innen bleibt 1000, nur Schnittstelle `lo`, jede Verbindung nach außen scheitert; seit 2026-10-09 mit `lo` oben, siehe Nachtrag unten) und gibt die Ausgabe an die Linse `arithmetik`. Fehlt `unshare` ohne Netz, fehlen `ip`, `setpriv` oder `python3` oder kommt `lo` im Namensraum nicht hoch, bricht das Skript vorher mit `MUTANTEN_LAUF_FEHLER` ab, statt ungesperrt oder ohne `lo` zu laufen. Der Arg `mutanten` wählt `voll` (Vorgabe), `muster` oder `aus`; die Rückgabe trägt dazu `mutanten_status` (`aus`, `ohne_kopie`, `keine_toml`, `ohne_ergebnis` oder `exit_<n>`) und `mutanten_modus` (`voll`, `muster`, `aus`). Der Wrapper gibt vor dem Lauf eine Zeile `modus: voll` oder `modus: muster (…)` aus; bei `modus: muster` weiß die Linse, dass kein Mutant gefahren ist. Der ganze Lauf hat 540 s (`BUDGET_S`, `timeout -k 15`), weil das Bash-Werkzeug eines Hilfsagenten nach zehn Minuten endet; eine überschrittene Grenze meldet die Zeile `ZEITGRENZE`. Je Mutant gilt zusätzlich ein Zeitlimit `ZEITLIMIT_S` (Vorgabe 120 s, muss kleiner sein als `BUDGET_S`): B1 mit 74 Mutanten brauchte 168 s gesamt, rund 2 s je Mutant. Bei Exit 2 oder 3 des Läufers sagt die Linse, der Lauf sei vor oder während der Mutanten gescheitert. Pfade übergibt man mit Vorwärtsschrägstrichen: `wsl -d Debian -- …` reicht die Befehlszeile an die Linux-Shell, und die verschluckt Backslashes.
|
||||
|
||||
9. **Korrektur als Ersetzungsliste (seit 2026-10-13):** `ersetzungen_einsetzen.py <planordner> <korrekturdatei> [--pruefen]` setzt eine Liste aus Abschnitten `## Ersetzung N — Titel` mit `Datei:`, `Alt:` und `Neu:` (Blöcke zwischen `~~~~text` und `~~~~`) der Reihe nach ein. Jeder Alt-Text muss zum Zeitpunkt seiner Ersetzung genau einmal in seiner Datei stehen, sonst schreibt das Skript gar nichts. Erst mit `--pruefen`, dann ohne; danach Schritt 4 und eine frisch gebaute Kopie. Die Korrekturdatei liest das Skript relativ zum aktuellen Ordner, nicht zum Planordner. Belegt am 2026-10-13 im Paket FP der Trading Plattform: 129 Ersetzungen in sechs Dateien, Prüflauf und Einsetzen ohne Befund.
|
||||
|
||||
## Was belegt ist
|
||||
|
||||
Seit 2026-09-27 gibt es `mutanten_lauf.py` und `mutanten_lauf_treiber.py` (Punkt 3 der Arbeitsweise vom 2026-09-26). Der Treiber fährt die 35 Angriffsfälle aus dem Entwurf auf einem kleinen Projekt unter `/tmp` und prüft nach jedem Fall, dass das Projekt byte-gleich zurückgesetzt ist. Am selben Tag hat der Koordinator neun Fehler in eine Kopie des Läufers eingebaut, und der Treiber hat jeden an seinem Fall gefangen:
|
||||
- „passed“ zählt als rot
|
||||
- kein Zurücksetzen
|
||||
- keine `.git`-Prüfung
|
||||
- `aus` gleich `wird` erlaubt
|
||||
- HOME nicht abgedichtet
|
||||
- Sammelfehler nicht rot
|
||||
- Grundlinie lässt Übersprungenes durch
|
||||
- Zeitlimit zählt als OK
|
||||
- eine Teilzeile zählt als Treffer
|
||||
|
||||
Das Skript dazu liegt außerhalb des Vaults und ist nicht Teil der Werkzeuge. Erster echter Einsatz war am selben Tag B1 der Trading Plattform: 74 von 74 Mutanten getötet, alle Fehlschläge Zusicherungen, die Kopie danach unverändert. Eine Lehre aus der Übertragung: Nennt ein Plan einen parametrisierten Test ohne Fall, gehören nur die Fälle in `rot_bei`, die der Mutant umkippt. Bei D1 wären drei von fünf Fällen unter `return False` grün geblieben.
|
||||
|
||||
Nachbesserung vom 2026-09-28 (Prüfbericht desselben Tages): `mutanten_lauf_kopie.sh` läuft jetzt unter `unshare --map-current-user --net`, mit `python3 -u` und einem Zeitlimit je Mutant (`ZEITLIMIT_S`, kleiner als `BUDGET_S`), schreibt vor dem Lauf eine Modus-Zeile und räumt bei SIGTERM auf (`timeout -k 15`, eigener Handler in `mutanten_lauf.py`). `plankritik.js` gibt zusätzlich `mutanten_status` und `mutanten_modus` zurück und erklärt der Linse `arithmetik` Modus `muster` und Exit 2/3. Treiber-Fall 35 (SIGTERM mitten in einem hängenden Mutanten) und die zusätzlichen Fälle in `faelle_workflows.mjs`/`mutanten_workflows.py` sind gefahren und grün; die Rot-Ausgaben davor stehen im Bericht des Executers vom selben Tag.
|
||||
|
||||
Nachbesserung vom 2026-10-06 (Plankritik 2 von C1 im Projekt After Install, Plan `docs/plans/2026-10-06-pytest-konfiguration.md` und seine Nachbesserung): pytest-Konfiguration wie pytest selbst, aber nie oberhalb der Wurzel; Abbruch von pytest mit Ursache statt `FEHLT`. Belegt am selben Tag:
|
||||
- Treiber 45 von 45 grün. Die neuen Fälle 36 bis 45 waren je mindestens einmal rot: gegen den Läufer vor der Änderung (36, 38, 40), gegen den Stand vor der Nachbesserung (40, 41, 43, 45) und gegen kaputte Varianten (37 ohne eigene Konfiguration, 39 ohne `--confcutdir`, 42 ohne Rückgabewert 3, 43 ohne `setup.py`-Zweig oder ohne zweite Suche, 44 nach dem ersten Abbruch ohne weitere Gruppe).
|
||||
- Gegenprobe an allen sechs `mutanten.toml` im Vault, alter gegen neuer Läufer auf Kopien aus `git archive HEAD` unter `/tmp`: Headscale A, Mattermost, After Install C1 und D-40 gleich (175, 130, 69 und 41 Mutanten `OK`); Headscale B und Trading Plattform WH enden bei beiden mit Exit 2, weil ihr Stand nicht zum Plan passt. Unter dem Windows-Temp endete C1 mit dem alten Läufer mit 101 × `FEHLT`, mit dem neuen mit 69 `OK`.
|
||||
- Ein `pruefer` fand in der ersten Fassung den Symlink-Fall und fehlende Fälle; die Nachbesserung behob beides. Ein zweiter `pruefer` fand danach keinen Verhaltensfehler mehr, nur eine Aufräumliste (Kopfzeile je Interpreter ohne eigenen Fall, Kurzfassung bei Rückgabewert 3, `;` und Seitenvorschub in `tox.ini`, Selbstprüfung der Probe, lange Fassung im Bericht, zwei Lücken benennen); sie ist in Nachbesserung 2 erledigt, Treiber 46 von 46.
|
||||
- Rohdaten: `AI/secondbrain/linux-haertung-after-install-memory/2026-10-06-c1-abschluss/roh/` (`02-*`, `03-gegenprobe/`, `07-*`, `08-gegenprobe2/` mit dem nachgebesserten Läufer, `09-*` für Nachbesserung 2).
|
||||
|
||||
Nachbesserung vom 2026-10-09 (Paket RB3 der Trading Plattform, Plan `docs/plans/2026-10-09-mutanten-kopie-loopback.md`): Im frischen Netz-Namensraum war `lo` aus. Jeder Test mit einem Server auf `127.0.0.1` scheiterte dort mit „Network is unreachable“ (`Errno 101`), auch ohne Mutant; die Mutanten T1 und T12 des Pakets liefen deshalb von Hand außerhalb des Namensraums. `mutanten_lauf_kopie.sh` startet den Läufer jetzt mit `unshare --map-current-user --net --keep-caps`, fährt `lo` mit `ip` hoch, wirft mit `setpriv` alle vererbbaren und ambienten Fähigkeiten ab und geht per `exec` in `python3` über, ohne Zwischenschale. Eine Vorprüfung vor dem Kopieren verlangt `ip`, `setpriv` und `python3`, ein nutzbares `lo` mit einer Verbindung über `127.0.0.1` und `CapEff` und `CapAmb` gleich 0; sonst endet das Skript mit `MUTANTEN_LAUF_FEHLER` und Exit 2. `127.0.0.1` ist im Namensraum dessen eigenes `lo`, ein Dienst des Rechners bleibt unerreichbar. Belegt am selben Tag:
|
||||
- `mutanten_lauf_kopie_treiber.py` ruft das Skript wie die Plankritik mit Windows-Pfaden auf und fährt die Fälle W1 bis W13: Server auf `127.0.0.1` (W1), keine Route nach außen und `ENETUNREACH` (W2), uid wie außen und keine Fähigkeiten (W3), `ip`, `setpriv` oder `python3` fehlen (W4, W5, W9), Zeitgrenze mit `timeout` als Elternprozess des Läufers und ohne Prozessreste (W6), Grundlinie ohne Server in beiden Modi (W7), Dienst des Rechners unerreichbar (W8). Seit Nachbesserung 1 auch: `ip` scheitert in der Probe (W10), `ip` meldet Erfolg und `lo` bleibt aus (W11), `ip` scheitert erst im Hauptaufruf nach der Modus-Zeile (W12), `setpriv` wirft keine Fähigkeit ab (W13). W10 bis W13 ersetzen dazu einzelne Werkzeuge durch kleine `sh`-Skripte. Neuer Stand ohne Fehler.
|
||||
- Gegen das Skript vor der Änderung rot: W1, W2, W4, W5, W9. Gegen kaputte Varianten genau an ihrem Fall, gefahren mit W1 bis W9: ohne `setpriv` W3, `setpriv` ohne `exec` W6, ohne Vorprüfung W4, W5 und W9, ohne `--keep-caps` alle (die Vorprüfung meldet `Operation not permitted`), ohne Namensraum W2 und W8. Gefahren mit W1 bis W13 (Nachbesserung 1, `docs/plans/2026-10-09-mutanten-kopie-loopback-nachbesserung-1.md`): ohne `exit 2` nach der Probe W10, W11 und W13; Probe ohne `setpriv` W1 bis W9 und W12; Hauptaufruf ohne den Zweig nach `||` W12; Probe ohne Fähigkeitsprüfung W13.
|
||||
- Nicht abgeworfen und nicht geprüft: die Begrenzungsmenge (`CapBnd`) und `no_new_privs`. `plankritik.js` führt eine Vorprüfung mit `MUTANTEN_LAUF_FEHLER` wie bisher bei fehlendem `unshare` als `ohne_ergebnis`, ohne den Text der Meldung.
|
||||
- Die Fähigkeiten prüft das Skript nur an der Probe und nur `CapEff` und `CapAmb`, nicht am Läufer selbst. Alle vier Mengen am Läufer prüft Treiber-Fall W3.
|
||||
- Die Vorprüfung steht vor der Suche nach `mutanten.toml`. Fehlt auf einem Rechner `ip`, `setpriv` oder `python3` oder kommt `lo` nicht hoch, endet das Skript deshalb auch für einen Planordner ohne `mutanten.toml` mit `MUTANTEN_LAUF_FEHLER` und Exit 2 statt mit `KEINE_MUTANTEN_TOML`. `plankritik.js` meldet dann `ohne_ergebnis` statt `keine_toml`, und die Linse erfährt nicht, dass die `mutanten.toml` fehlt. Das bleibt so.
|
||||
- Lauf mit echten Daten: die 73 Mutanten des Pakets RB3 (Kopie `rb3d-kopie`) durch das neue Skript, alle `OK`, T1 und T12 im Namensraum.
|
||||
- Ein `pruefer` fand keinen blockierenden Befund. Seine Lücken (kein Fall für den Fehlerzweig der Probe und den Zweig nach `||` im Hauptaufruf, Kopfkommentar zu weit gefasst, der Satz zur Reihenfolge oben) behebt Nachbesserung 1. Rohdaten: `AI/secondbrain/trading-plattform-memory/2026-10-09-mkopie-roh/`.
|
||||
|
||||
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-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).
|
||||
|
||||
Seit 2026-09-22 behandelt `rot_nachweis.py` `tests/conftest.py` als Produktivseite. Vorher zählte alles unter `tests/` als Testdatei — lag die Korrektur eines Tasks selbst in `conftest.py` (Trading Plattform, Task 369: die Ersatzfunktionen der Netzsperre trugen eine andere Vorgabe als die Originale), wendete der Lauf „ohne Umsetzung" sie mit an und meldete `GRUEN pytest: 147 passed` — der eine Zustand, in dem der Nachweis nichts misst, sah aus wie ein sauberer Befund. Gemessen an derselben Basis-Kopie und demselben Task, altes gegen neues Skript: `GRUEN 147 passed` → `ROT 3 failed, 144 passed` mit der gemeinten Fehlerzeile (`assert ('', '') == ('probe-thema', '')`). Die Gegenprobe für den anderen Fall — eine neue Fixture des Tasks liegt in `conftest.py` (Task 368) — mit `--conftest-als-test` `ROT 1 failed, 11 passed` wie vor der Änderung, ohne Schalter `ROT 6 failed, 6 passed` mit `AttributeError: module 'conftest' has no attribute '_gesperrte_pfade'` als erster Fehlerzeile: rot aus dem falschen Grund, aber sichtbar, und das Skript sagt je Task, wie es `conftest.py` behandelt hat. Was es weiter nicht sieht: ob ein Task beides in `conftest.py` legt, Fixture und Korrektur — dann ist keine der beiden Einstellungen richtig, und der Nachweis läuft von Hand.
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
Das ältere `.vault-tools/plan-diff/plan_diff.py` vergleicht Ganzdatei-Blöcke einer früheren Plankonvention und bleibt dafür in Gebrauch.
|
||||
+1023
File diff suppressed because it is too large.
Load diff
+634
@@ -0,0 +1,634 @@
|
||||
---
|
||||
type: plan
|
||||
title: "Mutantenläufer: Nachbesserung 2 (Aufräumliste der zweiten Prüfung)"
|
||||
status: active
|
||||
summary: "Delta zur Nachbesserung 1: Kopfzeile je Interpreter mit eigenem Fall, Kurzfassung bei Rückgabewert 3 aus der letzten INTERNALERROR-Zeile, lange Fassung im Bericht, Abschnittszeile mit splitlines, Probe mit Selbstprüfung und zwei neuen Aufbauten, Treiber-Fall 46, zwei Lücken benannt."
|
||||
tags: [plan, vault-tools, pruefwerkzeuge]
|
||||
created: 2026-10-06
|
||||
updated: 2026-10-06
|
||||
modell: sonnet
|
||||
effort: low
|
||||
modell_begruendung: "Jeder Block steht wörtlich da; Entwurf und LIESMICH tragen typografische Zeichen, die Haiku still ersetzt."
|
||||
---
|
||||
|
||||
Übersicht: [LIESMICH der Planwerkzeuge](../../LIESMICH.md) · Vorgänger: [Nachbesserung 1](2026-10-06-pytest-konfiguration-nachbesserung-1.md)
|
||||
|
||||
# Mutantenläufer: Nachbesserung 2
|
||||
|
||||
## Warum so
|
||||
|
||||
Der zweite `pruefer` fand am 2026-10-06 keinen Verhaltensfehler mit Wirkung auf die Projekte, nur eine Aufräumliste, und hielt eine weitere volle Prüfrunde für entbehrlich. Umgesetzt:
|
||||
|
||||
| Befund | Hier |
|
||||
|---|---|
|
||||
| Kopfzeile je Interpreter ohne eigenen Fall (wichtig) | Fall 44 zählt genau zwei Kopfzeilen, je eine je Interpreter |
|
||||
| Kurzfassung bei Rückgabewert 3 ohne Fall | Fall 46 (Mutant lässt `conftest.py` werfen). Dabei gefunden: „letzte Zeile“ war die Summenzeile `no tests ran …`; jetzt die letzte `INTERNALERROR>`-Zeile |
|
||||
| `;` als Kommentarzeichen ohne Fall | Aufbau `tox_abschnitt_mit_semikolon` in der Probe |
|
||||
| iniconfig trennt Zeilen mit `splitlines` | Läufer auch; Aufbau `tox_seitenvorschub_vor_abschnitt` |
|
||||
| Lange Fassung unter einem Mutanten verloren | Bericht trägt beim Test `ausgabe`; Fälle 40 und 46 prüfen es |
|
||||
| Probe prüft ihre Voraussetzung nicht | Selbstprüfung: ohne `-c` Rückgabewert 0 (außer `knoten_fehl*`), mit `conftest.py` im Aufbau mindestens eines geladen |
|
||||
| `rootdir` unter der Wurzel, Zeilentrenner | als Lücken benannt |
|
||||
| `safe_exists` fängt auch `ValueError` | `_existiert` ebenso |
|
||||
| LIESMICH: Rohdaten `08-gegenprobe2/`, Satz zur Nachbesserung | nachgezogen |
|
||||
|
||||
**Gemessen vom Koordinator am 2026-10-06** (Rohdaten `AI/secondbrain/linux-haertung-after-install-memory/2026-10-06-c1-abschluss/roh/09-*`): neuer Stand 46 von 46; Stand nach Nachbesserung 1 rot bei 40, 43, 46; Varianten rot genau an ihrem Fall (eine Kopfzeile → 44 an beiden Stellen; erste statt letzte `INTERNALERROR>`-Zeile → 46; ohne den Zweig für 3 → 46; kaputtes `conftest.py` der ersten Probe → 43; ohne `;` → 43).
|
||||
|
||||
## Zieldateien
|
||||
|
||||
Pfade relativ zu `D:/Claude/.vault-tools/plan-kopie/`:
|
||||
|
||||
- `mutanten_lauf.py` (geändert)
|
||||
- `mutanten_lauf_treiber.py` (geändert)
|
||||
- `mutanten_lauf_probe_konfig.py` (geändert)
|
||||
- `mutanten_lauf_ENTWURF.md` (geändert)
|
||||
- `LIESMICH.md` (geändert)
|
||||
|
||||
## Verbote
|
||||
|
||||
- Nur die fünf Zieldateien ändern. Kein `git add`, kein `git commit`.
|
||||
- Jeden Block byte-genau übernehmen; der ganze Aus-Block als `old_string`, der ganze wird-Block als `new_string`. Steht ein Aus-Block nicht genau einmal da oder weicht eine Ausgabe ab: anhalten und melden.
|
||||
- `python -m py_compile` legt `__pycache__` an; den Ordner danach löschen.
|
||||
|
||||
## Schritt 1 — Tests zuerst: Treiber und Probe
|
||||
|
||||
### `mutanten_lauf_treiber.py`, Stelle 1 von 9
|
||||
|
||||
Aus:
|
||||
|
||||
```python
|
||||
#!/usr/bin/env python3
|
||||
"""Treiber fuer mutanten_lauf.py: 45 Angriffsfaelle auf einer Wegwerfkopie.
|
||||
|
||||
```
|
||||
|
||||
wird:
|
||||
|
||||
```python
|
||||
#!/usr/bin/env python3
|
||||
"""Treiber fuer mutanten_lauf.py: 46 Angriffsfaelle auf einer Wegwerfkopie.
|
||||
|
||||
```
|
||||
|
||||
### `mutanten_lauf_treiber.py`, Stelle 2 von 9
|
||||
|
||||
Aus:
|
||||
|
||||
```python
|
||||
Interpretergruppen an erster und an letzter Stelle, ein ueberlanger Knoten.
|
||||
"""
|
||||
```
|
||||
|
||||
wird:
|
||||
|
||||
```python
|
||||
Interpretergruppen an erster und an letzter Stelle, ein ueberlanger Knoten.
|
||||
Fall 46 (Nachbesserung 2): Rueckgabewert 3 unter einem Mutanten, Kurzfassung in
|
||||
der Mutantenzeile und lange Fassung im Bericht.
|
||||
"""
|
||||
```
|
||||
|
||||
### `mutanten_lauf_treiber.py`, Stelle 3 von 9
|
||||
|
||||
Aus:
|
||||
|
||||
```python
|
||||
import hashlib
|
||||
import os
|
||||
```
|
||||
|
||||
wird:
|
||||
|
||||
```python
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
```
|
||||
|
||||
### `mutanten_lauf_treiber.py`, Stelle 4 von 9
|
||||
|
||||
Aus:
|
||||
|
||||
```python
|
||||
))
|
||||
ergebnis = starte_laeufer(wurzel, toml)
|
||||
mutantenzeile = [z for z in ergebnis.stdout.split("\n") if z.startswith("M40 ")]
|
||||
ok = (
|
||||
```
|
||||
|
||||
wird:
|
||||
|
||||
```python
|
||||
))
|
||||
bericht = rahmen / "bericht40.json"
|
||||
ergebnis = starte_laeufer(wurzel, toml, bericht_pfad=bericht)
|
||||
mutantenzeile = [z for z in ergebnis.stdout.split("\n") if z.startswith("M40 ")]
|
||||
try:
|
||||
ausgabe_bericht = json.loads(bericht.read_text(encoding="utf-8"))["mutanten"][0]["tests"][0].get("ausgabe", "")
|
||||
except (OSError, ValueError, KeyError, IndexError):
|
||||
ausgabe_bericht = ""
|
||||
ok = (
|
||||
```
|
||||
|
||||
### `mutanten_lauf_treiber.py`, Stelle 5 von 9
|
||||
|
||||
Aus:
|
||||
|
||||
```python
|
||||
and "--gibt-es-nicht" in mutantenzeile[0]
|
||||
)
|
||||
```
|
||||
|
||||
wird:
|
||||
|
||||
```python
|
||||
and "--gibt-es-nicht" in mutantenzeile[0]
|
||||
and "Ende der Ausgabe:" in ausgabe_bericht
|
||||
)
|
||||
```
|
||||
|
||||
### `mutanten_lauf_treiber.py`, Stelle 6 von 9
|
||||
|
||||
Aus:
|
||||
|
||||
```python
|
||||
ausgabe = ergebnis.stdout + ergebnis.stderr
|
||||
ok = (
|
||||
```
|
||||
|
||||
wird:
|
||||
|
||||
```python
|
||||
ausgabe = ergebnis.stdout + ergebnis.stderr
|
||||
kopfzeilen = [z for z in ergebnis.stdout.split("\n") if z.startswith("pytest-konfiguration (Grundlinie, ")]
|
||||
ok = (
|
||||
```
|
||||
|
||||
### `mutanten_lauf_treiber.py`, Stelle 7 von 9
|
||||
|
||||
Aus:
|
||||
|
||||
```python
|
||||
and "test_f1_positiv ist" not in ausgabe
|
||||
)
|
||||
```
|
||||
|
||||
wird:
|
||||
|
||||
```python
|
||||
and "test_f1_positiv ist" not in ausgabe
|
||||
and len(kopfzeilen) == 2
|
||||
and sum(1 for z in kopfzeilen if z.startswith("pytest-konfiguration (Grundlinie, {0}): ".format(INTERPRETER))) == 1
|
||||
and sum(1 for z in kopfzeilen if z.startswith("pytest-konfiguration (Grundlinie, {0}): ".format(zweit_python))) == 1
|
||||
)
|
||||
```
|
||||
|
||||
### `mutanten_lauf_treiber.py`, Stelle 8 von 9
|
||||
|
||||
Aus:
|
||||
|
||||
```python
|
||||
|
||||
FAELLE = [
|
||||
```
|
||||
|
||||
wird:
|
||||
|
||||
```python
|
||||
|
||||
def fall_46(basis, projektpfad):
|
||||
rahmen, wurzel, dateien = baue_rahmen(eigene_konfig=True, fremde_konfig=False, fremdes_conftest=False)
|
||||
try:
|
||||
(wurzel / "conftest.py").write_text("X = 1\n", encoding="utf-8")
|
||||
dateien = dateien + [wurzel / "conftest.py"]
|
||||
snap = snapshot(dateien)
|
||||
toml = rahmen / "mutanten.toml"
|
||||
schreibe_toml(toml, kopf() + (
|
||||
"[[mutant]]\n"
|
||||
"id = \"M46\"\n"
|
||||
"datei = \"conftest.py\"\n"
|
||||
"aus = '''\n"
|
||||
"X = 1\n"
|
||||
"'''\n"
|
||||
"wird = '''\n"
|
||||
"def pytest_collection_modifyitems(items):\n"
|
||||
" raise RuntimeError(\"intern Fall 46\")\n"
|
||||
"'''\n"
|
||||
"rot_bei = [\"tests/test_modul.py::test_f1_positiv\"]\n"
|
||||
))
|
||||
bericht = rahmen / "bericht46.json"
|
||||
ergebnis = starte_laeufer(wurzel, toml, bericht_pfad=bericht)
|
||||
mutantenzeile = [z for z in ergebnis.stdout.split("\n") if z.startswith("M46 ")]
|
||||
try:
|
||||
ausgabe_bericht = json.loads(bericht.read_text(encoding="utf-8"))["mutanten"][0]["tests"][0].get("ausgabe", "")
|
||||
except (OSError, ValueError, KeyError, IndexError):
|
||||
ausgabe_bericht = ""
|
||||
ok = (
|
||||
ergebnis.returncode == 1
|
||||
and len(mutantenzeile) == 1
|
||||
and "M46 UNKLAR" in mutantenzeile[0]
|
||||
and "pytest-Abbruch mit internem Fehler (Rueckgabewert 3): " in mutantenzeile[0]
|
||||
and "intern Fall 46" in mutantenzeile[0]
|
||||
and "Ende der Ausgabe:" in ausgabe_bericht
|
||||
)
|
||||
melde("Fall 46", ok, "exit={0}\n{1}{2}".format(ergebnis.returncode, ergebnis.stdout, ergebnis.stderr))
|
||||
pruefe_unveraendert(dateien, snap, "Fall 46")
|
||||
finally:
|
||||
shutil.rmtree(rahmen, ignore_errors=True)
|
||||
|
||||
|
||||
FAELLE = [
|
||||
```
|
||||
|
||||
### `mutanten_lauf_treiber.py`, Stelle 9 von 9
|
||||
|
||||
Aus:
|
||||
|
||||
```python
|
||||
fall_31, fall_32, fall_33, fall_34, fall_35, fall_36, fall_37, fall_38, fall_39, fall_40,
|
||||
fall_41, fall_42, fall_43, fall_44, fall_45,
|
||||
]
|
||||
```
|
||||
|
||||
wird:
|
||||
|
||||
```python
|
||||
fall_31, fall_32, fall_33, fall_34, fall_35, fall_36, fall_37, fall_38, fall_39, fall_40,
|
||||
fall_41, fall_42, fall_43, fall_44, fall_45, fall_46,
|
||||
]
|
||||
```
|
||||
|
||||
### `mutanten_lauf_probe_konfig.py`, Stelle 1 von 3
|
||||
|
||||
Aus:
|
||||
|
||||
```python
|
||||
prueft Treiber-Fall 41.
|
||||
|
||||
```
|
||||
|
||||
wird:
|
||||
|
||||
```python
|
||||
prueft Treiber-Fall 41.
|
||||
|
||||
Die Probe prueft ihre eigene Voraussetzung mit: Ohne -c im sauberen Rahmen
|
||||
muss pytest mit 0 enden (ausser bei den Aufbauten knoten_fehl*), und wo der
|
||||
Aufbau ein conftest.py hat, muss eines geladen sein. Sonst endeten beide
|
||||
Laeufe gleich kaputt, und "gleich" hiesse nichts.
|
||||
|
||||
```
|
||||
|
||||
### `mutanten_lauf_probe_konfig.py`, Stelle 2 von 3
|
||||
|
||||
Aus:
|
||||
|
||||
```python
|
||||
"tox_abschnitt_mit_kommentar": ({"tox.ini": "[pytest] # k\nmarkers = x\n", "tests/test_a.py": TEST}, ["tests/test_a.py::test_a"]),
|
||||
"setup_cfg_tool_pytest": ({"setup.cfg": "[tool:pytest]\n", "tests/test_a.py": TEST}, ["tests/test_a.py::test_a"]),
|
||||
```
|
||||
|
||||
wird:
|
||||
|
||||
```python
|
||||
"tox_abschnitt_mit_kommentar": ({"tox.ini": "[pytest] # k\nmarkers = x\n", "tests/test_a.py": TEST}, ["tests/test_a.py::test_a"]),
|
||||
"tox_abschnitt_mit_semikolon": ({"tox.ini": "[pytest] ; k\nmarkers = x\n", "tests/test_a.py": TEST}, ["tests/test_a.py::test_a"]),
|
||||
"tox_seitenvorschub_vor_abschnitt": ({"tox.ini": "[tox]" + chr(12) + "[pytest]\n", "tests/test_a.py": TEST}, ["tests/test_a.py::test_a"]),
|
||||
"setup_cfg_tool_pytest": ({"setup.cfg": "[tool:pytest]\n", "tests/test_a.py": TEST}, ["tests/test_a.py::test_a"]),
|
||||
```
|
||||
|
||||
### `mutanten_lauf_probe_konfig.py`, Stelle 3 von 3
|
||||
|
||||
Aus:
|
||||
|
||||
```python
|
||||
gleich_lauf = ohne_c == mit_c
|
||||
if not (gleich_suche and gleich_lauf):
|
||||
abweichungen += 1
|
||||
print("{0:34s} suche={1} lauf={2}".format(
|
||||
name, "gleich" if gleich_suche else "ABWEICHUNG", "gleich" if gleich_lauf else "ABWEICHUNG"))
|
||||
if not gleich_suche:
|
||||
```
|
||||
|
||||
wird:
|
||||
|
||||
```python
|
||||
gleich_lauf = ohne_c == mit_c
|
||||
voraussetzung = name.startswith("knoten_fehl") or ohne_c[0] == 0
|
||||
if any(rel.endswith("conftest.py") for rel in dateien) and not ohne_c[2]:
|
||||
voraussetzung = False
|
||||
if not (gleich_suche and gleich_lauf and voraussetzung):
|
||||
abweichungen += 1
|
||||
print("{0:34s} suche={1} lauf={2} voraussetzung={3}".format(
|
||||
name, "gleich" if gleich_suche else "ABWEICHUNG", "gleich" if gleich_lauf else "ABWEICHUNG",
|
||||
"ok" if voraussetzung else "VERLETZT"))
|
||||
if not gleich_suche:
|
||||
```
|
||||
|
||||
|
||||
### Erwarteter Fehlschlag vor der Umsetzung
|
||||
|
||||
In der Git-Bash:
|
||||
|
||||
```bash
|
||||
# Laeufer im jetzigen Stand, neuer Treiber und neue Probe in einen leeren Rot-Ordner
|
||||
R=/c/Users/PASCAL~1/AppData/Local/Temp/mlauf-rot3
|
||||
rm -rf "$R" && mkdir -p "$R"
|
||||
cp /d/Claude/.vault-tools/plan-kopie/mutanten_lauf.py /d/Claude/.vault-tools/plan-kopie/mutanten_lauf_treiber.py /d/Claude/.vault-tools/plan-kopie/mutanten_lauf_probe_konfig.py "$R/"
|
||||
```
|
||||
|
||||
In der PowerShell:
|
||||
|
||||
```powershell
|
||||
# Treiber in WSL gegen den Laeufer vor Nachbesserung 2
|
||||
wsl -d Debian -- timeout 1500 /home/claude/venvs/trading/bin/python /mnt/c/Users/PASCAL~1/AppData/Local/Temp/mlauf-rot3/mutanten_lauf_treiber.py *> "C:\Users\PASCAL~1\AppData\Local\Temp\mlauf-rot3\ausgabe.txt"
|
||||
"exit=$LASTEXITCODE"
|
||||
Select-String -Path "C:\Users\PASCAL~1\AppData\Local\Temp\mlauf-rot3\ausgabe.txt" -Pattern "FEHLER:|gerechnete"
|
||||
```
|
||||
|
||||
Erwartet: `exit=1`, und die Treffer enden genau auf diese vier Zeilen:
|
||||
|
||||
```
|
||||
Fall 40 FEHLER: exit=1
|
||||
Fall 43 FEHLER: exit=1
|
||||
Fall 46 FEHLER: exit=1
|
||||
gerechnete Fehler: 3
|
||||
```
|
||||
|
||||
## Schritt 2 — Umsetzung
|
||||
|
||||
### `mutanten_lauf.py`, Stelle 1 von 7
|
||||
|
||||
Aus:
|
||||
|
||||
```python
|
||||
Konfiguration in der Wurzel ist config.inipath die leere Datei, ini-Werte vom
|
||||
Typ paths aus -o oder PYTEST_ADDOPTS gelten dann relativ zum Plugin-Ordner.
|
||||
|
||||
```
|
||||
|
||||
wird:
|
||||
|
||||
```python
|
||||
Konfiguration in der Wurzel ist config.inipath die leere Datei, ini-Werte vom
|
||||
Typ paths aus -o oder PYTEST_ADDOPTS gelten dann relativ zum Plugin-Ordner;
|
||||
iniconfig trennt Zeilen mit splitlines, der Laeufer auch. Liegt rootdir unter
|
||||
der Wurzel (Konfiguration oder setup.py in einem Unterordner der Tests),
|
||||
bildet pytest die Knotennamen relativ dazu, und rot_bei passt nicht mehr: Die
|
||||
Grundlinie meldet FEHLT, die Kopfzeile nennt die Datei.
|
||||
|
||||
```
|
||||
|
||||
### `mutanten_lauf.py`, Stelle 2 von 7
|
||||
|
||||
Aus:
|
||||
|
||||
```python
|
||||
return True
|
||||
for zeile in text.split("\n"):
|
||||
zeile = zeile.rstrip()
|
||||
```
|
||||
|
||||
wird:
|
||||
|
||||
```python
|
||||
return True
|
||||
for zeile in text.splitlines():
|
||||
zeile = zeile.rstrip()
|
||||
```
|
||||
|
||||
### `mutanten_lauf.py`, Stelle 3 von 7
|
||||
|
||||
Aus:
|
||||
|
||||
```python
|
||||
return pfad.exists()
|
||||
except OSError:
|
||||
return False
|
||||
```
|
||||
|
||||
wird:
|
||||
|
||||
```python
|
||||
return pfad.exists()
|
||||
except (OSError, ValueError):
|
||||
return False
|
||||
```
|
||||
|
||||
### `mutanten_lauf.py`, Stelle 4 von 7
|
||||
|
||||
Aus:
|
||||
|
||||
```python
|
||||
"""Die Zeile, die die Ursache nennt: bei 4 die erste mit "error" ausser der
|
||||
Gebrauchszeile von argparse, bei 3 und ohne Treffer die letzte."""
|
||||
if rueckgabe == 4:
|
||||
```
|
||||
|
||||
wird:
|
||||
|
||||
```python
|
||||
"""Die Zeile, die die Ursache nennt: bei 4 die erste mit "error" ausser der
|
||||
Gebrauchszeile von argparse, bei 3 die letzte mit INTERNALERROR> (danach
|
||||
folgt noch die Summenzeile, Treiber-Fall 46), ohne Treffer die letzte."""
|
||||
if rueckgabe == 4:
|
||||
```
|
||||
|
||||
### `mutanten_lauf.py`, Stelle 5 von 7
|
||||
|
||||
Aus:
|
||||
|
||||
```python
|
||||
return zeile.strip()
|
||||
return ende[-1].strip() if ende else "(keine Ausgabe)"
|
||||
```
|
||||
|
||||
wird:
|
||||
|
||||
```python
|
||||
return zeile.strip()
|
||||
if rueckgabe == 3:
|
||||
interne = [zeile for zeile in ende if zeile.startswith("INTERNALERROR>")]
|
||||
if interne:
|
||||
return interne[-1].strip()
|
||||
return ende[-1].strip() if ende else "(keine Ausgabe)"
|
||||
```
|
||||
|
||||
### `mutanten_lauf.py`, Stelle 6 von 7
|
||||
|
||||
Aus:
|
||||
|
||||
```python
|
||||
ergebnis, meldung = ergebnis_bestimmen(knoten, reports, deselected)
|
||||
if ergebnis == "FEHLT":
|
||||
```
|
||||
|
||||
wird:
|
||||
|
||||
```python
|
||||
ergebnis, meldung = ergebnis_bestimmen(knoten, reports, deselected)
|
||||
abbruch = None
|
||||
if ergebnis == "FEHLT":
|
||||
```
|
||||
|
||||
### `mutanten_lauf.py`, Stelle 7 von 7
|
||||
|
||||
Aus:
|
||||
|
||||
```python
|
||||
luecke = True
|
||||
test_ergebnisse.append({"eintrag": eintrag, "knoten": knoten, "ergebnis": ergebnis, "meldung": meldung})
|
||||
|
||||
```
|
||||
|
||||
wird:
|
||||
|
||||
```python
|
||||
luecke = True
|
||||
eintrag_bericht = {"eintrag": eintrag, "knoten": knoten, "ergebnis": ergebnis, "meldung": meldung}
|
||||
if ergebnis == "FEHLT" and abbruch:
|
||||
eintrag_bericht["ausgabe"] = abbruch["lang"]
|
||||
test_ergebnisse.append(eintrag_bericht)
|
||||
|
||||
```
|
||||
|
||||
### `mutanten_lauf_ENTWURF.md`, Stelle 1 von 3
|
||||
|
||||
Aus:
|
||||
|
||||
```markdown
|
||||
- Die Kopfzeilen `pytest-konfiguration (Grundlinie, <python>): …` nennen je Interpreter, was für seinen Aufruf in der Grundlinie gilt.
|
||||
- Gegen pytest selbst geprüft wird das mit `mutanten_lauf_probe_konfig.py` (Treiber-Fall 43): 24 Aufbauten, je `pytest_konfig_bestimmen` gegen `determine_setup` und ein echtes `pytest --co` ohne `-c` gegen einen Lauf mit den Argumenten des Läufers unter fremder Konfiguration.
|
||||
- Benannte Lücken: Eine `tox.ini` oder ein `setup.cfg`, die iniconfig nicht lesen kann, übergeht der Läufer, pytest bricht dort mit 4 ab; eine nicht lesbare `pyproject.toml` oder `pytest.toml` gilt dagegen als Treffer, damit pytest den Fehler selbst meldet. Ein `setup.cfg` mit `[pytest]`, bei pytest ein Fehler, gilt hier als keine Konfiguration. Ohne Konfiguration in der Wurzel ist `config.inipath` die leere Datei; ini-Werte vom Typ `paths`, die über `-o` oder `PYTEST_ADDOPTS` kommen, gelten dann relativ zum Plugin-Ordner.
|
||||
|
||||
**Abbruch von pytest (seit 2026-10-06):** Endet ein pytest-Aufruf mit Rückgabewert 3 (`INTERNAL_ERROR`) oder 4 (`USAGE_ERROR`, auch bei einem Testknoten, den es nicht gibt), hält der Läufer die letzten 20 nicht leeren Zeilen von stdout und stderr fest. In der Grundlinie wird daraus je Interpreter ein Befund `Grundlinie: pytest-Bedienfehler (Rueckgabewert 4) mit <python>; Ende der Ausgabe: …` (bei 3: `pytest-Abbruch mit internem Fehler`) statt einer `FEHLT`-Zeile je Test; der Bericht führt die Tests mit `FEHLT` und der Meldung „pytest abgebrochen, siehe Befunde“. Unter einem Mutanten tragen die Tests ohne Bericht und ohne Sammelfehler eine kurze Fassung als Meldung, die Ursache vorn, damit sie in die 120 Zeichen der Mutantenzeile passt: `pytest-Bedienfehler (Rueckgabewert 4): <zeile>`. Die Zeile ist bei 4 die erste mit „error“ außer der Gebrauchszeile `ERROR: usage:` von argparse, bei 3 die letzte. Diese Tests machen den Mutanten wie bisher `UNKLAR`. Ein Sammelfehler bleibt `ROT`, auch wenn pytest dabei mit 4 endet: Ein Knoten in einer Datei, die sich nicht sammeln lässt, ergibt zusätzlich `found no collectors for …` (Treiber-Fall 11, am 2026-10-06 an einer ersten Fassung gesehen, die jeden Abbruch als `UNKLAR` zählte).
|
||||
|
||||
```
|
||||
|
||||
wird:
|
||||
|
||||
```markdown
|
||||
- Die Kopfzeilen `pytest-konfiguration (Grundlinie, <python>): …` nennen je Interpreter, was für seinen Aufruf in der Grundlinie gilt.
|
||||
- Gegen pytest selbst geprüft wird das mit `mutanten_lauf_probe_konfig.py` (Treiber-Fall 43): 26 Aufbauten, je `pytest_konfig_bestimmen` gegen `determine_setup` und ein echtes `pytest --co` ohne `-c` gegen einen Lauf mit den Argumenten des Läufers unter fremder Konfiguration. Die Probe sichert ihre Voraussetzung mit: Ohne `-c` endet pytest mit 0 (außer bei fehlenden Knoten), und wo der Aufbau ein `conftest.py` hat, ist eines geladen.
|
||||
- Benannte Lücken: Eine `tox.ini` oder ein `setup.cfg`, die iniconfig nicht lesen kann, übergeht der Läufer, pytest bricht dort mit 4 ab; eine nicht lesbare `pyproject.toml` oder `pytest.toml` gilt dagegen als Treffer, damit pytest den Fehler selbst meldet. Ein `setup.cfg` mit `[pytest]`, bei pytest ein Fehler, gilt hier als keine Konfiguration. Ohne Konfiguration in der Wurzel ist `config.inipath` die leere Datei; ini-Werte vom Typ `paths`, die über `-o` oder `PYTEST_ADDOPTS` kommen, gelten dann relativ zum Plugin-Ordner. Liegt `rootdir` unter der Wurzel (Konfiguration oder `setup.py` in einem Unterordner der Tests), bildet pytest die Knotennamen relativ dazu, und `rot_bei` passt nicht mehr: Die Grundlinie meldet `FEHLT`, die Kopfzeile nennt die gefundene Datei; heute betrifft das kein Projekt im Vault. Zeilen trennt der Läufer wie iniconfig mit `splitlines`, also auch an Seitenvorschub und Unicode-Zeilentrennern.
|
||||
|
||||
**Abbruch von pytest (seit 2026-10-06):** Endet ein pytest-Aufruf mit Rückgabewert 3 (`INTERNAL_ERROR`) oder 4 (`USAGE_ERROR`, auch bei einem Testknoten, den es nicht gibt), hält der Läufer die letzten 20 nicht leeren Zeilen von stdout und stderr fest. In der Grundlinie wird daraus je Interpreter ein Befund `Grundlinie: pytest-Bedienfehler (Rueckgabewert 4) mit <python>; Ende der Ausgabe: …` (bei 3: `pytest-Abbruch mit internem Fehler`) statt einer `FEHLT`-Zeile je Test; der Bericht führt die Tests mit `FEHLT` und der Meldung „pytest abgebrochen, siehe Befunde“. Unter einem Mutanten tragen die Tests ohne Bericht und ohne Sammelfehler eine kurze Fassung als Meldung, die Ursache vorn, damit sie in die 120 Zeichen der Mutantenzeile passt: `pytest-Bedienfehler (Rueckgabewert 4): <zeile>`. Die Zeile ist bei 4 die erste mit „error“ außer der Gebrauchszeile `ERROR: usage:` von argparse, bei 3 die letzte mit `INTERNALERROR>` (danach steht noch die Summenzeile `no tests ran …`, Treiber-Fall 46), ohne Treffer die letzte überhaupt. Die lange Fassung steht im Bericht beim Test unter `ausgabe`. Diese Tests machen den Mutanten wie bisher `UNKLAR`. Ein Sammelfehler bleibt `ROT`, auch wenn pytest dabei mit 4 endet: Ein Knoten in einer Datei, die sich nicht sammeln lässt, ergibt zusätzlich `found no collectors for …` (Treiber-Fall 11, am 2026-10-06 an einer ersten Fassung gesehen, die jeden Abbruch als `UNKLAR` zählte).
|
||||
|
||||
```
|
||||
|
||||
### `mutanten_lauf_ENTWURF.md`, Stelle 2 von 3
|
||||
|
||||
Aus:
|
||||
|
||||
```markdown
|
||||
|
||||
`--bericht` schreibt dasselbe als JSON: `{"wurzel", "mutanten_datei", "befunde": [...], "grundlinie": {knoten: {ergebnis, meldung}}, "mutanten": [{"id", "datei", "status", "hinweis", "treffer", "tests": [{"eintrag", "knoten", "ergebnis", "meldung"}]}], "zusammenfassung": {...}}`. Der Bericht wird auch bei einem Abbruch geschrieben, mit allem, was bis dahin feststand.
|
||||
|
||||
```
|
||||
|
||||
wird:
|
||||
|
||||
```markdown
|
||||
|
||||
`--bericht` schreibt dasselbe als JSON: `{"wurzel", "mutanten_datei", "befunde": [...], "grundlinie": {knoten: {ergebnis, meldung}}, "mutanten": [{"id", "datei", "status", "hinweis", "treffer", "tests": [{"eintrag", "knoten", "ergebnis", "meldung", "ausgabe" (nur bei einem Abbruch von pytest)}]}], "zusammenfassung": {...}}`. Der Bericht wird auch bei einem Abbruch geschrieben, mit allem, was bis dahin feststand.
|
||||
|
||||
```
|
||||
|
||||
### `mutanten_lauf_ENTWURF.md`, Stelle 3 von 3
|
||||
|
||||
Aus:
|
||||
|
||||
```markdown
|
||||
| 39 | Wurzel ohne eigene Konfiguration, darüber ein `conftest.py`, das beim Laden abbricht | Exit 0, zweimal `OK`; prüft `--confcutdir` |
|
||||
| 40 | ein Mutant schreibt in die eigene `pyproject.toml` `addopts = "--gibt-es-nicht"` | Exit 1; die Mutantenzeile selbst trägt `UNKLAR`, `pytest-Bedienfehler (Rueckgabewert 4)` und `--gibt-es-nicht` |
|
||||
| 41 | Wurzel über einen Symlink angegeben (`link/k` → `echt/k`), seit der Nachbesserung vom 2026-10-06 | Exit 0, zweimal `OK` |
|
||||
| 42 | ein `conftest.py` der Wurzel wirft in `pytest_collection_modifyitems` | Exit 2, `pytest-Abbruch mit internem Fehler (Rueckgabewert 3)` und der Text der Ausnahme, keine Zeile `ist FEHLT` |
|
||||
| 43 | `mutanten_lauf_probe_konfig.py`: 24 Aufbauten gegen `determine_setup` und gegen echte `pytest --co`-Läufe | Exit 0, `abweichungen: 0` |
|
||||
| 44 | zwei Interpretergruppen (`python_je_datei`), nur in einer ein Knoten, den es nicht gibt; diese Gruppe einmal an erster, einmal an letzter Stelle | je Exit 2, genau ein Befund mit dem Interpreter dieser Gruppe, keiner für die andere, keine Zeile `ist FEHLT` |
|
||||
| 45 | ein Knoten mit einem Dateinamen über 255 Zeichen | Exit 2 mit einem Befund `Grundlinie: pytest-…`, kein `unerwarteter Abbruch` |
|
||||
|
||||
```
|
||||
|
||||
wird:
|
||||
|
||||
```markdown
|
||||
| 39 | Wurzel ohne eigene Konfiguration, darüber ein `conftest.py`, das beim Laden abbricht | Exit 0, zweimal `OK`; prüft `--confcutdir` |
|
||||
| 40 | ein Mutant schreibt in die eigene `pyproject.toml` `addopts = "--gibt-es-nicht"` | Exit 1; die Mutantenzeile selbst trägt `UNKLAR`, `pytest-Bedienfehler (Rueckgabewert 4)` und `--gibt-es-nicht`; im Bericht steht beim Test `ausgabe` mit „Ende der Ausgabe:“ |
|
||||
| 41 | Wurzel über einen Symlink angegeben (`link/k` → `echt/k`), seit der Nachbesserung vom 2026-10-06 | Exit 0, zweimal `OK` |
|
||||
| 42 | ein `conftest.py` der Wurzel wirft in `pytest_collection_modifyitems` | Exit 2, `pytest-Abbruch mit internem Fehler (Rueckgabewert 3)` und der Text der Ausnahme, keine Zeile `ist FEHLT` |
|
||||
| 43 | `mutanten_lauf_probe_konfig.py`: 26 Aufbauten gegen `determine_setup` und gegen echte `pytest --co`-Läufe, mit Selbstprüfung der Voraussetzung | Exit 0, `abweichungen: 0` |
|
||||
| 44 | zwei Interpretergruppen (`python_je_datei`), nur in einer ein Knoten, den es nicht gibt; diese Gruppe einmal an erster, einmal an letzter Stelle | je Exit 2, genau ein Befund mit dem Interpreter dieser Gruppe, keiner für die andere, keine Zeile `ist FEHLT`; genau zwei Kopfzeilen, je eine je Interpreter |
|
||||
| 45 | ein Knoten mit einem Dateinamen über 255 Zeichen | Exit 2 mit einem Befund `Grundlinie: pytest-…`, kein `unerwarteter Abbruch` |
|
||||
| 46 | ein Mutant lässt ein `conftest.py` der Wurzel in `pytest_collection_modifyitems` werfen | Exit 1, die Mutantenzeile trägt `UNKLAR`, `pytest-Abbruch mit internem Fehler (Rueckgabewert 3): ` und den Text der Ausnahme; im Bericht `ausgabe` mit „Ende der Ausgabe:“ |
|
||||
|
||||
```
|
||||
|
||||
### `LIESMICH.md`, Stelle 1 von 2
|
||||
|
||||
Aus:
|
||||
|
||||
```markdown
|
||||
|
||||
8. **Mutanten fahren (seit 2026-09-27):** `mutanten_lauf.py --wurzel <wegwerfkopie> --mutanten <planordner>/mutanten.toml [--nur ID,…] [--nur-muster] [--bericht <datei.json>] [--zeitlimit <s>]`, in WSL mit einem Python ab 3.11. Die Mutanten stehen nicht mehr als Prosa im Plan, sondern in `mutanten.toml` im Planordner. Das Format, die Status und die Abdichtung stehen in `mutanten_lauf_ENTWURF.md`. Das Skript verweigert eine Wurzel mit `.git` und jeden Pfad aus `ohne`. Seit 2026-10-06 sucht es die pytest-Konfiguration wie pytest selbst, aber nie oberhalb der Wurzel, und gibt sie mit `-c` vor; eine fremde `pyproject.toml` im Windows-Temp stört damit nicht mehr, und ein Bedienfehler von pytest steht mit dem Ende der pytest-Ausgabe im Befund statt als `FEHLT` je Test (Entwurf, Abschnitt 3, „Konfiguration“ und „Abbruch von pytest“). Ob die Suche pytest selbst entspricht, prüft `mutanten_lauf_probe_konfig.py <mutanten_lauf.py> <arbeitsordner>` mit dem Python eines Venvs, in dem pytest liegt (24 Aufbauten, Exit 0 ohne Abweichung). Es fährt zuerst die Grundlinie, dann je Mutant nur die genannten Tests, setzt die Datei zurück und prüft das per SHA-256. Je Test gibt es die Fehlermeldung mit aus; ob ein Test an der gemeinten Stelle fällt, liest man dort. `--nur-muster` prüft nur, ob jedes Muster genau einmal dasteht, ohne pytest. Die Kopie legt man in WSL unter `/tmp` an (`cp -a`, danach `__pycache__` löschen). Auf `/mnt/d` läuft es auch, nur langsamer. **Aus der Git-Bash ein Skript nie mit `wsl … bash /mnt/d/…` starten:** Die Git-Bash macht aus dem Pfad einen Windows-Pfad. Aus der PowerShell starten oder `MSYS_NO_PATHCONV=1` davorsetzen. Ein SIGTERM auf `mutanten_lauf.py` selbst (etwa `timeout -k` oder ein Hilfsagent, der abgebrochen wird) löst dort einen eigenen Handler aus: Der laufende Mutant wird per SHA-Probe zurückgesetzt, die Temp-Ordner werden geräumt, und die Ausgabe trägt `ABBRUCH: SIGTERM` (Treiber-Fall 35, `mutanten_lauf_treiber.py`).
|
||||
**Die Plankritik fährt die Mutanten selbst (seit 2026-09-27, nachgebessert 2026-09-28):** Liegt `mutanten.toml` im Planordner, startet `plankritik.js` in der Vorbereitung `mutanten_lauf_kopie.sh`. Das Skript kopiert die Kopie mit angewendetem Plan nach `/tmp` in WSL, fährt dort den Läufer in einem eigenen Netz-Namensraum (`unshare --map-current-user --net`, gemessen in WSL Debian als Benutzer `claude`: UID innen bleibt 1000, nur Schnittstelle `lo`, jede Verbindung nach außen scheitert) und gibt die Ausgabe an die Linse `arithmetik`. Fehlt `unshare` ohne Netz, bricht das Skript vorher mit `MUTANTEN_LAUF_FEHLER` ab, statt ungesperrt zu laufen. Der Arg `mutanten` wählt `voll` (Vorgabe), `muster` oder `aus`; die Rückgabe trägt dazu `mutanten_status` (`aus`, `ohne_kopie`, `keine_toml`, `ohne_ergebnis` oder `exit_<n>`) und `mutanten_modus` (`voll`, `muster`, `aus`). Der Wrapper gibt vor dem Lauf eine Zeile `modus: voll` oder `modus: muster (…)` aus; bei `modus: muster` weiß die Linse, dass kein Mutant gefahren ist. Der ganze Lauf hat 540 s (`BUDGET_S`, `timeout -k 15`), weil das Bash-Werkzeug eines Hilfsagenten nach zehn Minuten endet; eine überschrittene Grenze meldet die Zeile `ZEITGRENZE`. Je Mutant gilt zusätzlich ein Zeitlimit `ZEITLIMIT_S` (Vorgabe 120 s, muss kleiner sein als `BUDGET_S`): B1 mit 74 Mutanten brauchte 168 s gesamt, rund 2 s je Mutant. Bei Exit 2 oder 3 des Läufers sagt die Linse, der Lauf sei vor oder während der Mutanten gescheitert. Pfade übergibt man mit Vorwärtsschrägstrichen: `wsl -d Debian -- …` reicht die Befehlszeile an die Linux-Shell, und die verschluckt Backslashes.
|
||||
```
|
||||
|
||||
wird:
|
||||
|
||||
```markdown
|
||||
|
||||
8. **Mutanten fahren (seit 2026-09-27):** `mutanten_lauf.py --wurzel <wegwerfkopie> --mutanten <planordner>/mutanten.toml [--nur ID,…] [--nur-muster] [--bericht <datei.json>] [--zeitlimit <s>]`, in WSL mit einem Python ab 3.11. Die Mutanten stehen nicht mehr als Prosa im Plan, sondern in `mutanten.toml` im Planordner. Das Format, die Status und die Abdichtung stehen in `mutanten_lauf_ENTWURF.md`. Das Skript verweigert eine Wurzel mit `.git` und jeden Pfad aus `ohne`. Seit 2026-10-06 sucht es die pytest-Konfiguration wie pytest selbst, aber nie oberhalb der Wurzel, und gibt sie mit `-c` vor; eine fremde `pyproject.toml` im Windows-Temp stört damit nicht mehr, und ein Bedienfehler von pytest steht mit dem Ende der pytest-Ausgabe im Befund statt als `FEHLT` je Test (Entwurf, Abschnitt 3, „Konfiguration“ und „Abbruch von pytest“). Ob die Suche pytest selbst entspricht, prüft `mutanten_lauf_probe_konfig.py <mutanten_lauf.py> <arbeitsordner>` mit dem Python eines Venvs, in dem pytest liegt (26 Aufbauten, Exit 0 ohne Abweichung). Es fährt zuerst die Grundlinie, dann je Mutant nur die genannten Tests, setzt die Datei zurück und prüft das per SHA-256. Je Test gibt es die Fehlermeldung mit aus; ob ein Test an der gemeinten Stelle fällt, liest man dort. `--nur-muster` prüft nur, ob jedes Muster genau einmal dasteht, ohne pytest. Die Kopie legt man in WSL unter `/tmp` an (`cp -a`, danach `__pycache__` löschen). Auf `/mnt/d` läuft es auch, nur langsamer. **Aus der Git-Bash ein Skript nie mit `wsl … bash /mnt/d/…` starten:** Die Git-Bash macht aus dem Pfad einen Windows-Pfad. Aus der PowerShell starten oder `MSYS_NO_PATHCONV=1` davorsetzen. Ein SIGTERM auf `mutanten_lauf.py` selbst (etwa `timeout -k` oder ein Hilfsagent, der abgebrochen wird) löst dort einen eigenen Handler aus: Der laufende Mutant wird per SHA-Probe zurückgesetzt, die Temp-Ordner werden geräumt, und die Ausgabe trägt `ABBRUCH: SIGTERM` (Treiber-Fall 35, `mutanten_lauf_treiber.py`).
|
||||
**Die Plankritik fährt die Mutanten selbst (seit 2026-09-27, nachgebessert 2026-09-28):** Liegt `mutanten.toml` im Planordner, startet `plankritik.js` in der Vorbereitung `mutanten_lauf_kopie.sh`. Das Skript kopiert die Kopie mit angewendetem Plan nach `/tmp` in WSL, fährt dort den Läufer in einem eigenen Netz-Namensraum (`unshare --map-current-user --net`, gemessen in WSL Debian als Benutzer `claude`: UID innen bleibt 1000, nur Schnittstelle `lo`, jede Verbindung nach außen scheitert) und gibt die Ausgabe an die Linse `arithmetik`. Fehlt `unshare` ohne Netz, bricht das Skript vorher mit `MUTANTEN_LAUF_FEHLER` ab, statt ungesperrt zu laufen. Der Arg `mutanten` wählt `voll` (Vorgabe), `muster` oder `aus`; die Rückgabe trägt dazu `mutanten_status` (`aus`, `ohne_kopie`, `keine_toml`, `ohne_ergebnis` oder `exit_<n>`) und `mutanten_modus` (`voll`, `muster`, `aus`). Der Wrapper gibt vor dem Lauf eine Zeile `modus: voll` oder `modus: muster (…)` aus; bei `modus: muster` weiß die Linse, dass kein Mutant gefahren ist. Der ganze Lauf hat 540 s (`BUDGET_S`, `timeout -k 15`), weil das Bash-Werkzeug eines Hilfsagenten nach zehn Minuten endet; eine überschrittene Grenze meldet die Zeile `ZEITGRENZE`. Je Mutant gilt zusätzlich ein Zeitlimit `ZEITLIMIT_S` (Vorgabe 120 s, muss kleiner sein als `BUDGET_S`): B1 mit 74 Mutanten brauchte 168 s gesamt, rund 2 s je Mutant. Bei Exit 2 oder 3 des Läufers sagt die Linse, der Lauf sei vor oder während der Mutanten gescheitert. Pfade übergibt man mit Vorwärtsschrägstrichen: `wsl -d Debian -- …` reicht die Befehlszeile an die Linux-Shell, und die verschluckt Backslashes.
|
||||
```
|
||||
|
||||
### `LIESMICH.md`, Stelle 2 von 2
|
||||
|
||||
Aus:
|
||||
|
||||
```markdown
|
||||
- Gegenprobe an allen sechs `mutanten.toml` im Vault, alter gegen neuer Läufer auf Kopien aus `git archive HEAD` unter `/tmp`: Headscale A, Mattermost, After Install C1 und D-40 gleich (175, 130, 69 und 41 Mutanten `OK`); Headscale B und Trading Plattform WH enden bei beiden mit Exit 2, weil ihr Stand nicht zum Plan passt. Unter dem Windows-Temp endete C1 mit dem alten Läufer mit 101 × `FEHLT`, mit dem neuen mit 69 `OK`.
|
||||
- Ein `pruefer` fand in der ersten Fassung den Symlink-Fall und die fehlenden Fälle; beides ist in der Nachbesserung behoben.
|
||||
- Rohdaten: `AI/secondbrain/linux-haertung-after-install-memory/2026-10-06-c1-abschluss/roh/` (`02-*`, `03-gegenprobe/`, `07-*`).
|
||||
|
||||
```
|
||||
|
||||
wird:
|
||||
|
||||
```markdown
|
||||
- Gegenprobe an allen sechs `mutanten.toml` im Vault, alter gegen neuer Läufer auf Kopien aus `git archive HEAD` unter `/tmp`: Headscale A, Mattermost, After Install C1 und D-40 gleich (175, 130, 69 und 41 Mutanten `OK`); Headscale B und Trading Plattform WH enden bei beiden mit Exit 2, weil ihr Stand nicht zum Plan passt. Unter dem Windows-Temp endete C1 mit dem alten Läufer mit 101 × `FEHLT`, mit dem neuen mit 69 `OK`.
|
||||
- Ein `pruefer` fand in der ersten Fassung den Symlink-Fall und fehlende Fälle; die Nachbesserung behob beides. Ein zweiter `pruefer` fand danach keinen Verhaltensfehler mehr, nur eine Aufräumliste (Kopfzeile je Interpreter ohne eigenen Fall, Kurzfassung bei Rückgabewert 3, `;` und Seitenvorschub in `tox.ini`, Selbstprüfung der Probe, lange Fassung im Bericht, zwei Lücken benennen); sie ist in Nachbesserung 2 erledigt, Treiber 46 von 46.
|
||||
- Rohdaten: `AI/secondbrain/linux-haertung-after-install-memory/2026-10-06-c1-abschluss/roh/` (`02-*`, `03-gegenprobe/`, `07-*`, `08-gegenprobe2/` mit dem nachgebesserten Läufer, `09-*` für Nachbesserung 2).
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Schritt 3 — Verifikation
|
||||
|
||||
```bash
|
||||
# Kompilieren, ASCII, keine CR, Cache wieder weg
|
||||
cd /d/Claude/.vault-tools/plan-kopie
|
||||
python -m py_compile mutanten_lauf.py mutanten_lauf_treiber.py mutanten_lauf_probe_konfig.py && echo kompiliert
|
||||
rm -rf __pycache__
|
||||
LC_ALL=C grep -cP '[^\x00-\x7F]' mutanten_lauf.py mutanten_lauf_treiber.py mutanten_lauf_probe_konfig.py
|
||||
for f in mutanten_lauf.py mutanten_lauf_treiber.py mutanten_lauf_probe_konfig.py mutanten_lauf_ENTWURF.md LIESMICH.md; do printf '%s %s\n' "$f" "$(tr -cd '\r' < "$f" | wc -c)"; done
|
||||
```
|
||||
|
||||
Erwartet: `kompiliert`, überall `0`.
|
||||
|
||||
```powershell
|
||||
# Treiber gegen den Laeufer nach Nachbesserung 2
|
||||
wsl -d Debian -- timeout 1500 /home/claude/venvs/trading/bin/python /mnt/d/Claude/.vault-tools/plan-kopie/mutanten_lauf_treiber.py *> "C:\Users\PASCAL~1\AppData\Local\Temp\mlauf-rot3\ausgabe-neu.txt"
|
||||
"exit=$LASTEXITCODE"
|
||||
Select-String -Path "C:\Users\PASCAL~1\AppData\Local\Temp\mlauf-rot3\ausgabe-neu.txt" -Pattern "FEHLER:|gerechnete"
|
||||
```
|
||||
|
||||
Erwartet: `exit=0` und als einziger Treffer eine Zeile, die auf `gerechnete Fehler: 0` endet. Danach den Rot-Ordner und ein von WSL angelegtes `__pycache__` unter `plan-kopie/` löschen.
|
||||
|
||||
## Bericht
|
||||
|
||||
Je Zieldatei die Zahl der angewendeten Paare, die Ausgaben aus Schritt 1 und 3 im Wortlaut, jede Abweichung und Annahme.
|
||||
|
||||
## Fertig heißt (Koordinator)
|
||||
|
||||
1. Die fünf Zieldateien byte-gleich mit der Kopie mit angewendetem Plan (`cmp`).
|
||||
2. Treiber 46 von 46, selbst gefahren.
|
||||
3. Rot-Nachweis wie unter „Gemessen“.
|
||||
File diff suppressed because it is too large.
Load diff
+693
@@ -0,0 +1,693 @@
|
||||
---
|
||||
type: plan
|
||||
title: "Mutantenlauf auf der Kopie: Nachbesserung 1 (Fälle für die Fehlerzweige der lo-Prüfung)"
|
||||
status: active
|
||||
summary: "Delta zum Plan vom 2026-10-09: Treiber-Fälle W10 bis W13 (Probe scheitert an ip, lo bleibt aus, ip scheitert erst im Hauptaufruf, setpriv wirft nichts ab), Varianten v6 bis v9 als Regressionsnachweis, Kopfkommentar des Skripts berichtigt, Satz zur Reihenfolge vor KEINE_MUTANTEN_TOML in LIESMICH und Entwurf, Datensatz rb3d-kopie im Plan."
|
||||
tags: [plan, vault-tools, pruefwerkzeuge]
|
||||
created: 2026-10-09
|
||||
updated: 2026-10-09
|
||||
modell: haiku
|
||||
modell_begruendung: "Jeder Block steht wörtlich da, Zieldateien, Verbote und Haltepunkte sind genannt; kein Codeblock trägt typografische Anführungszeichen, Striche oder Auslassungspunkte."
|
||||
---
|
||||
|
||||
Übersicht: [LIESMICH der Planwerkzeuge](../../LIESMICH.md) · Vorgänger: [Plan lo im Netz-Namensraum](2026-10-09-mutanten-kopie-loopback.md)
|
||||
|
||||
# Mutantenlauf auf der Kopie: Nachbesserung 1
|
||||
|
||||
## Ziel
|
||||
|
||||
Der Treiber `mutanten_lauf_kopie_treiber.py` sieht danach auch die Fehlerzweige der lo-Prüfung: eine gescheiterte Probe (W10, W11), die Fähigkeitsprüfung der Probe (W13) und den Zweig nach `||` im Hauptaufruf (W12). Jeder dieser Fälle ist an einer kaputten Variante rot gesehen. Kopfkommentar, LIESMICH und Entwurf sagen genau, was die Vorprüfung prüft und dass sie vor `KEINE_MUTANTEN_TOML` steht. Der Plan nennt für den Lauf mit echten Daten den richtigen Datensatz.
|
||||
|
||||
## Nicht im Umfang
|
||||
|
||||
- Das Verhalten von `mutanten_lauf_kopie.sh` bleibt gleich. Im Skript ändert sich nur der Kopfkommentar; Schritt 2 prüft das.
|
||||
- Die Reihenfolge der Vorprüfung vor `KEINE_MUTANTEN_TOML` bleibt (Befund 3), sie wird nur beschrieben.
|
||||
- Kein neuer Lauf gegen das Skript aus HEAD, kein neuer Lauf der Varianten v1 bis v5, kein neuer Lauf mit echten Daten (siehe Getroffene Annahmen).
|
||||
- `mutanten_lauf.py`, `mutanten_lauf_treiber.py`, `.claude/workflows/*` und alles unter `D:/Claude/.arbeitsbaeume/` bleiben unberührt.
|
||||
|
||||
## Warum so
|
||||
|
||||
Der `pruefer` fand am 2026-10-09 keinen blockierenden Befund. Treiber W1 bis W9 grün, die 73 Mutanten von RB3 durch das neue Skript alle `OK`, T1 und T12 im Namensraum grün. Rohdaten: `D:/Claude/AI/secondbrain/trading-plattform-memory/2026-10-09-mkopie-roh/pruefer/` (`angriffe.py`, `angriffe.out`, `varianten_pruefer.py`).
|
||||
|
||||
| Befund | Hier |
|
||||
|---|---|
|
||||
| 1 (wichtig): kein Fall für `exit 2` nach der Probe und keiner für den Zweig nach `\|\|` im Hauptaufruf; v6 lief ganz grün | W10 (A1), W11 (A3), W12 (A4), dazu W13 für die Fähigkeitsprüfung der Probe ohne Variante; Varianten v6 und v7 des Prüfers, dazu v8 und v9 |
|
||||
| 2 (klein): Kopfkommentar Z. 42 bis 43 zu weit | Kopfkommentar trennt Probe und Hauptaufruf, nennt CapEff und CapAmb und nur die Probe |
|
||||
| 3 (klein): Vorprüfung vor `KEINE_MUTANTEN_TOML` | bleibt; Satz in LIESMICH, Entwurf und Kopfkommentar |
|
||||
| 4 (klein): Datensatz `rb3d-kopie` statt `rb3b-kopie` | drei Ersetzungen im Plan, Abschnitt „Lauf mit echten Daten“ |
|
||||
|
||||
### Bauweise der neuen Fälle
|
||||
|
||||
Aus `angriffe.py` des Prüfers übernommen: Der `PATH` des Skripts ist ein Ordner mit Verweisen auf alle Werkzeuge aus `WERKZEUGE`. Einzelne Werkzeuge sind dort durch kleine Skripte mit `#!/bin/sh` ersetzt. W4, W5 und W9 lassen ein Werkzeug weg, W10 bis W13 ersetzen eines.
|
||||
|
||||
| Fall | Ersatz | Erwartung | Gemessen oder gerechnet |
|
||||
|---|---|---|---|
|
||||
| W10 | `ip`: Meldung auf stderr, Exit 1 | Exit 2, `MUTANTEN_LAUF_FEHLER: lo im Netz-Namensraum nicht nutzbar`, Text der Attrappe in stdout, keine Modus-Zeile, keine `EXIT`-Zeile, keine Kopie | gemessen als A1 |
|
||||
| W11 | `ip`: Exit 0 ohne Wirkung | dasselbe, mit `Errno 101` in stdout | gemessen als A3 |
|
||||
| W12 | `ip`: zählt seine Aufrufe in einer Datei, ab dem zweiten Exit 1, sonst `exec` des echten `ip` | Exit 2, `modus: voll`, `MUTANTEN_LAUF_FEHLER: lo im Netz-Namensraum nicht hochgefahren`, `EXIT mutanten_lauf: 2`, kein `MW7`, Text der Attrappe in stderr, genau zwei Aufrufe, keine Kopie | gemessen als A4, ohne die Zählung |
|
||||
| W13 | `setpriv`: überspringt alles bis `--` und startet den Rest per `exec`, ohne etwas abzuwerfen | wie W10, mit `Faehigkeiten nicht abgeworfen` in stdout | gerechnet |
|
||||
|
||||
W13 rechnet so: `unshare --keep-caps` hebt die Fähigkeiten in die ambiente Menge. Sonst käme `ip` nach dem `exec` der `sh` nicht an `CAP_NET_ADMIN`. Ohne `setpriv` behält `python3` sie deshalb, wie bei Variante v1 des Plans (W3 rot, gemessen).
|
||||
|
||||
### Varianten
|
||||
|
||||
| Variante | Änderung | rot erwartet | gemessen oder gerechnet |
|
||||
|---|---|---|---|
|
||||
| `v6_probe_ohne_exit` | Zeile `exit 2` nach der Meldung der Probe gestrichen (v6 des Prüfers) | W10, W11, W13 | W1 bis W9 grün gemessen (Prüfer), W10 rot gemessen (A6), W11 und W13 gerechnet |
|
||||
| `v7_probe_ohne_setpriv` | Probe ohne `setpriv` (v7 des Prüfers) | W1 bis W9, W12 | gerechnet |
|
||||
| `v8_haupt_ohne_oder` | Hauptaufruf ohne den Zweig nach `\|\|` | W12 | gerechnet |
|
||||
| `v9_probe_ohne_faehigkeiten` | Probe ohne Fähigkeitsprüfung (`if False:`) | W13 | gerechnet |
|
||||
|
||||
Damit ist jeder neue Fall einmal rot: W10 und W11 an v6, W12 an v7 und v8, W13 an v6 und v9.
|
||||
|
||||
## Zieldateien
|
||||
|
||||
Pfade relativ zu `D:/Claude/.vault-tools/plan-kopie/`:
|
||||
|
||||
- `mutanten_lauf_kopie_treiber.py` (geändert)
|
||||
- `mutanten_lauf_kopie.sh` (geändert)
|
||||
- `LIESMICH.md` (geändert)
|
||||
- `mutanten_lauf_ENTWURF.md` (geändert)
|
||||
- `docs/plans/2026-10-09-mutanten-kopie-loopback.md` (geändert)
|
||||
|
||||
## Verbote
|
||||
|
||||
- Nur die fünf Zieldateien ändern. Kein `git add`, kein `git commit`.
|
||||
- Hilfsdateien nur im Rot-Ordner `C:\Users\PASCAL~1\AppData\Local\Temp\mkopie-rot-n1` (Git-Bash: `/c/Users/PASCAL~1/AppData/Local/Temp/mkopie-rot-n1`, WSL: `/mnt/c/Users/PASCAL~1/AppData/Local/Temp/mkopie-rot-n1`).
|
||||
- Jeden Block byte-genau übernehmen: keine Umformulierung, keine Ersetzung von Zeichen, keine zusätzlichen Leerzeilen. Aus-Block als `old_string`, wird-Block als `new_string` im Edit-Werkzeug; die Variantendatei mit dem Write-Werkzeug.
|
||||
- Kein `sed`, kein Suchen-und-Ersetzen über ganze Dateien.
|
||||
- WSL-Aufrufe nur aus der PowerShell.
|
||||
- Kein Netz außer `127.0.0.1`.
|
||||
- Den Treiber nicht fahren, während eine Plankritik läuft.
|
||||
- Steht ein Aus-Block nicht genau einmal in seiner Datei oder weicht eine Pflicht-Ausgabe ab: anhalten und melden. Den Treiber nie anpassen, damit ein Fall grün wird.
|
||||
- `python -m py_compile` legt `__pycache__` an; den Ordner danach löschen.
|
||||
|
||||
## Schritt 0 — Rot-Ordner und Ausgangsstand
|
||||
|
||||
In der Git-Bash:
|
||||
|
||||
```bash
|
||||
# Rot-Ordner neu anlegen, Skript im jetzigen Stand als Vergleichsdatei hineinlegen
|
||||
R=/c/Users/PASCAL~1/AppData/Local/Temp/mkopie-rot-n1
|
||||
rm -rf "$R" && mkdir -p "$R"
|
||||
cp /d/Claude/.vault-tools/plan-kopie/mutanten_lauf_kopie.sh "$R/skript_vor_n1.sh"
|
||||
# Ausgangsstand: Plan umgesetzt, nicht committet
|
||||
git -C /d/Claude status --porcelain -- .vault-tools
|
||||
```
|
||||
|
||||
Erwartet: genau diese sechs Zeilen, in beliebiger Reihenfolge. Sonst anhalten und melden.
|
||||
|
||||
```text
|
||||
M .vault-tools/plan-kopie/LIESMICH.md
|
||||
M .vault-tools/plan-kopie/mutanten_lauf_ENTWURF.md
|
||||
M .vault-tools/plan-kopie/mutanten_lauf_kopie.sh
|
||||
?? .vault-tools/plan-kopie/docs/plans/2026-10-09-mutanten-kopie-loopback-nachbesserung-1.md
|
||||
?? .vault-tools/plan-kopie/docs/plans/2026-10-09-mutanten-kopie-loopback.md
|
||||
?? .vault-tools/plan-kopie/mutanten_lauf_kopie_treiber.py
|
||||
```
|
||||
|
||||
## Schritt 1 — Tests zuerst: Treiber
|
||||
|
||||
### `mutanten_lauf_kopie_treiber.py`, Stelle 1 von 7
|
||||
|
||||
Aus:
|
||||
|
||||
```python
|
||||
#!/usr/bin/env python3
|
||||
"""Treiber fuer mutanten_lauf_kopie.sh: Faelle W1 bis W9 (lo im Netz-Namensraum).
|
||||
```
|
||||
|
||||
wird:
|
||||
|
||||
```python
|
||||
#!/usr/bin/env python3
|
||||
"""Treiber fuer mutanten_lauf_kopie.sh: Faelle W1 bis W13 (lo im Netz-Namensraum).
|
||||
```
|
||||
|
||||
### `mutanten_lauf_kopie_treiber.py`, Stelle 2 von 7
|
||||
|
||||
Aus:
|
||||
|
||||
```python
|
||||
Nicht im Treiber: ob plankritik.js das Skript so aufruft wie hier
|
||||
(MSYS_NO_PATHCONV, cygpath -m -l).
|
||||
"""
|
||||
```
|
||||
|
||||
wird:
|
||||
|
||||
```python
|
||||
Faelle W10 bis W13 (Nachbesserung 1, Plan
|
||||
docs/plans/2026-10-09-mutanten-kopie-loopback-nachbesserung-1.md) ersetzen im
|
||||
Attrappen-Ordner einzelne Werkzeuge durch kleine sh-Skripte, nach den
|
||||
Angriffen A1, A3 und A4 des Pruefers vom 2026-10-09: ip scheitert in der
|
||||
Probe (W10), ip meldet Erfolg und lo bleibt aus (W11), ip scheitert erst im
|
||||
Hauptaufruf (W12), setpriv wirft keine Faehigkeit ab (W13).
|
||||
|
||||
Nicht im Treiber: ob plankritik.js das Skript so aufruft wie hier
|
||||
(MSYS_NO_PATHCONV, cygpath -m -l); ob die Probe bei Exit 0 mit falscher
|
||||
Ausgabe scheitert.
|
||||
"""
|
||||
```
|
||||
|
||||
### `mutanten_lauf_kopie_treiber.py`, Stelle 3 von 7
|
||||
|
||||
Aus:
|
||||
|
||||
```python
|
||||
import os
|
||||
import shutil
|
||||
```
|
||||
|
||||
wird:
|
||||
|
||||
```python
|
||||
import os
|
||||
import shlex
|
||||
import shutil
|
||||
```
|
||||
|
||||
### `mutanten_lauf_kopie_treiber.py`, Stelle 4 von 7
|
||||
|
||||
Aus:
|
||||
|
||||
```python
|
||||
# Was mutanten_lauf_kopie.sh ueber PATH aufruft (Stand 2026-10-09); daraus
|
||||
# bauen W4, W5 und W9 ihre Attrappen-Ordner.
|
||||
```
|
||||
|
||||
wird:
|
||||
|
||||
```python
|
||||
# Was mutanten_lauf_kopie.sh ueber PATH aufruft (Stand 2026-10-09); daraus
|
||||
# bauen W4, W5, W9 und W10 bis W13 ihre Attrappen-Ordner.
|
||||
```
|
||||
|
||||
### `mutanten_lauf_kopie_treiber.py`, Stelle 5 von 7
|
||||
|
||||
Aus:
|
||||
|
||||
```python
|
||||
def fall_w9(skript):
|
||||
fahre_ohne_werkzeug("W9", skript, "python3")
|
||||
|
||||
|
||||
def prozesse_mit_marke(marke):
|
||||
```
|
||||
|
||||
wird:
|
||||
|
||||
```python
|
||||
def fall_w9(skript):
|
||||
fahre_ohne_werkzeug("W9", skript, "python3")
|
||||
|
||||
|
||||
def baue_attrappen_mit_ersatz(ordner, ersatz):
|
||||
# Wie baue_attrappen, aber jedes Werkzeug aus ersatz ist ein kleines
|
||||
# sh-Skript statt eines Verweises auf das echte Werkzeug (Bauweise aus den
|
||||
# Angriffen des Pruefers vom 2026-10-09).
|
||||
ordner.mkdir()
|
||||
for name in WERKZEUGE:
|
||||
pfad = ordner / name
|
||||
if name in ersatz:
|
||||
pfad.write_text("#!/bin/sh\n" + ersatz[name] + "\n", encoding="utf-8")
|
||||
pfad.chmod(0o755)
|
||||
else:
|
||||
os.symlink(shutil.which(name), pfad)
|
||||
|
||||
|
||||
def fahre_probe_scheitert(name, skript, ersatz, merkmal):
|
||||
# Die Vorpruefung muss scheitern: Exit 2 mit der Meldung der Probe und dem
|
||||
# Merkmal des Angriffs, ohne Modus-Zeile, ohne EXIT-Zeile, ohne Kopie.
|
||||
fall = fall_einfach()
|
||||
try:
|
||||
ordner = fall.basis / "attrappen"
|
||||
baue_attrappen_mit_ersatz(ordner, ersatz)
|
||||
vorher = plankritik_ordner()
|
||||
ergebnis = starte_skript(skript, fall, pfad=str(ordner))
|
||||
neu = sorted(str(p) for p in plankritik_ordner() - vorher)
|
||||
ok = (
|
||||
ergebnis.returncode == 2
|
||||
and "MUTANTEN_LAUF_FEHLER: lo im Netz-Namensraum nicht nutzbar" in ergebnis.stdout
|
||||
and merkmal in ergebnis.stdout
|
||||
and "modus:" not in ergebnis.stdout
|
||||
and "EXIT mutanten_lauf" not in ergebnis.stdout
|
||||
and not neu
|
||||
)
|
||||
melde(name, ok, "neue Kopien={0}, {1}".format(neu, kurz(ergebnis)))
|
||||
fall.pruefe_kopie(name)
|
||||
finally:
|
||||
fall.aufraeumen()
|
||||
|
||||
|
||||
def fall_w10(skript):
|
||||
# ip scheitert schon in der Probe (Angriff A1 des Pruefers).
|
||||
fahre_probe_scheitert("W10", skript, {"ip": "echo ip-attrappe scheitert >&2; exit 1"}, "ip-attrappe scheitert")
|
||||
|
||||
|
||||
def fall_w11(skript):
|
||||
# ip meldet Erfolg, lo bleibt aus (A3): Die Verbindung der Probe scheitert
|
||||
# mit Errno 101.
|
||||
fahre_probe_scheitert("W11", skript, {"ip": "exit 0"}, "Errno 101")
|
||||
|
||||
|
||||
def fall_w12(skript):
|
||||
# ip klappt in der Probe und scheitert erst im Hauptaufruf (A4); eine
|
||||
# Zaehlerdatei haelt fest, der wievielte Aufruf es ist. Erwartet:
|
||||
# Modus-Zeile, Meldung des Hauptaufrufs, EXIT-Zeile mit 2, kein Laeufer,
|
||||
# genau zwei Aufrufe von ip, keine Kopie unter /tmp.
|
||||
fall = fall_einfach()
|
||||
try:
|
||||
zaehler = fall.basis / "ip_zaehler"
|
||||
z = shlex.quote(str(zaehler))
|
||||
ip_echt = shlex.quote(shutil.which("ip"))
|
||||
ersatz = {"ip": (
|
||||
"n=0\n"
|
||||
"if [ -f " + z + " ]; then read n < " + z + "; fi\n"
|
||||
"n=$((n + 1))\n"
|
||||
"echo \"$n\" > " + z + "\n"
|
||||
"if [ \"$n\" -ge 2 ]; then echo \"ip-attrappe zweiter Aufruf\" >&2; exit 1; fi\n"
|
||||
"exec " + ip_echt + " \"$@\""
|
||||
)}
|
||||
ordner = fall.basis / "attrappen"
|
||||
baue_attrappen_mit_ersatz(ordner, ersatz)
|
||||
vorher = plankritik_ordner()
|
||||
ergebnis = starte_skript(skript, fall, pfad=str(ordner))
|
||||
neu = sorted(str(p) for p in plankritik_ordner() - vorher)
|
||||
try:
|
||||
aufrufe = zaehler.read_text(encoding="ascii").strip()
|
||||
except OSError:
|
||||
aufrufe = "keine"
|
||||
ok = (
|
||||
ergebnis.returncode == 2
|
||||
and "modus: voll" in ergebnis.stdout
|
||||
and "MUTANTEN_LAUF_FEHLER: lo im Netz-Namensraum nicht hochgefahren" in ergebnis.stdout
|
||||
and "EXIT mutanten_lauf: 2" in ergebnis.stdout
|
||||
and "nicht nutzbar" not in ergebnis.stdout
|
||||
and "MW7" not in ergebnis.stdout
|
||||
and "ip-attrappe zweiter Aufruf" in ergebnis.stderr
|
||||
and aufrufe == "2"
|
||||
and not neu
|
||||
)
|
||||
melde("W12", ok, "ip-aufrufe={0}, neue Kopien={1}, {2}".format(aufrufe, neu, kurz(ergebnis)))
|
||||
fall.pruefe_kopie("W12")
|
||||
finally:
|
||||
fall.aufraeumen()
|
||||
|
||||
|
||||
SETPRIV_OHNE_WIRKUNG = (
|
||||
"while [ \"$#\" -gt 0 ] && [ \"$1\" != \"--\" ]; do shift; done\n"
|
||||
"shift\n"
|
||||
"exec \"$@\""
|
||||
)
|
||||
|
||||
|
||||
def fall_w13(skript):
|
||||
# setpriv wirft keine Faehigkeit ab und startet nur den Befehl hinter --:
|
||||
# Die Faehigkeitspruefung der Probe muss anschlagen.
|
||||
fahre_probe_scheitert("W13", skript, {"setpriv": SETPRIV_OHNE_WIRKUNG}, "Faehigkeiten nicht abgeworfen")
|
||||
|
||||
|
||||
def prozesse_mit_marke(marke):
|
||||
```
|
||||
|
||||
### `mutanten_lauf_kopie_treiber.py`, Stelle 6 von 7
|
||||
|
||||
Aus:
|
||||
|
||||
```python
|
||||
FAELLE = [
|
||||
("W7", fall_w7),
|
||||
("W1", fall_w1),
|
||||
("W2", fall_w2),
|
||||
("W3", fall_w3),
|
||||
("W4", fall_w4),
|
||||
("W5", fall_w5),
|
||||
("W6", fall_w6),
|
||||
("W8", fall_w8),
|
||||
("W9", fall_w9),
|
||||
]
|
||||
```
|
||||
|
||||
wird:
|
||||
|
||||
```python
|
||||
FAELLE = [
|
||||
("W7", fall_w7),
|
||||
("W1", fall_w1),
|
||||
("W2", fall_w2),
|
||||
("W3", fall_w3),
|
||||
("W4", fall_w4),
|
||||
("W5", fall_w5),
|
||||
("W6", fall_w6),
|
||||
("W8", fall_w8),
|
||||
("W9", fall_w9),
|
||||
("W10", fall_w10),
|
||||
("W11", fall_w11),
|
||||
("W12", fall_w12),
|
||||
("W13", fall_w13),
|
||||
]
|
||||
```
|
||||
|
||||
### `mutanten_lauf_kopie_treiber.py`, Stelle 7 von 7
|
||||
|
||||
Aus:
|
||||
|
||||
```python
|
||||
zerleger = argparse.ArgumentParser(description="Treiber fuer mutanten_lauf_kopie.sh (Faelle W1 bis W9)")
|
||||
```
|
||||
|
||||
wird:
|
||||
|
||||
```python
|
||||
zerleger = argparse.ArgumentParser(description="Treiber fuer mutanten_lauf_kopie.sh (Faelle W1 bis W13)")
|
||||
```
|
||||
|
||||
Dann in der Git-Bash prüfen:
|
||||
|
||||
```bash
|
||||
# Treiber kompilieren, nur ASCII, keine CR, vier neue Faelle; __pycache__ danach weg
|
||||
cd /d/Claude/.vault-tools/plan-kopie
|
||||
python -m py_compile mutanten_lauf_kopie_treiber.py && echo kompiliert
|
||||
rm -rf __pycache__
|
||||
LC_ALL=C grep -cP '[^\x00-\x7F]' mutanten_lauf_kopie_treiber.py
|
||||
tr -cd '\r' < mutanten_lauf_kopie_treiber.py | wc -c
|
||||
grep -c '^def fall_w1[0-3](skript):$' mutanten_lauf_kopie_treiber.py
|
||||
```
|
||||
|
||||
Erwartet: `kompiliert`, dann `0`, `0`, `4`.
|
||||
|
||||
### Kein Rot vor der Umsetzung
|
||||
|
||||
Diese Nachbesserung ändert das Verhalten des Skripts nicht. Die neuen Fälle sind deshalb schon gegen den jetzigen Stand grün; W10 bis W12 hat der Prüfer als A1, A3 und A4 gemessen. Ihr Rot zeigen die Varianten in Schritt 4.
|
||||
|
||||
## Schritt 2 — Umsetzung: Kopfkommentar des Skripts
|
||||
|
||||
### `./mutanten_lauf_kopie.sh`, Stelle 1 von 1
|
||||
|
||||
Aus:
|
||||
|
||||
```bash
|
||||
# - Vorpruefung vor dem Kopieren: Fehlt ip, setpriv oder python3, kommt lo nicht hoch oder
|
||||
# bleiben Faehigkeiten, endet das Skript mit MUTANTEN_LAUF_FEHLER und Exit 2. Ein Lauf ohne
|
||||
# Netzsperre oder ohne lo ist nie die Ausweichloesung.
|
||||
# - Treiber: mutanten_lauf_kopie_treiber.py, Faelle W1 bis W9.
|
||||
set -u
|
||||
```
|
||||
|
||||
wird:
|
||||
|
||||
```bash
|
||||
# - Vorpruefung vor dem Kopieren: Fehlt ip, setpriv oder python3, kommt lo in einer Probe im
|
||||
# eigenen Namensraum nicht hoch, gelingt dort keine Verbindung ueber 127.0.0.1 oder hat die
|
||||
# Probe nach setpriv noch CapEff oder CapAmb ungleich 0, endet das Skript mit
|
||||
# MUTANTEN_LAUF_FEHLER und Exit 2, ohne EXIT-Zeile. Die Faehigkeiten prueft das Skript nur an
|
||||
# dieser Probe und nur CapEff und CapAmb, nicht am Laeufer selbst; alle vier Mengen am Laeufer
|
||||
# prueft Treiber-Fall W3. Die Vorpruefung steht vor der Suche nach mutanten.toml: Scheitert
|
||||
# sie, endet das Skript auch fuer einen Planordner ohne mutanten.toml mit Exit 2 statt mit
|
||||
# KEINE_MUTANTEN_TOML.
|
||||
# - Hauptaufruf: Kommt lo dort nicht hoch, endet das Skript nach der Modus-Zeile mit
|
||||
# MUTANTEN_LAUF_FEHLER, Exit 2 und EXIT-Zeile, ohne den Laeufer zu starten. Ein Lauf ohne
|
||||
# Netzsperre oder ohne lo ist nie die Ausweichloesung.
|
||||
# - Treiber: mutanten_lauf_kopie_treiber.py, Faelle W1 bis W13 (W10 bis W13 seit Nachbesserung 1,
|
||||
# docs/plans/2026-10-09-mutanten-kopie-loopback-nachbesserung-1.md).
|
||||
set -u
|
||||
```
|
||||
|
||||
Dann in der Git-Bash:
|
||||
|
||||
```bash
|
||||
# nur ASCII, keine CR, und ausser Kommentarzeilen nichts geaendert
|
||||
cd /d/Claude/.vault-tools/plan-kopie
|
||||
R=/c/Users/PASCAL~1/AppData/Local/Temp/mkopie-rot-n1
|
||||
LC_ALL=C grep -cP '[^\x00-\x7F]' mutanten_lauf_kopie.sh
|
||||
tr -cd '\r' < mutanten_lauf_kopie.sh | wc -c
|
||||
diff <(grep -v '^#' "$R/skript_vor_n1.sh") <(grep -v '^#' mutanten_lauf_kopie.sh) && echo nur-kommentar
|
||||
```
|
||||
|
||||
Erwartet: `0`, `0`, `nur-kommentar`.
|
||||
|
||||
In der PowerShell:
|
||||
|
||||
```powershell
|
||||
# Syntax des Skripts in WSL
|
||||
wsl -d Debian -- bash -n /mnt/d/Claude/.vault-tools/plan-kopie/mutanten_lauf_kopie.sh
|
||||
"exit=$LASTEXITCODE"
|
||||
```
|
||||
|
||||
Erwartet: nur `exit=0`.
|
||||
|
||||
## Schritt 3 — Verifikation: Treiber gegen das Skript
|
||||
|
||||
```powershell
|
||||
# Treiber in WSL gegen das Skript nach Nachbesserung 1
|
||||
wsl -d Debian -- timeout 900 /home/claude/venvs/trading/bin/python /mnt/d/Claude/.vault-tools/plan-kopie/mutanten_lauf_kopie_treiber.py *> "C:\Users\PASCAL~1\AppData\Local\Temp\mkopie-rot-n1\ausgabe-neu.txt"
|
||||
"exit=$LASTEXITCODE"
|
||||
Select-String -Path "C:\Users\PASCAL~1\AppData\Local\Temp\mkopie-rot-n1\ausgabe-neu.txt" -Pattern "^W\d.*FEHLER:|^gerechnete|^TREIBER_FEHLER|^rundreise"
|
||||
Select-String -Path "C:\Users\PASCAL~1\AppData\Local\Temp\mkopie-rot-n1\ausgabe-neu.txt" -Pattern "^W1[0-3] ok"
|
||||
```
|
||||
|
||||
Erwartet:
|
||||
|
||||
- `exit=0`.
|
||||
- Aus dem ersten `Select-String` genau zwei Treffer: einer mit `rundreise: /tmp/mkopie_treiber_rundreise_`, einer endet auf `gerechnete Fehler: 0`.
|
||||
- Aus dem zweiten genau vier Treffer, sie enden auf `W10 ok`, `W11 ok`, `W12 ok`, `W13 ok`.
|
||||
|
||||
Sonst anhalten und die ganze Datei `ausgabe-neu.txt` melden. Gerechnet, nicht gemessen ist nur W13; steht dort `W13 FEHLER: neue Kopien=[], exit=0`, hat die Probe ohne `setpriv` keine Fähigkeiten gesehen. Das ist ein Befund für den Koordinator, kein Grund, den Treiber anzupassen.
|
||||
|
||||
## Schritt 4 — Regressionsnachweis: vier Varianten
|
||||
|
||||
### Varianten bauen
|
||||
|
||||
Leg mit dem Write-Werkzeug die Datei `C:\Users\PASCAL~1\AppData\Local\Temp\mkopie-rot-n1\varianten_n1.py` an.
|
||||
|
||||
Inhalt:
|
||||
|
||||
```python
|
||||
# Baut vier kaputte Fassungen von mutanten_lauf_kopie.sh fuer Nachbesserung 1 (v6 bis v9).
|
||||
# Aufruf: python varianten_n1.py <skript> <zielordner>
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
quelle = Path(sys.argv[1])
|
||||
ziel = Path(sys.argv[2])
|
||||
text = quelle.read_bytes().decode("utf-8")
|
||||
if "\r" in text:
|
||||
raise SystemExit("Skript hat CR")
|
||||
|
||||
|
||||
def ersetze(text, aus, wird, anzahl):
|
||||
gefunden = text.count(aus)
|
||||
if gefunden != anzahl:
|
||||
raise SystemExit("{0!r}: {1} Treffer statt {2}".format(aus, gefunden, anzahl))
|
||||
return text.replace(aus, wird)
|
||||
|
||||
|
||||
varianten = {}
|
||||
# v6: Probe meldet, das Skript laeuft weiter; Zeile exit 2 nach der Meldung gestrichen (erwartet rot: W10, W11, W13)
|
||||
zeilen = text.split("\n")
|
||||
treffer = [i for i, z in enumerate(zeilen)
|
||||
if z.lstrip().startswith("echo \"MUTANTEN_LAUF_FEHLER: lo im Netz-Namensraum nicht nutzbar")]
|
||||
if len(treffer) != 1 or zeilen[treffer[0] + 1].strip() != "exit 2":
|
||||
raise SystemExit("Meldung der Probe mit exit 2 nicht eindeutig gefunden")
|
||||
k = treffer[0]
|
||||
varianten["v6_probe_ohne_exit"] = "\n".join(zeilen[:k + 1] + zeilen[k + 2:])
|
||||
# v7: Probe ohne setpriv (erwartet rot: W1 bis W9, W12)
|
||||
varianten["v7_probe_ohne_setpriv"] = ersetze(
|
||||
text, "exec setpriv --inh-caps=-all --ambient-caps=-all -- python3 -c ", "exec python3 -c ", 1)
|
||||
# v8: Hauptaufruf ohne den Zweig nach || (erwartet rot: W12)
|
||||
varianten["v8_haupt_ohne_oder"] = ersetze(
|
||||
text, " || { echo \"MUTANTEN_LAUF_FEHLER: lo im Netz-Namensraum nicht hochgefahren\"; exit 2; }", "", 1)
|
||||
# v9: Probe ohne Faehigkeitspruefung (erwartet rot: W13)
|
||||
varianten["v9_probe_ohne_faehigkeiten"] = ersetze(
|
||||
text, "if int(felder[\"CapEff\"], 16) != 0 or int(felder[\"CapAmb\"], 16) != 0:", "if False:", 1)
|
||||
|
||||
for name, inhalt in varianten.items():
|
||||
if inhalt == text:
|
||||
raise SystemExit(name + " unveraendert")
|
||||
(ziel / (name + ".sh")).write_bytes(inhalt.encode("utf-8"))
|
||||
print(name)
|
||||
```
|
||||
|
||||
In der Git-Bash:
|
||||
|
||||
```bash
|
||||
# Varianten aus dem Skript nach Nachbesserung 1 bauen
|
||||
R=/c/Users/PASCAL~1/AppData/Local/Temp/mkopie-rot-n1
|
||||
python "$R/varianten_n1.py" /d/Claude/.vault-tools/plan-kopie/mutanten_lauf_kopie.sh "$R"
|
||||
```
|
||||
|
||||
Erwartet genau diese vier Zeilen:
|
||||
|
||||
```text
|
||||
v6_probe_ohne_exit
|
||||
v7_probe_ohne_setpriv
|
||||
v8_haupt_ohne_oder
|
||||
v9_probe_ohne_faehigkeiten
|
||||
```
|
||||
|
||||
Endet das Skript mit einer Meldung statt der vier Zeilen: anhalten und melden.
|
||||
|
||||
### Treiber gegen jede Variante
|
||||
|
||||
```powershell
|
||||
# Treiber in WSL gegen jede kaputte Variante
|
||||
foreach ($v in "v6_probe_ohne_exit","v7_probe_ohne_setpriv","v8_haupt_ohne_oder","v9_probe_ohne_faehigkeiten") {
|
||||
wsl -d Debian -- timeout 900 /home/claude/venvs/trading/bin/python /mnt/d/Claude/.vault-tools/plan-kopie/mutanten_lauf_kopie_treiber.py --skript "/mnt/c/Users/PASCAL~1/AppData/Local/Temp/mkopie-rot-n1/$v.sh" *> "C:\Users\PASCAL~1\AppData\Local\Temp\mkopie-rot-n1\ausgabe-$v.txt"
|
||||
"$v exit=$LASTEXITCODE"
|
||||
Select-String -Path "C:\Users\PASCAL~1\AppData\Local\Temp\mkopie-rot-n1\ausgabe-$v.txt" -Pattern "^W\d.*FEHLER:|^gerechnete|^TREIBER_FEHLER"
|
||||
}
|
||||
```
|
||||
|
||||
Erwartet je Variante `exit=1`, und die `FEHLER`-Zeilen betreffen genau diese Fälle. Zeilen wie `W12 (kopie) FEHLER:` oder `W12 (aufraeumen) FEHLER:` zählen zu ihrem Fall:
|
||||
|
||||
| Variante | rote Fälle | gerechnet: Zeilen der neuen Fälle | was es belegt |
|
||||
|---|---|---|---|
|
||||
| `v6_probe_ohne_exit` | W10, W11, W13 | `W10 FEHLER: neue Kopien=[], exit=2`, `W11 FEHLER: neue Kopien=[], exit=0`, `W13 FEHLER: neue Kopien=[], exit=0` | W10, W11 und W13 sehen ein Skript, das nach gescheiterter Probe weiterläuft |
|
||||
| `v7_probe_ohne_setpriv` | W1 bis W9, W12 | `W12 FEHLER: ip-aufrufe=1, neue Kopien=[], exit=2` | die Fähigkeitsprüfung der Probe schlägt an; mit Fähigkeiten läuft kein Mutant |
|
||||
| `v8_haupt_ohne_oder` | W12 | `W12 FEHLER: ip-aufrufe=2, neue Kopien=[], exit=0` | W12 sieht den fehlenden Zweig nach `\|\|` |
|
||||
| `v9_probe_ohne_faehigkeiten` | W13 | `W13 FEHLER: neue Kopien=[], exit=0` | W13 sieht eine Probe ohne Fähigkeitsprüfung |
|
||||
|
||||
- Pflicht ist die Menge der roten Fälle je Variante. Weicht sie ab: anhalten und die ganze Ausgabedatei der Variante melden. Nicht weiter zu Schritt 5.
|
||||
- Gerechnet, nicht gemessen: die Zeilen in der dritten Spalte und die Zahl hinter `gerechnete Fehler:` (3, 10, 1, 1). Weicht nur ein solcher Wert ab, meldest du ihn und machst weiter.
|
||||
|
||||
## Schritt 5 — Doku und Plan
|
||||
|
||||
### `LIESMICH.md`, Stelle 1 von 1
|
||||
|
||||
Aus:
|
||||
|
||||
```markdown
|
||||
- `mutanten_lauf_kopie_treiber.py` ruft das Skript wie die Plankritik mit Windows-Pfaden auf und fährt neun Fälle: Server auf `127.0.0.1` (W1), keine Route nach außen und `ENETUNREACH` (W2), uid wie außen und keine Fähigkeiten (W3), `ip`, `setpriv` oder `python3` fehlen (W4, W5, W9), Zeitgrenze mit `timeout` als Elternprozess des Läufers und ohne Prozessreste (W6), Grundlinie ohne Server in beiden Modi (W7), Dienst des Rechners unerreichbar (W8). Neuer Stand ohne Fehler.
|
||||
- Gegen das Skript vor der Änderung rot: W1, W2, W4, W5, W9. Gegen kaputte Varianten genau an ihrem Fall: ohne `setpriv` W3, `setpriv` ohne `exec` W6, ohne Vorprüfung W4, W5 und W9, ohne `--keep-caps` alle (die Vorprüfung meldet `Operation not permitted`), ohne Namensraum W2 und W8.
|
||||
- Nicht abgeworfen und nicht geprüft: die Begrenzungsmenge (`CapBnd`) und `no_new_privs`. `plankritik.js` führt eine Vorprüfung mit `MUTANTEN_LAUF_FEHLER` wie bisher bei fehlendem `unshare` als `ohne_ergebnis`, ohne den Text der Meldung.
|
||||
```
|
||||
|
||||
wird:
|
||||
|
||||
```markdown
|
||||
- `mutanten_lauf_kopie_treiber.py` ruft das Skript wie die Plankritik mit Windows-Pfaden auf und fährt die Fälle W1 bis W13: Server auf `127.0.0.1` (W1), keine Route nach außen und `ENETUNREACH` (W2), uid wie außen und keine Fähigkeiten (W3), `ip`, `setpriv` oder `python3` fehlen (W4, W5, W9), Zeitgrenze mit `timeout` als Elternprozess des Läufers und ohne Prozessreste (W6), Grundlinie ohne Server in beiden Modi (W7), Dienst des Rechners unerreichbar (W8). Seit Nachbesserung 1 auch: `ip` scheitert in der Probe (W10), `ip` meldet Erfolg und `lo` bleibt aus (W11), `ip` scheitert erst im Hauptaufruf nach der Modus-Zeile (W12), `setpriv` wirft keine Fähigkeit ab (W13). W10 bis W13 ersetzen dazu einzelne Werkzeuge durch kleine `sh`-Skripte. Neuer Stand ohne Fehler.
|
||||
- Gegen das Skript vor der Änderung rot: W1, W2, W4, W5, W9. Gegen kaputte Varianten genau an ihrem Fall, gefahren mit W1 bis W9: ohne `setpriv` W3, `setpriv` ohne `exec` W6, ohne Vorprüfung W4, W5 und W9, ohne `--keep-caps` alle (die Vorprüfung meldet `Operation not permitted`), ohne Namensraum W2 und W8. Gefahren mit W1 bis W13 (Nachbesserung 1, `docs/plans/2026-10-09-mutanten-kopie-loopback-nachbesserung-1.md`): ohne `exit 2` nach der Probe W10, W11 und W13; Probe ohne `setpriv` W1 bis W9 und W12; Hauptaufruf ohne den Zweig nach `||` W12; Probe ohne Fähigkeitsprüfung W13.
|
||||
- Nicht abgeworfen und nicht geprüft: die Begrenzungsmenge (`CapBnd`) und `no_new_privs`. `plankritik.js` führt eine Vorprüfung mit `MUTANTEN_LAUF_FEHLER` wie bisher bei fehlendem `unshare` als `ohne_ergebnis`, ohne den Text der Meldung.
|
||||
- Die Fähigkeiten prüft das Skript nur an der Probe und nur `CapEff` und `CapAmb`, nicht am Läufer selbst. Alle vier Mengen am Läufer prüft Treiber-Fall W3.
|
||||
- Die Vorprüfung steht vor der Suche nach `mutanten.toml`. Fehlt auf einem Rechner `ip`, `setpriv` oder `python3` oder kommt `lo` nicht hoch, endet das Skript deshalb auch für einen Planordner ohne `mutanten.toml` mit `MUTANTEN_LAUF_FEHLER` und Exit 2 statt mit `KEINE_MUTANTEN_TOML`. `plankritik.js` meldet dann `ohne_ergebnis` statt `keine_toml`, und die Linse erfährt nicht, dass die `mutanten.toml` fehlt. Das bleibt so.
|
||||
- Lauf mit echten Daten: die 73 Mutanten des Pakets RB3 (Kopie `rb3d-kopie`) durch das neue Skript, alle `OK`, T1 und T12 im Namensraum.
|
||||
- Ein `pruefer` fand keinen blockierenden Befund. Seine Lücken (kein Fall für den Fehlerzweig der Probe und den Zweig nach `||` im Hauptaufruf, Kopfkommentar zu weit gefasst, der Satz zur Reihenfolge oben) behebt Nachbesserung 1. Rohdaten: `AI/secondbrain/trading-plattform-memory/2026-10-09-mkopie-roh/`.
|
||||
```
|
||||
|
||||
### `mutanten_lauf_ENTWURF.md`, Stelle 1 von 1
|
||||
|
||||
Aus:
|
||||
|
||||
```markdown
|
||||
Treiber: `mutanten_lauf_kopie_treiber.py` (W1 bis W9), Plan `docs/plans/2026-10-09-mutanten-kopie-loopback.md`.
|
||||
```
|
||||
|
||||
wird:
|
||||
|
||||
```markdown
|
||||
Treiber: `mutanten_lauf_kopie_treiber.py` (W1 bis W13), Plan `docs/plans/2026-10-09-mutanten-kopie-loopback.md` und seine Nachbesserung 1. Die Fähigkeiten prüft der Wrapper nur an der Probe (`CapEff`, `CapAmb`), nicht am Läufer. Weil die Vorprüfung vor der Suche nach `mutanten.toml` steht, endet er ohne `ip`, `setpriv`, `python3` oder nutzbares `lo` auch ohne `mutanten.toml` mit Exit 2 statt mit `KEINE_MUTANTEN_TOML`.
|
||||
```
|
||||
|
||||
### `docs/plans/2026-10-09-mutanten-kopie-loopback.md`, Stelle 1 von 3
|
||||
|
||||
Aus:
|
||||
|
||||
```markdown
|
||||
- **Datensatz:** Paket RB3 der Trading Plattform, Kopie `D:/Claude/.arbeitsbaeume/rb3b-kopie`, Planordner `D:/Claude/.arbeitsbaeume/trading-plattform-rb3/docs/plans/2026-10-08-paket-rb3-totmann-ping` (dieselben wie in `rb3_mutanten.sh`). Fehlt die Kopie, vorher neu bauen.
|
||||
```
|
||||
|
||||
wird:
|
||||
|
||||
```markdown
|
||||
- **Datensatz:** Paket RB3 der Trading Plattform, Kopie `D:/Claude/.arbeitsbaeume/rb3d-kopie`, Planordner `D:/Claude/.arbeitsbaeume/trading-plattform-rb3/docs/plans/2026-10-08-paket-rb3-totmann-ping` (dieselben wie in `rb3d_mutanten.sh`). Die Mutanten der `mutanten.toml` passen nur zum Stand in `rb3d-kopie`, nicht zu `rb3b-kopie` aus `rb3_mutanten.sh` (berichtigt in Nachbesserung 1). Fehlt die Kopie, vorher neu bauen.
|
||||
```
|
||||
|
||||
### `docs/plans/2026-10-09-mutanten-kopie-loopback.md`, Stelle 2 von 3
|
||||
|
||||
Aus:
|
||||
|
||||
```powershell
|
||||
wsl -d Debian -- env BUDGET_S=3000 ZEITLIMIT_S=300 timeout 3300 bash /mnt/d/Claude/.vault-tools/plan-kopie/mutanten_lauf_kopie.sh "D:/Claude/.arbeitsbaeume/rb3b-kopie" "D:/Claude/.arbeitsbaeume/trading-plattform-rb3/docs/plans/2026-10-08-paket-rb3-totmann-ping" voll *> "C:\Users\PASCAL~1\AppData\Local\Temp\mkopie-rot\rb3.txt"
|
||||
```
|
||||
|
||||
wird:
|
||||
|
||||
```powershell
|
||||
wsl -d Debian -- env BUDGET_S=3000 ZEITLIMIT_S=300 timeout 3300 bash /mnt/d/Claude/.vault-tools/plan-kopie/mutanten_lauf_kopie.sh "D:/Claude/.arbeitsbaeume/rb3d-kopie" "D:/Claude/.arbeitsbaeume/trading-plattform-rb3/docs/plans/2026-10-08-paket-rb3-totmann-ping" voll *> "C:\Users\PASCAL~1\AppData\Local\Temp\mkopie-rot\rb3.txt"
|
||||
```
|
||||
|
||||
### `docs/plans/2026-10-09-mutanten-kopie-loopback.md`, Stelle 3 von 3
|
||||
|
||||
Aus:
|
||||
|
||||
```markdown
|
||||
Die übrigen Mutanten haben denselben Status wie in Teil A von `rb3_mutanten.sh`.
|
||||
```
|
||||
|
||||
wird:
|
||||
|
||||
```markdown
|
||||
Die übrigen Mutanten haben denselben Status wie in Teil A von `rb3d_mutanten.sh`.
|
||||
```
|
||||
|
||||
## Schritt 6 — Abschlussprüfung
|
||||
|
||||
In der Git-Bash:
|
||||
|
||||
```bash
|
||||
# keine CR in den fuenf Zieldateien, nichts ausserhalb der Zieldateien geaendert
|
||||
cd /d/Claude/.vault-tools/plan-kopie
|
||||
for f in mutanten_lauf_kopie_treiber.py mutanten_lauf_kopie.sh LIESMICH.md mutanten_lauf_ENTWURF.md docs/plans/2026-10-09-mutanten-kopie-loopback.md; do printf '%s %s\n' "$f" "$(tr -cd '\r' < "$f" | wc -c)"; done
|
||||
ls -d __pycache__ 2>/dev/null || echo kein-pycache
|
||||
git -C /d/Claude status --porcelain -- .vault-tools
|
||||
```
|
||||
|
||||
Erwartet: je Datei die Zahl `0`, dann `kein-pycache`, dann dieselben sechs Zeilen wie in Schritt 0.
|
||||
|
||||
Den Rot-Ordner lässt du stehen; der Koordinator legt die Ausgabedateien ab und löscht ihn.
|
||||
|
||||
## Bericht
|
||||
|
||||
Melde:
|
||||
|
||||
- je Zieldatei die Zahl der angewendeten Aus/wird-Paare (Treiber 7, Skript 1, LIESMICH 1, Entwurf 1, Plan 3);
|
||||
- die Ausgaben aus Schritt 0, 1, 2, 3 (alle `Select-String`-Treffer im Wortlaut), 4 (Variantenzeilen und alle `Select-String`-Treffer im Wortlaut) und 6;
|
||||
- jede Abweichung vom Plan, jeden gerechneten Wert, der anders ausfiel, und jede getroffene Annahme. „Keine Abweichungen“ nur, wenn es keine gab.
|
||||
|
||||
## Fertig heißt (Koordinator)
|
||||
|
||||
1. Die fünf Zieldateien sind byte-gleich mit der Kopie, auf die diese Nachbesserung per `plan_materialisiere.py` angewendet wurde (`cmp`); sonst ist unter `.vault-tools/` nichts geändert.
|
||||
2. Treiber in WSL gegen das Skript nach Nachbesserung 1, selbst gefahren: Exit 0, `gerechnete Fehler: 0`, `W10 ok` bis `W13 ok` stehen da.
|
||||
3. Die vier Varianten selbst gesehen: rot genau an ihren Fällen laut Tabelle in Schritt 4. Die Ausgabedateien aus dem Rot-Ordner liegen als Rohdaten neben `pruefer/` unter `AI/secondbrain/trading-plattform-memory/2026-10-09-mkopie-roh/`.
|
||||
4. Schritt 2 hat `nur-kommentar` gezeigt: Das Verhalten des Skripts ist unverändert, der Lauf mit echten Daten vom 2026-10-09 gilt weiter.
|
||||
|
||||
## Lauf mit echten Daten
|
||||
|
||||
Entfällt. Das Skript ändert sich nur im Kommentar, Schritt 2 belegt das mit `grep -v '^#'`. Es gilt der Lauf des Prüfers vom 2026-10-09: 73 Mutanten von RB3 aus `rb3d-kopie`, alle `OK`, T1 und T12 im Namensraum.
|
||||
|
||||
## Risiken
|
||||
|
||||
1. **W13 auf einem Rechner ohne ambiente Fähigkeiten.** Hebt `unshare --keep-caps` die Fähigkeiten nicht in die ambiente Menge, sieht die Probe ohne `setpriv` keine, und W13 ist schon gegen das neue Skript rot. Merkmal: `W13 FEHLER: neue Kopien=[], exit=0` in Schritt 3. Dann wäre auch v7 grün statt rot; Befund für den Koordinator.
|
||||
2. **Zählerdatei in W12.** Die Attrappe schreibt im Namensraum in den Fallordner des Treibers. Gelingt das nicht, zählt `ip` nicht weiter. Merkmal: `ip-aufrufe=` ungleich `2` auf dem neuen Skript.
|
||||
3. **Gleichzeitige Plankritik.** Legt eine Plankritik während des Treiberlaufs `/tmp/plankritik-mut-*` an, meldet der Treiber `(aufraeumen)` für den gerade laufenden Fall.
|
||||
4. **Laufzeit.** Fünf Treiberläufe; v7 wartet in W6 bis zum Ende der Schleife (rund sechs Sekunden). Zusammen gerechnet unter einer Viertelstunde, gemessen nicht.
|
||||
|
||||
## Getroffene Annahmen
|
||||
|
||||
- W13 ließ sich ohne Variante bauen: eine `setpriv`-Attrappe, die nichts abwirft. v7 des Prüfers bleibt trotzdem im Nachweis, wie beauftragt. Weil v7 W13 nicht rot macht (die Probe scheitert dort ebenfalls), kommt v9 dazu: Probe ohne Fähigkeitsprüfung, rot nur an W13.
|
||||
- Für den Zweig nach `||` im Hauptaufruf kommt Variante v8 dazu. Der Auftrag nannte nur v6 und v7; v6 lässt W12 grün, v7 macht W12 nur über die Probe rot.
|
||||
- W10 bis W13 fahren keinen Kontrolllauf wie W4, W5 und W9. Jeder prüft stattdessen ein Merkmal, das nur seine Attrappe oder die Probe erzeugt (Text der Attrappe, `Errno 101`, Zählerstand, `Faehigkeiten nicht abgeworfen`). Dass ein Ordner nur aus Verweisen ein grünes Skript ergibt, zeigt der Kontrolllauf in W4.
|
||||
- „Keine Reste“ in W12 heißt: kein neuer Ordner `/tmp/plankritik-mut-*` und die übergebene Kopie byte-gleich. Prozessreste prüft W6.
|
||||
- Das echte `ip` holt W12 über `shutil.which("ip")` des Treibers statt fest über `/usr/bin/ip` wie der Prüfer.
|
||||
- Kein neuer Lauf gegen das Skript aus HEAD und gegen v1 bis v5: Sie belegen W1 bis W9, und an diesen Fällen ändert sich nichts. LIESMICH sagt deshalb, mit welchen Fällen welche Varianten gefahren sind.
|
||||
- Im Plan ändert sich neben dem Datensatz auch der Verweis auf das Skript mit Teil A (`rb3d_mutanten.sh` statt `rb3_mutanten.sh`), weil `rb3_mutanten.sh` `rb3b-kopie` benutzt. Die Verweise auf `rb3_mutanten.sh` im Anlass des Plans bleiben, sie beschreiben die Geschichte.
|
||||
- Der Satz zu Befund 3 steht in LIESMICH, wie beauftragt, und zusätzlich knapp im Kopfkommentar und im Entwurf, weil beide die Vorprüfung beschreiben.
|
||||
- `modell: haiku`: Alles steht wörtlich da, kein Codeblock trägt typografische Zeichen.
|
||||
|
||||
## Offene Punkte
|
||||
|
||||
- **Gerechnet, nicht gemessen:** W13 grün gegen das neue Skript; die roten Mengen von v7, v8 und v9; bei v6 W11 und W13 rot; alle Zeilen hinter `FEHLER:` und die Zahlen hinter `gerechnete Fehler:` in Schritt 4. Gemessen sind W10 bis W12 als A1, A3 und A4, v6 grün an W1 bis W9 und rot an W10 (A6).
|
||||
- **Ausgabevergleich der Probe ohne Fall.** Endet die Probe mit 0, aber mit anderer Ausgabe als `lo-probe ok`, greift der zweite Teil der Bedingung. Dafür gibt es weder Fall noch Variante; im Docstring des Treibers als Lücke benannt.
|
||||
- **`plankritik.js` verliert die Meldung** weiter (offener Punkt des Plans). Mit Befund 3 trifft das jetzt auch den Hinweis auf eine fehlende `mutanten.toml`.
|
||||
- **Weitere Prüfrunde:** Die Nachbesserung ändert nur Treiber und Text. Ob ein `pruefer` sie noch einmal liest oder Schritt 3 und 4 genügen, entscheidest du.
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,81 @@
|
||||
"""Setzt eine Ersetzungsliste eines Planers in die Taskdateien ein, alles oder nichts.
|
||||
|
||||
Format je Abschnitt (siehe Auftrag an den Planer):
|
||||
## Ersetzung N — Titel
|
||||
Datei: <taskdatei>
|
||||
Alt:
|
||||
~~~~text
|
||||
...
|
||||
~~~~
|
||||
Neu:
|
||||
~~~~text
|
||||
...
|
||||
~~~~
|
||||
Jeder Alt-Text muss zum Zeitpunkt seiner Ersetzung genau einmal in der Datei stehen. Bricht beim ersten
|
||||
Verstoss ab, ohne eine Datei zu schreiben. Mit --pruefen nur pruefen, nicht schreiben.
|
||||
Aufruf: python ersetzungen_einsetzen.py <planordner> <korrekturdatei> [--pruefen]
|
||||
"""
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ordner = Path(sys.argv[1])
|
||||
korrektur = Path(sys.argv[2])
|
||||
nur_pruefen = "--pruefen" in sys.argv[3:]
|
||||
|
||||
zeilen = korrektur.read_text(encoding="utf-8").split("\n")
|
||||
ersetzungen = []
|
||||
i = 0
|
||||
while i < len(zeilen):
|
||||
if not zeilen[i].startswith("## Ersetzung "):
|
||||
i += 1
|
||||
continue
|
||||
titel = zeilen[i]
|
||||
i += 1
|
||||
datei = alt = neu = None
|
||||
while i < len(zeilen) and not zeilen[i].startswith("## Ersetzung "):
|
||||
z = zeilen[i]
|
||||
if z.startswith("Datei:"):
|
||||
datei = z.split(":", 1)[1].strip().strip("`")
|
||||
elif z.strip() in ("Alt:", "Neu:"):
|
||||
art = z.strip()[:-1]
|
||||
if i + 1 >= len(zeilen) or not zeilen[i + 1].startswith("~~~~"):
|
||||
sys.exit(f"{titel}: nach {art}: fehlt die Zeile ~~~~text")
|
||||
j = i + 2
|
||||
inhalt = []
|
||||
while j < len(zeilen) and zeilen[j].rstrip() != "~~~~":
|
||||
inhalt.append(zeilen[j])
|
||||
j += 1
|
||||
if j >= len(zeilen):
|
||||
sys.exit(f"{titel}: {art}-Block ohne schliessende ~~~~")
|
||||
text = "\n".join(inhalt)
|
||||
if art == "Alt":
|
||||
alt = text
|
||||
else:
|
||||
neu = text
|
||||
i = j
|
||||
i += 1
|
||||
if datei is None or alt is None or neu is None:
|
||||
sys.exit(f"{titel}: Datei, Alt oder Neu fehlt")
|
||||
ersetzungen.append((titel, datei, alt, neu))
|
||||
|
||||
texte: dict[str, str] = {}
|
||||
for titel, datei, alt, neu in ersetzungen:
|
||||
if datei not in texte:
|
||||
pfad = ordner / datei
|
||||
if not pfad.is_file():
|
||||
sys.exit(f"{titel}: Datei {datei} gibt es nicht")
|
||||
texte[datei] = pfad.read_text(encoding="utf-8")
|
||||
n = texte[datei].count(alt)
|
||||
if n != 1:
|
||||
sys.exit(f"{titel}: Alt-Text steht {n}-mal in {datei} statt einmal; nichts geschrieben")
|
||||
texte[datei] = texte[datei].replace(alt, neu)
|
||||
print(f"ok {titel} ({datei})")
|
||||
|
||||
print(f"{len(ersetzungen)} Ersetzungen in {len(texte)} Datei(en) geprueft")
|
||||
if nur_pruefen:
|
||||
print("nur geprueft, nichts geschrieben")
|
||||
else:
|
||||
for datei, text in texte.items():
|
||||
(ordner / datei).write_text(text, encoding="utf-8", newline="\n")
|
||||
print("geschrieben:", ", ".join(sorted(texte)))
|
||||
@@ -0,0 +1,98 @@
|
||||
"""Baut die schlanke Kopie eines Projektbaums fuer den Byte-Vergleich -- Ausschluesse nur als Pfade vom Wurzelordner.
|
||||
|
||||
Ersteller: Pascal Michel
|
||||
Erstellt: 2026-09-17
|
||||
Doku: .vault-tools/plan-kopie/LIESMICH.md
|
||||
|
||||
Aufruf: python kopie_bauen.py <projekt> <ziel> --ohne <relpfad> [<relpfad> ...]
|
||||
|
||||
Warum Pfade und keine Namen: Ein Ausschluss von "jedem Ordner namens data" hat am 2026-09-17 auch das Paket
|
||||
trading/data getroffen. Nach dem Kopieren vergleicht das Skript deshalb die Python-Pakete (Ordner mit __init__.py)
|
||||
beider Baeume und meldet jedes, das in der Kopie fehlt und nicht unter einem ausgeschlossenen Pfad liegt.
|
||||
Das Ziel darf nicht existieren; Verknuepfungen und Junctions werden nicht verfolgt, sondern uebersprungen.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import shutil
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def _relativ(pfad: Path, wurzel: Path) -> str:
|
||||
return pfad.relative_to(wurzel).as_posix()
|
||||
|
||||
|
||||
def _pakete(wurzel: Path, ohne: set[str]) -> set[str]:
|
||||
gefunden = set()
|
||||
for ordner, unterordner, dateien in os.walk(wurzel):
|
||||
rel = _relativ(Path(ordner), wurzel)
|
||||
unterordner[:] = [
|
||||
name for name in unterordner
|
||||
if (f"{rel}/{name}" if rel != "." else name) not in ohne
|
||||
and not Path(ordner, name).is_symlink()
|
||||
and not Path(ordner, name).is_junction()
|
||||
]
|
||||
if "__init__.py" in dateien:
|
||||
gefunden.add(rel)
|
||||
return gefunden
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(allow_abbrev=False)
|
||||
parser.add_argument("projekt", type=Path)
|
||||
parser.add_argument("ziel", type=Path)
|
||||
parser.add_argument("--ohne", nargs="*", default=[], help="relative Pfade vom Wurzelordner, mit /")
|
||||
args = parser.parse_args()
|
||||
|
||||
projekt = args.projekt.resolve()
|
||||
ziel = args.ziel.resolve()
|
||||
if not projekt.is_dir():
|
||||
print(f"BEFUND Projekt {projekt} ist kein Ordner")
|
||||
return 2
|
||||
if ziel.exists():
|
||||
print(f"BEFUND Ziel {ziel} existiert schon -- erst ansehen, dann selbst loeschen")
|
||||
return 2
|
||||
|
||||
ohne = {eintrag.replace("\\", "/").strip("/") for eintrag in args.ohne}
|
||||
befunde = []
|
||||
for eintrag in sorted(ohne):
|
||||
if "/" not in eintrag and not (projekt / eintrag).exists():
|
||||
befunde.append(f"Ausschluss '{eintrag}' gibt es im Wurzelordner nicht -- ein Name statt eines Pfads?")
|
||||
elif not (projekt / eintrag).exists():
|
||||
befunde.append(f"Ausschluss '{eintrag}' gibt es nicht")
|
||||
|
||||
uebersprungen = []
|
||||
|
||||
def ignoriere(ordner: str, namen: list[str]) -> list[str]:
|
||||
rel = _relativ(Path(ordner), projekt)
|
||||
raus = []
|
||||
for name in namen:
|
||||
pfad = Path(ordner, name)
|
||||
relpfad = name if rel == "." else f"{rel}/{name}"
|
||||
if relpfad in ohne or name == "__pycache__":
|
||||
raus.append(name)
|
||||
elif pfad.is_symlink() or pfad.is_junction():
|
||||
uebersprungen.append(relpfad)
|
||||
raus.append(name)
|
||||
return raus
|
||||
|
||||
shutil.copytree(projekt, ziel, ignore=ignoriere)
|
||||
|
||||
fehlend = sorted(_pakete(projekt, ohne) - _pakete(ziel, set()))
|
||||
for paket in fehlend:
|
||||
befunde.append(f"Paket '{paket}' fehlt in der Kopie")
|
||||
|
||||
dateien = sum(len(d) for _, _, d in os.walk(ziel))
|
||||
print(f"{dateien} Dateien kopiert, {len(ohne)} Pfade ausgeschlossen, "
|
||||
f"{len(uebersprungen)} Verknuepfungen uebersprungen, {len(_pakete(ziel, set()))} Pakete in der Kopie.")
|
||||
for zeile in uebersprungen:
|
||||
print(f"HINWEIS Verknuepfung uebersprungen: {zeile}")
|
||||
for zeile in befunde:
|
||||
print(f"BEFUND {zeile}")
|
||||
print(f"{len(befunde)} Befund(e).")
|
||||
return 1 if befunde else 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,229 @@
|
||||
---
|
||||
type: resource
|
||||
status: active
|
||||
title: "Entwurf: mutanten_lauf.py — allgemeiner Mutantenläufer aus der Plantabelle"
|
||||
summary: "Entscheidungsliste, Dateiformat mutanten.toml, Ablauf, Status und Exit-Codes, Abdichtung und Angriffsliste für den allgemeinen Mutantenläufer. Punkt 3 der Arbeitsweise vom 2026-09-26."
|
||||
tags: [vault-tools, codeworkflow, pruefwerkzeuge]
|
||||
created: 2026-09-27
|
||||
updated: 2026-10-09
|
||||
---
|
||||
|
||||
# Entwurf: `mutanten_lauf.py`
|
||||
|
||||
Bisher hatte jeder Plan seinen eigenen Mutantenläufer. `mutlauf.py` für L1a las die Mutanten per Regex aus dem Plantext, `mutanten_py.py` hielt sie als Python-Liste. Die Mutanten von B1 stehen in einer dritten Form im Plan, die keiner der beiden liest. Der allgemeine Läufer liest stattdessen **eine Datei in einem festen Format**, die im Planordner neben der Übersicht liegt: `mutanten.toml`. Prosa im Plan wird nicht geparst.
|
||||
|
||||
Das Werkzeug ist ein Fangnetz, kein Nachweis. Es sagt, ob die genannten Tests unter einer bestimmten Änderung fallen. Ob sie **an der gemeinten Stelle** fallen, zeigt es nur über die Fehlermeldung, die es mit ausgibt. Das Urteil darüber bleibt beim Menschen.
|
||||
|
||||
## 1 Entscheidungsliste
|
||||
|
||||
| Frage | Herkunft |
|
||||
|---|---|
|
||||
| Steht das Muster eines Mutanten genau einmal als ganze Zeilenfolge in seiner Datei? | Projektvorgabe (Pläne L1a und B1: „jedes Muster steht als ganze Zeile genau einmal in seiner Datei“) |
|
||||
| Sind alle genannten Tests ohne Mutant grün (Grundlinie)? | Projektvorgabe ([[AI/Regelwerk/pruefwerkzeuge\|pruefwerkzeuge.md]], Abschnitt 3: zuerst die Grundlinie) |
|
||||
| Fällt unter dem Mutanten **jeder** genannte Test? | Projektvorgabe (Pläne: „‚Rot bei‘ nennt Tests, die fallen müssen“) |
|
||||
| Was heißt „fällt“? | Projektvorgabe: Der Test hat in einer Phase (setup, call, teardown) das Ergebnis `failed`, oder er fehlt, weil das Einsammeln seiner Datei scheitert. `skipped` und `xfail` sind nicht rot. |
|
||||
| Ist die Datei nach jedem Mutanten wieder byte-gleich? | Projektvorgabe: SHA-256 vorher und nachher |
|
||||
| Läuft das auf einer Wegwerfkopie? | Projektvorgabe: Die Wurzel darf kein `.git` haben (Ordner oder Datei), und keiner der Pfade aus `ohne` darf existieren |
|
||||
| Welche Tests und Ergebnisse meldet pytest? | Quelle: pytest-Hooks `pytest_runtest_logreport`, `pytest_collectreport`, `pytest_deselected` (pytest 9.1.1 im Venv `trading`, nachgesehen 2026-09-27). Die Knotennamen kommen aus `report.nodeid`, nicht aus einer JUnit-Klassenbezeichnung. |
|
||||
| Hält die Abdichtung Netz und echte Zugänge fern? | Annahme, aufgelöst als Projektvorgabe: Sie ist ein Fangnetz mit benannten Lücken (Abschnitt 5), kein Nachweis. |
|
||||
| Welche pytest-Konfiguration gilt? | Quelle: pytest 9.1.1, `_pytest/config/findpaths.py` (`determine_setup`, `locate_config`, `get_common_ancestor`, `load_config_dict_from_file`; SHA-256 `535d149e6f3962e4…`, in den Venvs `trading` und `mattermost` gleich) und `_pytest/config/__init__.py` (Vorgabe von `confcutdir`, `ExitCode`), nachgelesen am 2026-10-06; die Abschnittszeile in `tox.ini` und `setup.cfg` aus iniconfig 2.3.0 (`_parse.py`, `_parseline`) im selben Venv. Projektvorgabe seit 2026-10-06: gesucht wird wie pytest, aber nie oberhalb der Wurzel (Abschnitt 3, „Konfiguration“). |
|
||||
|
||||
## 2 Dateiformat `mutanten.toml`, Format 1
|
||||
|
||||
TOML, gelesen mit `tomllib`. Literale Zeichenketten in `'''…'''` nehmen Backslashes wörtlich; deshalb dieses Format und nicht JSON.
|
||||
|
||||
```toml
|
||||
format = 1
|
||||
python = "/home/claude/venvs/trading/bin/python"
|
||||
ohne = ["config/secrets.toml", "config/zugang.toml"]
|
||||
|
||||
[kuerzel]
|
||||
DA = "tests/test_bewertung_daten.py"
|
||||
U = "tests/test_bewertung_urteil.py"
|
||||
|
||||
[python_je_datei]
|
||||
"tests/test_broker_ibkr.py" = "/home/claude/venvs/trading-ibkr/bin/python"
|
||||
|
||||
[[mutant]]
|
||||
id = "D1"
|
||||
datei = "trading/bewertung/daten.py"
|
||||
aus = '''
|
||||
return ort.weekday() < 5 and SPERRE_VON <= ort.time() < SPERRE_BIS
|
||||
'''
|
||||
wird = '''
|
||||
return False
|
||||
'''
|
||||
rot_bei = ["DA::test_montag_bis_freitag_von_neun_bis_halb_fuenf_new_york_wird_nicht_abgerufen"]
|
||||
hinweis = "frei, wird nur ausgegeben"
|
||||
```
|
||||
|
||||
**Oberste Ebene**, erlaubt sind genau diese Schlüssel:
|
||||
|
||||
| Schlüssel | Pflicht | Typ | Bedeutung |
|
||||
|---|---|---|---|
|
||||
| `format` | ja | Ganzzahl, genau `1` | Formatfassung |
|
||||
| `python` | ja | absoluter Pfad | Interpreter für pytest; `~` ist ein Befund |
|
||||
| `ohne` | nein | Liste von relativen Pfaden | darf es in der Wurzel nicht geben; `.git` gilt immer zusätzlich |
|
||||
| `kuerzel` | nein | Tabelle Name → relativer Pfad einer Testdatei | Abkürzung in `rot_bei` |
|
||||
| `python_je_datei` | nein | Tabelle relativer Pfad → absoluter Pfad | anderer Interpreter für diese Testdatei |
|
||||
| `mutant` | ja | Liste von Tabellen, nicht leer | die Mutanten |
|
||||
|
||||
**Je Mutant**, erlaubt sind genau diese Schlüssel:
|
||||
|
||||
| Schlüssel | Pflicht | Typ | Bedeutung |
|
||||
|---|---|---|---|
|
||||
| `id` | ja | nicht leere Zeichenkette, eindeutig | Name in der Ausgabe und in `--nur` |
|
||||
| `datei` | ja | relativer Pfad | Datei in der Wurzel; kein `..`, nicht absolut, keine Verknüpfung, muss existieren |
|
||||
| `aus` | ja | nicht leere Zeichenkette | ganze Zeilen, die ersetzt werden |
|
||||
| `wird` | ja | Zeichenkette | ganze Zeilen, die dafür eingesetzt werden; leer heißt: Zeilen entfernen |
|
||||
| `rot_bei` | ja | nicht leere Liste von Zeichenketten | Tests, die fallen müssen |
|
||||
| `hinweis` | nein | Zeichenkette | wird in Ausgabe und Bericht mitgeführt |
|
||||
|
||||
Jeder unbekannte Schlüssel ist ein Befund (Positivliste). So fällt `rotbei` statt `rot_bei` auf.
|
||||
|
||||
**Zeilen aus `aus` und `wird`:** Ein einzelnes `\n` am Ende wird entfernt (die schließenden `'''` auf eigener Zeile). Eine leere Zeichenkette ergibt null Zeilen. Sonst wird an `\n` getrennt. Ein `\r` bleibt Teil der Zeile.
|
||||
|
||||
**Einträge in `rot_bei`:** `K::name` mit `K` aus `kuerzel` wird zu `<pfad>::name`. Sonst muss der Eintrag mit einem relativen Pfad auf `.py` beginnen, gefolgt von `::`. Alles andere ist ein Befund (unbekanntes Kürzel). Parametrisierte Tests stehen mit ihrer vollen Kennung, etwa `U::test_knapp_hinter_jeder_grenze_faellt_genau_dieses_kriterium[max_drawdown-0.1501-max_drawdown]`.
|
||||
|
||||
## 3 Ablauf
|
||||
|
||||
Aufruf in WSL, mit einem Python ab 3.11 (nur Standardbibliothek):
|
||||
|
||||
```
|
||||
python mutanten_lauf.py --wurzel <kopie> --mutanten <mutanten.toml> [--nur ID,ID,…] [--nur-muster] [--bericht <datei.json>] [--zeitlimit <sekunden>]
|
||||
```
|
||||
|
||||
`--zeitlimit` gilt je pytest-Aufruf, Vorgabe 900. `--nur-muster` fährt nur die Schritte 1 und 2.
|
||||
|
||||
1. **Vorbedingungen und Format.** Alle Befunde sammeln, nicht beim ersten abbrechen: Wurzel fehlt oder ist kein Ordner; `.git` in der Wurzel; ein Pfad aus `ohne` existiert; TOML nicht lesbar; jede Formatregel aus Abschnitt 2; ein Interpreter fehlt oder ist nicht ausführbar; `--nur` nennt eine unbekannte ID. Gibt es einen Befund, endet der Lauf hier mit Exit 2.
|
||||
2. **Muster.** Für jeden ausgewählten Mutanten: Datei als Bytes lesen, streng als UTF-8 dekodieren (sonst Befund), an `\n` trennen, die Zeilenfolge aus `aus` an jeder Startposition suchen (überlappende Treffer zählen mit). Genau ein Treffer ist gut. Sonst Status `MUSTER` mit der Trefferzahl; enthält die Datei ein `\r`, steht der Hinweis „Datei hat CR“ dabei. `aus` gleich `wird` ist ein Formatbefund aus Schritt 1 („wirkungslos“).
|
||||
3. **Grundlinie.** Alle genannten Tests der ausgewählten Mutanten ohne Mutant fahren, gruppiert nach Interpreter. Jeder muss `GRUEN` sein. Fehlt einer, ist er abgewählt (etwa durch den Marker `integration`), übersprungen oder rot, ist das ein Befund mit der Meldung, und der Lauf endet mit Exit 2, bevor ein Mutant läuft. Bricht pytest für einen Interpreter ab (Rückgabewert 3 oder 4), steht statt der Zeilen je Test ein Befund für diesen Interpreter (unten, „Abbruch von pytest“).
|
||||
4. **Mutanten** in der Reihenfolge der Datei, `MUSTER`-Fälle ausgelassen. Je Mutant:
|
||||
- SHA-256 der Originalbytes merken, die Zeilen ersetzen, mit `\n` zusammenfügen, als UTF-8-Bytes schreiben.
|
||||
- Die genannten Tests fahren, gruppiert nach Interpreter.
|
||||
- Im `finally` die Originalbytes zurückschreiben und den SHA-256 neu lesen. Weicht er ab, endet der ganze Lauf sofort mit dem Befund „Rücksetzen gescheitert“ und Exit 2.
|
||||
5. **Zusammenfassung** und Exit-Code.
|
||||
|
||||
**pytest-Aufruf:** `<python> -m pytest -q -p no:cacheprovider -p _mutanten_lauf_plugin <konfiguration…> <knoten…>` mit `cwd` = Wurzel. Das Plugin ist eine kleine Datei, die das Skript in einen eigenen Temp-Ordner schreibt; der Ordner kommt vorn in `PYTHONPATH`. Das Plugin schreibt je Bericht eine JSON-Zeile in die Datei aus der Umgebungsvariable `MUTANTEN_LAUF_ERGEBNIS`:
|
||||
|
||||
- `pytest_runtest_logreport`: `{"art": "test", "knoten": nodeid, "phase": when, "ergebnis": outcome, "xfail": hat wasxfail, "meldung": erste Zeile}`
|
||||
- `pytest_collectreport` nur bei `failed`: `{"art": "sammeln", "knoten": nodeid, "meldung": erste Zeile}`
|
||||
- `pytest_deselected`: je Element `{"art": "abgewaehlt", "knoten": nodeid}`
|
||||
|
||||
Die Meldung ist `longrepr.reprcrash.message`, wenn es das gibt, sonst die erste nicht leere Zeile von `str(longrepr)`, gekürzt auf 300 Zeichen. Das Plugin schreibt jede Zeile sofort und mit `flush`.
|
||||
|
||||
**Konfiguration (seit 2026-10-06):** Ohne Vorgabe sucht pytest seine Konfiguration vom gemeinsamen Vorfahren der Testpfade aufwärts bis zur Wurzel des Dateisystems. Liegt die Kopie unter dem Windows-Temp, fand pytest dort eine fremde `pyproject.toml` mit `addopts = "--new-first --failed-first"`; die Optionen brauchen den Cache, den `-p no:cacheprovider` abschaltet, pytest brach mit Rückgabewert 4 ab, und der Läufer meldete jeden Test der Grundlinie als `FEHLT` (Plankritik 2 von C1 im Projekt After Install, 2026-10-05). Seitdem bestimmt der Läufer je Aufruf, was pytest selbst wählen würde, sucht aber nie oberhalb der Wurzel:
|
||||
|
||||
- Die Wurzel wird zuerst mit `realpath` aufgelöst. pytest rechnet vom Arbeitsordner aus, den der Kernel ohne Verknüpfungen meldet; über einen Symlink angegeben, wurden die Knotennamen sonst zu `::test_…` (Prüfung vom 2026-10-06, Treiber-Fall 41).
|
||||
- Reihenfolge der Namen und Bedingungen wie in pytest 9.1.1: `pytest.toml`, `.pytest.toml`, `pytest.ini`, `.pytest.ini` immer; `pyproject.toml` mit `[tool.pytest]` oder `[tool.pytest.ini_options]`; `tox.ini` mit `[pytest]`; `setup.cfg` mit `[tool:pytest]`; ohne Treffer die erste `pyproject.toml` überhaupt. Erst vom gemeinsamen Vorfahren aus, ohne Treffer und ohne `setup.py` dazwischen noch einmal von jedem Testordner aus. Die Abschnittszeile in `tox.ini` und `setup.cfg` erkennt der Läufer wie `_parseline` in iniconfig 2.3.0: `[` in Spalte 0, Kommentare nach `#` und `;` entfernt, dann `]` am Ende. Ein Pfad, dessen Name zu lang ist, gilt wie bei `safe_exists` in pytest als nicht vorhanden.
|
||||
- Gefunden: `-c <datei>`. pytest setzt `rootdir` und `confcutdir` dann selbst auf ihren Ordner, wie bei der eigenen Suche.
|
||||
- Nicht gefunden: `-c <leere pytest.ini im Plugin-Ordner> --rootdir <r> --confcutdir <r>`, `r` ist der Ordner mit `setup.py` oder sonst die Wurzel. Ohne `--confcutdir` läge `confcutdir` im Plugin-Ordner, und pytest lüde `conftest.py`-Dateien oberhalb der Wurzel.
|
||||
- Die Kopfzeilen `pytest-konfiguration (Grundlinie, <python>): …` nennen je Interpreter, was für seinen Aufruf in der Grundlinie gilt.
|
||||
- Gegen pytest selbst geprüft wird das mit `mutanten_lauf_probe_konfig.py` (Treiber-Fall 43): 26 Aufbauten, je `pytest_konfig_bestimmen` gegen `determine_setup` und ein echtes `pytest --co` ohne `-c` gegen einen Lauf mit den Argumenten des Läufers unter fremder Konfiguration. Die Probe sichert ihre Voraussetzung mit: Ohne `-c` endet pytest mit 0 (außer bei fehlenden Knoten), und wo der Aufbau ein `conftest.py` hat, ist eines geladen.
|
||||
- Benannte Lücken: Eine `tox.ini` oder ein `setup.cfg`, die iniconfig nicht lesen kann, übergeht der Läufer, pytest bricht dort mit 4 ab; eine nicht lesbare `pyproject.toml` oder `pytest.toml` gilt dagegen als Treffer, damit pytest den Fehler selbst meldet. Ein `setup.cfg` mit `[pytest]`, bei pytest ein Fehler, gilt hier als keine Konfiguration. Ohne Konfiguration in der Wurzel ist `config.inipath` die leere Datei; ini-Werte vom Typ `paths`, die über `-o` oder `PYTEST_ADDOPTS` kommen, gelten dann relativ zum Plugin-Ordner. Liegt `rootdir` unter der Wurzel (Konfiguration oder `setup.py` in einem Unterordner der Tests), bildet pytest die Knotennamen relativ dazu, und `rot_bei` passt nicht mehr: Die Grundlinie meldet `FEHLT`, die Kopfzeile nennt die gefundene Datei; heute betrifft das kein Projekt im Vault. Zeilen trennt der Läufer wie iniconfig mit `splitlines`, also auch an Seitenvorschub und Unicode-Zeilentrennern.
|
||||
|
||||
**Abbruch von pytest (seit 2026-10-06):** Endet ein pytest-Aufruf mit Rückgabewert 3 (`INTERNAL_ERROR`) oder 4 (`USAGE_ERROR`, auch bei einem Testknoten, den es nicht gibt), hält der Läufer die letzten 20 nicht leeren Zeilen von stdout und stderr fest. In der Grundlinie wird daraus je Interpreter ein Befund `Grundlinie: pytest-Bedienfehler (Rueckgabewert 4) mit <python>; Ende der Ausgabe: …` (bei 3: `pytest-Abbruch mit internem Fehler`) statt einer `FEHLT`-Zeile je Test; der Bericht führt die Tests mit `FEHLT` und der Meldung „pytest abgebrochen, siehe Befunde“. Unter einem Mutanten tragen die Tests ohne Bericht und ohne Sammelfehler eine kurze Fassung als Meldung, die Ursache vorn, damit sie in die 120 Zeichen der Mutantenzeile passt: `pytest-Bedienfehler (Rueckgabewert 4): <zeile>`. Die Zeile ist bei 4 die erste mit „error“ außer der Gebrauchszeile `ERROR: usage:` von argparse, bei 3 die letzte mit `INTERNALERROR>` (danach steht noch die Summenzeile `no tests ran …`, Treiber-Fall 46), ohne Treffer die letzte überhaupt. Die lange Fassung steht im Bericht beim Test unter `ausgabe`. Diese Tests machen den Mutanten wie bisher `UNKLAR`. Ein Sammelfehler bleibt `ROT`, auch wenn pytest dabei mit 4 endet: Ein Knoten in einer Datei, die sich nicht sammeln lässt, ergibt zusätzlich `found no collectors for …` (Treiber-Fall 11, am 2026-10-06 an einer ersten Fassung gesehen, die jeden Abbruch als `UNKLAR` zählte).
|
||||
|
||||
**Auswertung je Test:** eine Phase `failed` → `ROT` (bei `xfail` nicht); sonst `skipped` in einer Phase oder `xfail` → `UEBERSPRUNGEN`; sonst `call` mit `passed` → `GRUEN`; abgewählt → `ABGEWAEHLT`; gar kein Bericht → `FEHLT`. Unter einem Mutanten zählt `FEHLT` als `ROT` mit der Meldung „Sammelfehler: …“, wenn es für seine Testdatei (oder einen übergeordneten Knoten) einen `sammeln`-Bericht gibt. In der Grundlinie ist jedes `FEHLT` ein Befund, außer pytest ist für diesen Interpreter abgebrochen; dann steht der eine Befund aus „Abbruch von pytest“.
|
||||
|
||||
**Zeitlimit:** Prozess mit `start_new_session=True` starten; bei Überschreitung die ganze Prozessgruppe mit `SIGKILL` beenden. Der Mutant bekommt `ZEIT`, das Rücksetzen läuft trotzdem.
|
||||
|
||||
## 4 Status, Ausgabe, Exit-Codes
|
||||
|
||||
| Status je Mutant | Bedeutung |
|
||||
|---|---|
|
||||
| `OK` | jeder genannte Test `ROT` |
|
||||
| `LUECKE` | mindestens ein genannter Test `GRUEN` oder `UEBERSPRUNGEN` |
|
||||
| `UNKLAR` | ein Test `FEHLT` ohne Sammelfehler, oder pytest endete ohne auswertbare Ergebnisdatei |
|
||||
| `ZEIT` | Zeitlimit überschritten |
|
||||
| `MUSTER` | Muster nicht genau einmal gefunden, nichts gefahren |
|
||||
|
||||
**Modus-Zeile (seit 2026-09-28, Nachbesserung N5):** Der Wrapper `mutanten_lauf_kopie.sh` schreibt vor dem eigentlichen Lauf eine eigene Zeile, unabhängig von `mutanten_lauf.py` selbst: `modus: voll` oder `modus: muster (nur Musterpruefung, kein Mutant gefahren)`. Damit weiß eine Linse, die nur die Rohausgabe sieht, ob überhaupt ein Mutant gefahren ist, bevor sie ein `OK` bewertet.
|
||||
|
||||
**SIGTERM (seit 2026-09-28, Nachbesserung N4):** `mutanten_lauf.py` registriert einen eigenen Handler für `SIGTERM`, der eine Ausnahme auslöst, damit die laufenden `finally`-Blöcke greifen: der zuletzt angewendete Mutant wird per SHA-Probe zurückgesetzt, ein laufender pytest-Prozess wird über seine Prozessgruppe beendet, und die Temp-Ordner (`mutanten_lauf_home_*`, `mutanten_lauf_plugin_*`) sowie die Ergebnisdatei werden geräumt. Vor dem Ende steht die Zeile `ABBRUCH: SIGTERM, Mutant <id> zurueckgesetzt` (oder ohne den Mutant-Teil, wenn beim Signal noch keiner lief). Der Exit-Code ist in diesem Fall 3. Geprüft durch Treiber-Fall 35.
|
||||
|
||||
Ausgabe auf stdout, UTF-8, je Mutant eine Zeile: `D1 OK DA::test_x=ROT(<meldung, 120 Zeichen>) …`. Tests erscheinen in der Kurzform aus `rot_bei`. Ein vorhandener `hinweis` folgt in der nächsten Zeile, eingerückt. Oben stehen Wurzel, Mutantendatei, Anzahl gesamt und ausgewählt und je Interpreter die Python-Version. Unten eine gerechnete Zusammenfassung: `ergebnis: <n> Mutanten, <a> OK, <b> LUECKE, <c> UNKLAR, <d> ZEIT, <e> MUSTER`.
|
||||
|
||||
`--bericht` schreibt dasselbe als JSON: `{"wurzel", "mutanten_datei", "befunde": [...], "grundlinie": {knoten: {ergebnis, meldung}}, "mutanten": [{"id", "datei", "status", "hinweis", "treffer", "tests": [{"eintrag", "knoten", "ergebnis", "meldung", "ausgabe" (nur bei einem Abbruch von pytest)}]}], "zusammenfassung": {...}}`. Der Bericht wird auch bei einem Abbruch geschrieben, mit allem, was bis dahin feststand.
|
||||
|
||||
| Exit | Bedeutung |
|
||||
|---|---|
|
||||
| 0 | Grundlinie grün, jeder ausgewählte Mutant `OK` (mit `--nur-muster`: jedes Muster genau einmal) |
|
||||
| 1 | mindestens ein Mutant nicht `OK` |
|
||||
| 2 | Vorbedingung, Format, Grundlinie oder Rücksetzen gescheitert |
|
||||
| 3 | unerwarteter Abbruch; die bis dahin gesammelten Befunde und Ergebnisse werden trotzdem ausgegeben |
|
||||
|
||||
Der Hauptablauf steht in `try`. Ein Traceback ist ein Befund und wird als solcher gemeldet ([[AI/Regelwerk/pruefwerkzeuge|pruefwerkzeuge.md]], Abschnitt 4).
|
||||
|
||||
## 5 Abdichtung
|
||||
|
||||
Jeder pytest-Aufruf bekommt eine Kopie der Umgebung mit diesen Änderungen:
|
||||
|
||||
- `HOME` zeigt auf einen frischen, leeren Temp-Ordner. Zugänge, die unter `~/.config` liegen (Scharfschalter, IBKR-Live-Zugang, Scalable), sind damit nicht erreichbar.
|
||||
- `http_proxy`, `https_proxy`, `all_proxy` und dieselben in Großschrift zeigen auf `http://127.0.0.1:9`; `no_proxy` und `NO_PROXY` werden entfernt. Bibliotheken, die Proxy-Variablen lesen, laufen damit gegen einen geschlossenen Port.
|
||||
- `PYTHONDONTWRITEBYTECODE=1`, `COLUMNS=300`, `MUTANTEN_LAUF_ERGEBNIS=<temp-datei>`, `PYTHONPATH` mit dem Plugin-Ordner vorn.
|
||||
|
||||
**Netzsperre (seit 2026-09-28, Nachbesserung N1):** Der Wrapper `mutanten_lauf_kopie.sh` startet `mutanten_lauf.py` selbst unter `unshare --map-current-user --net`, nicht erst den einzelnen pytest-Aufruf. Gemessen in WSL Debian als Benutzer `claude` (util-linux 2.41.5): Die UID bleibt innen wie außen 1000, es gibt nur die Schnittstelle `lo`, `connect` auf eine Adresse außerhalb von localhost endet mit `OSError` (errno 101) oder `gaierror`. Nicht `-r` (macht UID 0, könnte Rechte-Tests der Mutanten selbst verfälschen). Fehlt `unshare` ohne Netz, bricht der Wrapper vorher mit `MUTANTEN_LAUF_FEHLER: unshare ohne Netz nicht moeglich` ab, statt einen ungesperrten Lauf durchzulassen. **Seit 2026-10-09 (Paket RB3 der Trading Plattform):** `lo` ist im Namensraum oben. Der Wrapper startet `unshare … --keep-caps`, fährt `lo` mit `ip` hoch und geht über `setpriv` (alle vererbbaren und ambienten Fähigkeiten abgeworfen) per `exec` in den Läufer über. Eine Vorprüfung verlangt `ip`, `setpriv`, `python3`, ein nutzbares `lo` und `CapEff` und `CapAmb` gleich 0, sonst `MUTANTEN_LAUF_FEHLER` und Exit 2. Eine Verbindung auf `127.0.0.1` erreicht damit Dienste im Namensraum, nicht die des Rechners; nach außen bleibt es bei errno 101. Treiber: `mutanten_lauf_kopie_treiber.py` (W1 bis W13), Plan `docs/plans/2026-10-09-mutanten-kopie-loopback.md` und seine Nachbesserung 1. Die Fähigkeiten prüft der Wrapper nur an der Probe (`CapEff`, `CapAmb`), nicht am Läufer. Weil die Vorprüfung vor der Suche nach `mutanten.toml` steht, endet er ohne `ip`, `setpriv`, `python3` oder nutzbares `lo` auch ohne `mutanten.toml` mit Exit 2 statt mit `KEINE_MUTANTEN_TOML`.
|
||||
|
||||
**Was das nicht abdichtet:** einen Unterprozess mit absolutem Pfad (so kam der Vorfall vom 2026-09-26 zustande), einen Socket ohne Proxy, einen Client mit `trust_env=False` oder mit eigenem Transport, Dateien mit absolutem Pfad außerhalb von `HOME`. Die eigentliche Sperre bleibt die Netzsperre der Testdateien selbst und eine Kopie ohne Geheimnisdateien (`ohne`).
|
||||
|
||||
Alle Temp-Ordner liegen außerhalb der Wurzel und werden am Ende entfernt, auch nach einem Abbruch.
|
||||
|
||||
## 6 Angriffsliste für den Treiber `mutanten_lauf_treiber.py`
|
||||
|
||||
Der Treiber baut in einem Temp-Ordner ein kleines Projekt (ein Modul, eine Testdatei, `pyproject.toml` mit `[tool.pytest.ini_options]`; für die Fälle 36 bis 42 je ein eigenes als Unterordner `k/` eines Rahmen-Ordners), schreibt je Fall eine `mutanten.toml`, startet den Läufer als Unterprozess und prüft Exit-Code und ein Meldungsstück. **Zuerst die Grundlinie:** Der saubere Fall muss Exit 0 und nur `OK` liefern, sonst misst der Rest nichts. Nach **jedem** Fall prüft der Treiber, dass jede Datei des kleinen Projekts byte-gleich mit dem Ausgangsstand ist.
|
||||
|
||||
| Nr | Fall | erwartet |
|
||||
|---|---|---|
|
||||
| 1 | sauber, zwei Mutanten, beide getötet | Exit 0, zweimal `OK` |
|
||||
| 2 | ein Test bleibt grün, als erster Eintrag in `rot_bei` | Exit 1, `LUECKE` |
|
||||
| 3 | dasselbe als letzter Eintrag | Exit 1, `LUECKE` |
|
||||
| 4 | der lückenhafte Mutant an erster und an letzter Position der Datei | je Exit 1, `LUECKE` beim richtigen Mutanten |
|
||||
| 5 | `aus` kommt nicht vor | Exit 1, `MUSTER` mit Zahl 0 |
|
||||
| 6 | `aus` kommt zweimal vor | Exit 1, `MUSTER` mit Zahl 2 |
|
||||
| 7 | `aus` ist nur ein Teil einer Zeile (andere Einrückung) | Exit 1, `MUSTER` mit Zahl 0 |
|
||||
| 8 | Zieldatei mit CRLF | Exit 1, `MUSTER` mit Hinweis auf CR |
|
||||
| 9 | `aus` über zwei Zeilen, getötet | Exit 0, `OK` |
|
||||
| 10 | `wird = ""` entfernt eine Zeile, getötet | Exit 0, `OK` |
|
||||
| 11 | Mutant erzeugt einen Syntaxfehler, das Einsammeln scheitert | Exit 0, `OK` mit „Sammelfehler“ |
|
||||
| 12 | Mutant erzeugt eine Endlosschleife, `--zeitlimit 5` | Exit 1, `ZEIT`, Datei danach byte-gleich |
|
||||
| 13 | ein Test ist in der Grundlinie rot | Exit 2 |
|
||||
| 14 | ein Test ist in der Grundlinie übersprungen | Exit 2 |
|
||||
| 15 | ein Test ist per Marker abgewählt | Exit 2, Meldung nennt „abgewählt“ oder `ABGEWAEHLT` |
|
||||
| 16 | ein Testname existiert nicht | Exit 2 |
|
||||
| 17 | `.git` als Ordner in der Wurzel; dasselbe als Datei | je Exit 2 |
|
||||
| 18 | ein Pfad aus `ohne` existiert | Exit 2 |
|
||||
| 19 | unbekannter Schlüssel im Mutanten (`rotbei`) und auf oberster Ebene | je Exit 2 |
|
||||
| 20 | doppelte `id` | Exit 2 |
|
||||
| 21 | `rot_bei` leer; `rot_bei` als Zeichenkette statt Liste | je Exit 2 |
|
||||
| 22 | `datei` mit `..`; absolut; fehlt | je Exit 2 |
|
||||
| 23 | unbekanntes Kürzel in `rot_bei` | Exit 2 |
|
||||
| 24 | `aus` gleich `wird` | Exit 2 |
|
||||
| 25 | `aus` leer; `format = 2`; `format` fehlt; `python` relativ; `python` existiert nicht | je Exit 2 |
|
||||
| 26 | TOML-Syntaxfehler | Exit 2 |
|
||||
| 27 | keine Mutanten | Exit 2 |
|
||||
| 28 | `--nur` mit unbekannter ID | Exit 2 |
|
||||
| 29 | `--nur` wählt einen von zwei Mutanten | Exit 0, nur dieser läuft |
|
||||
| 30 | `--nur-muster` bei sauberem Fall; bei Fall 6 | Exit 0 ohne pytest-Lauf; Exit 1 |
|
||||
| 31 | Zieldatei ist kein UTF-8 | Exit 1 oder 2, Meldung nennt die Kodierung |
|
||||
| 32 | Abdichtung: ein Test prüft, dass `HOME` nicht das echte ist und `https_proxy` gesetzt ist | Grundlinie grün, Exit 0 |
|
||||
| 33 | `--bericht` schreibt gültiges JSON mit den Status aus Fall 2 | Datei lesbar, Status stimmt |
|
||||
| 34 | `python_je_datei` setzt für eine Testdatei einen zweiten Interpreter (derselbe Pfad über einen anderen Namen, etwa eine Verknüpfung im Temp) | Exit 0 |
|
||||
| 35 | ein Mutant hängt, der Läufer bekommt nach drei Sekunden SIGTERM (eigenes `TMPDIR`), seit 2026-09-28 | Zeile `ABBRUCH: SIGTERM`, kein Prozess mit `HOME` im Fall-Tmpdir, keine `mutanten_lauf_*`-Reste, Zieldatei byte-gleich |
|
||||
| 36 | Wurzel ohne eigene Konfiguration, darüber eine fremde `pyproject.toml` mit `addopts = "--new-first --failed-first"` (der Fall der Plankritik 2 von C1), seit 2026-10-06 | Exit 0, zweimal `OK`, Kopfzeile „keine in der Wurzel, leere Datei“ |
|
||||
| 37 | Wurzel mit eigener `pyproject.toml` (Marker `integration` abgewählt), darüber dieselbe fremde | sauberer Fall Exit 0 mit Kopfzeile `k/pyproject.toml (aus der Wurzel)`; Test mit Marker in der Grundlinie Exit 2 und `ABGEWAEHLT`, die eigene Konfiguration gilt also |
|
||||
| 38 | eigene Konfiguration mit `addopts = "--gibt-es-nicht"` | Exit 2, `pytest-Bedienfehler (Rueckgabewert 4)` und `--gibt-es-nicht` aus der pytest-Ausgabe, keine Zeile `ist FEHLT` |
|
||||
| 39 | Wurzel ohne eigene Konfiguration, darüber ein `conftest.py`, das beim Laden abbricht | Exit 0, zweimal `OK`; prüft `--confcutdir` |
|
||||
| 40 | ein Mutant schreibt in die eigene `pyproject.toml` `addopts = "--gibt-es-nicht"` | Exit 1; die Mutantenzeile selbst trägt `UNKLAR`, `pytest-Bedienfehler (Rueckgabewert 4)` und `--gibt-es-nicht`; im Bericht steht beim Test `ausgabe` mit „Ende der Ausgabe:“ |
|
||||
| 41 | Wurzel über einen Symlink angegeben (`link/k` → `echt/k`), seit der Nachbesserung vom 2026-10-06 | Exit 0, zweimal `OK` |
|
||||
| 42 | ein `conftest.py` der Wurzel wirft in `pytest_collection_modifyitems` | Exit 2, `pytest-Abbruch mit internem Fehler (Rueckgabewert 3)` und der Text der Ausnahme, keine Zeile `ist FEHLT` |
|
||||
| 43 | `mutanten_lauf_probe_konfig.py`: 26 Aufbauten gegen `determine_setup` und gegen echte `pytest --co`-Läufe, mit Selbstprüfung der Voraussetzung | Exit 0, `abweichungen: 0` |
|
||||
| 44 | zwei Interpretergruppen (`python_je_datei`), nur in einer ein Knoten, den es nicht gibt; diese Gruppe einmal an erster, einmal an letzter Stelle | je Exit 2, genau ein Befund mit dem Interpreter dieser Gruppe, keiner für die andere, keine Zeile `ist FEHLT`; genau zwei Kopfzeilen, je eine je Interpreter |
|
||||
| 45 | ein Knoten mit einem Dateinamen über 255 Zeichen | Exit 2 mit einem Befund `Grundlinie: pytest-…`, kein `unerwarteter Abbruch` |
|
||||
| 46 | ein Mutant lässt ein `conftest.py` der Wurzel in `pytest_collection_modifyitems` werfen | Exit 1, die Mutantenzeile trägt `UNKLAR`, `pytest-Abbruch mit internem Fehler (Rueckgabewert 3): ` und den Text der Ausnahme; im Bericht `ausgabe` mit „Ende der Ausgabe:“ |
|
||||
|
||||
Der Treiber gibt je Fall eine Zeile aus (`Fall 5 ok` oder `Fall 5 FEHLER: erwartet …, bekommen …`) und am Ende die gerechnete Zahl der Fehler. Exit 0 nur, wenn alle Fälle wie erwartet ausgehen und jedes Mal byte-gleich zurückgesetzt wurde.
|
||||
|
||||
## 7 Abbruchbedingung
|
||||
|
||||
Eine Runde mit dem Prüfer endet, wenn die Befunde nur noch Formulierungen betreffen. Der erste echte Einsatz ist B1 (74 Mutanten auf einer Kopie von `b1-mat-kopie`).
|
||||
@@ -0,0 +1,158 @@
|
||||
#!/usr/bin/env bash
|
||||
# Mutantenlauf auf einer Kopie der Kopie, gedacht fuer den Hilfsagenten der Plankritik
|
||||
# (.claude/workflows/plankritik.js). Nie in der uebergebenen Kopie selbst arbeiten: Die Linsen
|
||||
# fahren dort gleichzeitig Tests, und mutanten_lauf.py setzt Dateien zwischenzeitlich zurueck.
|
||||
#
|
||||
# Aufruf: bash mutanten_lauf_kopie.sh <kopie als Windows-Pfad> <planordner als Windows-Pfad> <modus>
|
||||
# modus ist "voll" oder "muster". Der ganze Lauf ist auf BUDGET_S Sekunden begrenzt (Vorgabe 540),
|
||||
# weil das Bash-Werkzeug eines Hilfsagenten nach hoechstens zehn Minuten endet; eine ueberschrittene
|
||||
# Grenze meldet die Zeile ZEITGRENZE, die bis dahin gefahrenen Mutanten stehen in der Ausgabe.
|
||||
# Beide Pfade sind Windows-Pfade (auch Kurznamen wie PASCAL~1),
|
||||
# die dieses Skript selbst mit wslpath in WSL-Pfade umwandelt.
|
||||
#
|
||||
# Nachbesserung 2026-09-28 (Pruefbericht vom selben Tag, Befunde 1 bis 4, 7):
|
||||
# - Netzsperre: mutanten_lauf.py laeuft in einem eigenen Netz-Namensraum
|
||||
# (unshare --map-current-user --net). Gemessen in WSL Debian als Benutzer claude
|
||||
# (util-linux 2.41.5): UID innen bleibt 1000 wie aussen, nur Schnittstelle lo, jeder
|
||||
# Verbindungsversuch nach aussen (etwa 127.0.0.1:8144, wo das Dashboard lauscht) scheitert.
|
||||
# Fehlt unshare ohne Netz, bricht das Skript vorher ab statt einen ungesperrten Mutantenlauf
|
||||
# still durchzulassen.
|
||||
# - Ungepufferte Ausgabe: python3 -u, damit bei einer Zeitgrenze die bis dahin gefahrenen
|
||||
# Mutanten wirklich in der Ausgabe stehen.
|
||||
# - Zeitlimit je Mutant: ZEITLIMIT_S (Vorgabe 120 s, kleiner als BUDGET_S) geht als --zeitlimit
|
||||
# an mutanten_lauf.py; B1 mit 74 Mutanten brauchte 168 s gesamt, rund 2 s je Mutant.
|
||||
# - Modus-Zeile vor dem Lauf ("modus: voll" / "modus: muster (...)"), damit die Linse weiss, ob
|
||||
# ueberhaupt ein Mutant gefahren ist.
|
||||
# - Aufraeumen bei SIGTERM: timeout -k 15 schickt nach der Frist noch ein SIGKILL hinterher, falls
|
||||
# mutanten_lauf.py nach dem SIGTERM nicht rechtzeitig endet; sein eigener SIGTERM-Handler setzt
|
||||
# die Zieldatei vorher zurueck und raeumt seine Temp-Ordner auf.
|
||||
# - chmod 700 auf die Kopie der Kopie direkt nach dem Kopieren.
|
||||
#
|
||||
# Nachbesserung 2026-10-09 (Paket RB3 der Trading Plattform, Plan
|
||||
# docs/plans/2026-10-09-mutanten-kopie-loopback.md):
|
||||
# - lo im Netz-Namensraum: Im frischen Namensraum war lo aus; jeder Test mit einem Server auf
|
||||
# 127.0.0.1 scheiterte mit Errno 101, auch ohne Mutant. Jetzt startet unshare zusaetzlich mit
|
||||
# --keep-caps, eine sh faehrt lo mit ip hoch, und setpriv wirft danach alle vererbbaren und
|
||||
# ambienten Faehigkeiten ab und geht per exec in python3 ueber. Gemessen vom Koordinator am
|
||||
# 2026-10-09 in WSL Debian als Benutzer claude: uid 1000 wie aussen, CapEff und CapAmb 0,
|
||||
# Verbindung ueber 127.0.0.1 gelingt. 127.0.0.1 ist das lo des Namensraums, nicht das des
|
||||
# Rechners; ein Dienst des Rechners auf 127.0.0.1 bleibt unerreichbar (Treiber-Fall W8).
|
||||
# - exec-Kette: unshare, sh, setpriv und python3 sind derselbe Prozess, das SIGTERM von timeout
|
||||
# trifft den Laeufer selbst; keine Zwischenschale bleibt stehen (Treiber-Fall W6).
|
||||
# - Vorpruefung vor dem Kopieren: Fehlt ip, setpriv oder python3, kommt lo in einer Probe im
|
||||
# eigenen Namensraum nicht hoch, gelingt dort keine Verbindung ueber 127.0.0.1 oder hat die
|
||||
# Probe nach setpriv noch CapEff oder CapAmb ungleich 0, endet das Skript mit
|
||||
# MUTANTEN_LAUF_FEHLER und Exit 2, ohne EXIT-Zeile. Die Faehigkeiten prueft das Skript nur an
|
||||
# dieser Probe und nur CapEff und CapAmb, nicht am Laeufer selbst; alle vier Mengen am Laeufer
|
||||
# prueft Treiber-Fall W3. Die Vorpruefung steht vor der Suche nach mutanten.toml: Scheitert
|
||||
# sie, endet das Skript auch fuer einen Planordner ohne mutanten.toml mit Exit 2 statt mit
|
||||
# KEINE_MUTANTEN_TOML.
|
||||
# - Hauptaufruf: Kommt lo dort nicht hoch, endet das Skript nach der Modus-Zeile mit
|
||||
# MUTANTEN_LAUF_FEHLER, Exit 2 und EXIT-Zeile, ohne den Laeufer zu starten. Ein Lauf ohne
|
||||
# Netzsperre oder ohne lo ist nie die Ausweichloesung.
|
||||
# - Treiber: mutanten_lauf_kopie_treiber.py, Faelle W1 bis W13 (W10 bis W13 seit Nachbesserung 1,
|
||||
# docs/plans/2026-10-09-mutanten-kopie-loopback-nachbesserung-1.md).
|
||||
set -u
|
||||
|
||||
KOPIE_WIN="${1:-}"
|
||||
PLANORDNER_WIN="${2:-}"
|
||||
MODUS="${3:-}"
|
||||
|
||||
if [ -z "$KOPIE_WIN" ] || [ -z "$PLANORDNER_WIN" ] || [ -z "$MODUS" ]; then
|
||||
echo "MUTANTEN_LAUF_FEHLER: Aufruf braucht <kopie> <planordner> <modus>"
|
||||
exit 2
|
||||
fi
|
||||
|
||||
if [ "$MODUS" != "voll" ] && [ "$MODUS" != "muster" ]; then
|
||||
echo "MUTANTEN_LAUF_FEHLER: modus muss voll oder muster sein, nicht $MODUS"
|
||||
exit 2
|
||||
fi
|
||||
BUDGET_S="${BUDGET_S:-540}"
|
||||
ZEITLIMIT_S="${ZEITLIMIT_S:-120}"
|
||||
|
||||
if ! [ "$ZEITLIMIT_S" -lt "$BUDGET_S" ] 2>/dev/null; then
|
||||
echo "MUTANTEN_LAUF_FEHLER: ZEITLIMIT_S ($ZEITLIMIT_S) muss kleiner sein als BUDGET_S ($BUDGET_S)"
|
||||
exit 2
|
||||
fi
|
||||
|
||||
if ! unshare --map-current-user --net true 2>/dev/null; then
|
||||
echo "MUTANTEN_LAUF_FEHLER: unshare ohne Netz nicht moeglich"
|
||||
exit 2
|
||||
fi
|
||||
|
||||
# Vorpruefung lo (seit 2026-10-09): Werkzeuge da, lo kommt im eigenen Namensraum hoch, eine
|
||||
# Verbindung ueber 127.0.0.1 gelingt, und nach setpriv sind CapEff und CapAmb 0. Scheitert
|
||||
# etwas davon, endet das Skript hier, vor dem Kopieren.
|
||||
for WERKZEUG in ip setpriv python3; do
|
||||
if ! command -v "$WERKZEUG" >/dev/null 2>&1; then
|
||||
echo "MUTANTEN_LAUF_FEHLER: $WERKZEUG fehlt (gebraucht fuer lo im Netz-Namensraum)"
|
||||
exit 2
|
||||
fi
|
||||
done
|
||||
LO_PROBE='import socket
|
||||
server = socket.socket()
|
||||
server.bind(("127.0.0.1", 0))
|
||||
server.listen(1)
|
||||
client = socket.create_connection(server.getsockname(), timeout=3)
|
||||
client.close()
|
||||
server.close()
|
||||
felder = {}
|
||||
for zeile in open("/proc/self/status", encoding="utf-8", errors="replace"):
|
||||
schluessel, _, wert = zeile.partition(":")
|
||||
felder[schluessel] = wert.strip()
|
||||
if int(felder["CapEff"], 16) != 0 or int(felder["CapAmb"], 16) != 0:
|
||||
raise SystemExit("Faehigkeiten nicht abgeworfen: CapEff " + felder["CapEff"] + ", CapAmb " + felder["CapAmb"])
|
||||
print("lo-probe ok")'
|
||||
LO_AUSGABE=$(timeout 30 unshare --map-current-user --net --keep-caps \
|
||||
sh -c 'ip link set lo up && exec setpriv --inh-caps=-all --ambient-caps=-all -- python3 -c "$1"' sh "$LO_PROBE" 2>&1)
|
||||
LO_EXIT=$?
|
||||
if [ "$LO_EXIT" -ne 0 ] || [ "$LO_AUSGABE" != "lo-probe ok" ]; then
|
||||
echo "MUTANTEN_LAUF_FEHLER: lo im Netz-Namensraum nicht nutzbar (Exit $LO_EXIT): $LO_AUSGABE"
|
||||
exit 2
|
||||
fi
|
||||
|
||||
KOPIE=$(wslpath -u "$KOPIE_WIN")
|
||||
PLANORDNER=$(wslpath -u "$PLANORDNER_WIN")
|
||||
|
||||
if [ ! -d "$KOPIE" ]; then
|
||||
echo "MUTANTEN_LAUF_FEHLER: Kopie fehlt: $KOPIE"
|
||||
exit 2
|
||||
fi
|
||||
if [ ! -d "$PLANORDNER" ]; then
|
||||
echo "MUTANTEN_LAUF_FEHLER: Planordner fehlt: $PLANORDNER"
|
||||
exit 2
|
||||
fi
|
||||
|
||||
MUTANTEN_TOML="$PLANORDNER/mutanten.toml"
|
||||
if [ ! -f "$MUTANTEN_TOML" ]; then
|
||||
echo "KEINE_MUTANTEN_TOML"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
TMPKOPIE=$(mktemp -d /tmp/plankritik-mut-XXXXXX)
|
||||
trap 'rm -rf "$TMPKOPIE"' EXIT
|
||||
|
||||
cp -a "$KOPIE/." "$TMPKOPIE/"
|
||||
chmod 700 "$TMPKOPIE"
|
||||
find "$TMPKOPIE" -name __pycache__ -type d -prune -exec rm -rf {} +
|
||||
|
||||
MUSTER_SCHALTER=""
|
||||
if [ "$MODUS" = "muster" ]; then
|
||||
MUSTER_SCHALTER="--nur-muster"
|
||||
echo "modus: muster (nur Musterpruefung, kein Mutant gefahren)"
|
||||
else
|
||||
echo "modus: voll"
|
||||
fi
|
||||
|
||||
# exec-Kette ohne Zwischenschale (seit 2026-10-09): unshare, sh, setpriv und python3 sind derselbe
|
||||
# Prozess, das SIGTERM von timeout trifft den Laeufer selbst (Treiber-Fall W6).
|
||||
timeout -k 15 "$BUDGET_S" unshare --map-current-user --net --keep-caps \
|
||||
sh -c 'ip link set lo up || { echo "MUTANTEN_LAUF_FEHLER: lo im Netz-Namensraum nicht hochgefahren"; exit 2; }; exec setpriv --inh-caps=-all --ambient-caps=-all -- "$@"' sh \
|
||||
python3 -u /mnt/d/Claude/.vault-tools/plan-kopie/mutanten_lauf.py \
|
||||
--wurzel "$TMPKOPIE" --mutanten "$MUTANTEN_TOML" --zeitlimit "$ZEITLIMIT_S" $MUSTER_SCHALTER
|
||||
EXITCODE=$?
|
||||
if [ "$EXITCODE" -eq 124 ]; then
|
||||
echo "ZEITGRENZE: Mutantenlauf nach $BUDGET_S s abgebrochen; Mutanten ohne Zeile oben sind nicht gefahren"
|
||||
fi
|
||||
echo "EXIT mutanten_lauf: $EXITCODE"
|
||||
exit $EXITCODE
|
||||
@@ -0,0 +1,718 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Treiber fuer mutanten_lauf_kopie.sh: Faelle W1 bis W13 (lo im Netz-Namensraum).
|
||||
|
||||
Zweck: Baut je Fall unter /tmp ein kleines Projekt (kopie/) und einen
|
||||
Planordner (plan/) mit mutanten.toml, ruft mutanten_lauf_kopie.sh mit beiden
|
||||
Ordnern als Windows-Pfaden auf, wie plankritik.js es tut, und vergleicht
|
||||
Exit-Code und Ausgabezeilen mit der Erwartung aus dem Plan
|
||||
docs/plans/2026-10-09-mutanten-kopie-loopback.md. Jeder Fall raeumt seine
|
||||
Ordner selbst auf. Nach jedem Fall prueft der Treiber, dass die uebergebene
|
||||
Kopie byte-gleich ist und kein Ordner /tmp/plankritik-mut-* liegen blieb.
|
||||
|
||||
Aufruf (in WSL): python mutanten_lauf_kopie_treiber.py [--skript <pfad>]
|
||||
Ohne --skript faehrt er mutanten_lauf_kopie.sh neben sich, mit --skript eine
|
||||
andere Fassung (Regressionsnachweis: Stand aus git show HEAD, kaputte
|
||||
Varianten). W7 ist die Grundlinie und laeuft zuerst; ist sie rot, messen die
|
||||
anderen Faelle wenig.
|
||||
|
||||
Pfade: Das Skript wandelt seine Argumente mit wslpath -u um. Der Treiber gibt
|
||||
ihm deshalb wslpath -w seiner /tmp-Ordner und prueft vor dem ersten Fall,
|
||||
dass die Rundreise denselben Pfad ergibt.
|
||||
|
||||
Netz: Die Tests verbinden sich nur mit 127.0.0.1 und, nur im Netz-Namensraum
|
||||
des Skripts, mit 192.0.2.1 (TEST-NET-1). Der Test dazu prueft zuerst, dass es
|
||||
keine Route ausser ueber lo gibt; ohne Namensraum scheitert er dort, bevor er
|
||||
eine Verbindung versucht.
|
||||
|
||||
Faelle W10 bis W13 (Nachbesserung 1, Plan
|
||||
docs/plans/2026-10-09-mutanten-kopie-loopback-nachbesserung-1.md) ersetzen im
|
||||
Attrappen-Ordner einzelne Werkzeuge durch kleine sh-Skripte, nach den
|
||||
Angriffen A1, A3 und A4 des Pruefers vom 2026-10-09: ip scheitert in der
|
||||
Probe (W10), ip meldet Erfolg und lo bleibt aus (W11), ip scheitert erst im
|
||||
Hauptaufruf (W12), setpriv wirft keine Faehigkeit ab (W13).
|
||||
|
||||
Nicht im Treiber: ob plankritik.js das Skript so aufruft wie hier
|
||||
(MSYS_NO_PATHCONV, cygpath -m -l); ob die Probe bei Exit 0 mit falscher
|
||||
Ausgabe scheitert.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import os
|
||||
import shlex
|
||||
import shutil
|
||||
import socket
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import time
|
||||
import uuid
|
||||
from pathlib import Path
|
||||
|
||||
HIER = Path(__file__).resolve().parent
|
||||
SKRIPT_STANDARD = HIER / "mutanten_lauf_kopie.sh"
|
||||
INTERPRETER = "/home/claude/venvs/trading/bin/python"
|
||||
BASH = shutil.which("bash")
|
||||
# Was mutanten_lauf_kopie.sh ueber PATH aufruft (Stand 2026-10-09); daraus
|
||||
# bauen W4, W5, W9 und W10 bis W13 ihre Attrappen-Ordner.
|
||||
WERKZEUGE = (
|
||||
"unshare", "wslpath", "mktemp", "cp", "chmod", "find", "rm", "timeout",
|
||||
"sh", "true", "ip", "setpriv", "python3",
|
||||
)
|
||||
|
||||
FEHLERLISTE = []
|
||||
|
||||
|
||||
def melde(fallname, ok, zusatz=""):
|
||||
if ok:
|
||||
print("{0} ok".format(fallname))
|
||||
else:
|
||||
print("{0} FEHLER: {1}".format(fallname, zusatz))
|
||||
FEHLERLISTE.append(fallname)
|
||||
|
||||
|
||||
def kurz(ergebnis):
|
||||
return "exit={0}\n{1}{2}".format(ergebnis.returncode, ergebnis.stdout, ergebnis.stderr)
|
||||
|
||||
|
||||
def sha_von(pfad):
|
||||
return hashlib.sha256(pfad.read_bytes()).hexdigest()
|
||||
|
||||
|
||||
def plankritik_ordner():
|
||||
return set(Path("/tmp").glob("plankritik-mut-*"))
|
||||
|
||||
|
||||
def windows_pfad(pfad):
|
||||
ergebnis = subprocess.run(["wslpath", "-w", str(pfad)], capture_output=True, text=True, timeout=30)
|
||||
return ergebnis.stdout.strip()
|
||||
|
||||
|
||||
def linux_pfad(pfad):
|
||||
ergebnis = subprocess.run(["wslpath", "-u", pfad], capture_output=True, text=True, timeout=30)
|
||||
return ergebnis.stdout.strip()
|
||||
|
||||
|
||||
# --- Projektbausteine -------------------------------------------------------
|
||||
|
||||
GRUNDDATEIEN = {
|
||||
"pytest.ini": "[pytest]\n",
|
||||
"paket/__init__.py": "",
|
||||
"tests/__init__.py": "",
|
||||
}
|
||||
|
||||
MODUL = (
|
||||
"def f1(x):\n"
|
||||
" if x > 0:\n"
|
||||
" return True\n"
|
||||
" return False\n"
|
||||
)
|
||||
|
||||
TEST_MODUL = (
|
||||
"from paket.modul import f1\n"
|
||||
"\n"
|
||||
"\n"
|
||||
"def test_f1_positiv():\n"
|
||||
" assert f1(5) is True\n"
|
||||
)
|
||||
|
||||
DIENST = (
|
||||
"import socket\n"
|
||||
"\n"
|
||||
"\n"
|
||||
"def antwort_ueber_lo(nachricht):\n"
|
||||
" server = socket.socket()\n"
|
||||
" server.bind((\"127.0.0.1\", 0))\n"
|
||||
" server.listen(1)\n"
|
||||
" client = socket.create_connection(server.getsockname(), timeout=5)\n"
|
||||
" verbindung, _ = server.accept()\n"
|
||||
" client.sendall(nachricht)\n"
|
||||
" daten = verbindung.recv(1024)\n"
|
||||
" for sock in (client, verbindung, server):\n"
|
||||
" sock.close()\n"
|
||||
" return daten\n"
|
||||
)
|
||||
|
||||
TEST_DIENST = (
|
||||
"from paket.dienst import antwort_ueber_lo\n"
|
||||
"\n"
|
||||
"\n"
|
||||
"def test_echo_ueber_lo():\n"
|
||||
" assert antwort_ueber_lo(b\"hallo\") == b\"hallo\"\n"
|
||||
)
|
||||
|
||||
NETZ = (
|
||||
"AUSSEN = (\"192.0.2.1\", 9)\n"
|
||||
"\n"
|
||||
"\n"
|
||||
"def aussen_adresse():\n"
|
||||
" return AUSSEN\n"
|
||||
)
|
||||
|
||||
TEST_NETZ = (
|
||||
"import errno\n"
|
||||
"import socket\n"
|
||||
"\n"
|
||||
"import pytest\n"
|
||||
"\n"
|
||||
"from paket.netz import aussen_adresse\n"
|
||||
"\n"
|
||||
"\n"
|
||||
"def test_keine_route_nach_aussen():\n"
|
||||
" with open(\"/proc/net/route\", encoding=\"ascii\") as datei:\n"
|
||||
" zeilen = datei.read().splitlines()[1:]\n"
|
||||
" schnittstellen = [zeile.split()[0] for zeile in zeilen if zeile.strip()]\n"
|
||||
" assert all(name == \"lo\" for name in schnittstellen), \"Routen ausser lo: {0}\".format(schnittstellen)\n"
|
||||
" sock = socket.socket()\n"
|
||||
" sock.settimeout(5)\n"
|
||||
" try:\n"
|
||||
" sock.connect(aussen_adresse())\n"
|
||||
" except OSError as exc:\n"
|
||||
" assert exc.errno == errno.ENETUNREACH, \"erwartet ENETUNREACH: {0!r}\".format(exc)\n"
|
||||
" else:\n"
|
||||
" pytest.fail(\"Verbindung nach aussen gelungen\")\n"
|
||||
" finally:\n"
|
||||
" sock.close()\n"
|
||||
)
|
||||
|
||||
RECHTE = (
|
||||
"def status_feld(name):\n"
|
||||
" with open(\"/proc/self/status\", encoding=\"utf-8\", errors=\"replace\") as datei:\n"
|
||||
" for zeile in datei:\n"
|
||||
" schluessel, _, wert = zeile.partition(\":\")\n"
|
||||
" if schluessel == name:\n"
|
||||
" return wert.strip()\n"
|
||||
" raise KeyError(name)\n"
|
||||
)
|
||||
|
||||
HAENGEN = (
|
||||
"def schritt_a(n):\n"
|
||||
" return n + 1\n"
|
||||
"\n"
|
||||
"\n"
|
||||
"def schritt_b(n):\n"
|
||||
" return n + 2\n"
|
||||
)
|
||||
|
||||
TEST_HAENGEN = (
|
||||
"from paket.haengen import schritt_a, schritt_b\n"
|
||||
"\n"
|
||||
"\n"
|
||||
"def test_schritt_a():\n"
|
||||
" assert schritt_a(1) == 2\n"
|
||||
"\n"
|
||||
"\n"
|
||||
"def test_schritt_b():\n"
|
||||
" assert schritt_b(1) == 3\n"
|
||||
)
|
||||
|
||||
SCHLEIFE = " while True:\n pass\n"
|
||||
|
||||
PRUEFZEILE_AUSSEN = " assert info.value.errno in (errno.ECONNREFUSED, errno.ENETUNREACH), repr(info.value)\n"
|
||||
|
||||
|
||||
def text_test_rechte(uid):
|
||||
return (
|
||||
"from paket.rechte import status_feld\n"
|
||||
"\n"
|
||||
"UID_AUSSEN = \"" + str(uid) + "\"\n"
|
||||
"\n"
|
||||
"\n"
|
||||
"def test_keine_faehigkeiten_gleiche_uid():\n"
|
||||
" for feld in (\"CapInh\", \"CapPrm\", \"CapEff\", \"CapAmb\"):\n"
|
||||
" wert = status_feld(feld)\n"
|
||||
" assert int(wert, 16) == 0, \"{0} ist {1}\".format(feld, wert)\n"
|
||||
" assert status_feld(\"Uid\").split()[0] == UID_AUSSEN\n"
|
||||
)
|
||||
|
||||
|
||||
def text_test_aussen(port):
|
||||
return (
|
||||
"import errno\n"
|
||||
"import socket\n"
|
||||
"\n"
|
||||
"import pytest\n"
|
||||
"\n"
|
||||
"PORT_AUSSEN = " + str(port) + "\n"
|
||||
"\n"
|
||||
"\n"
|
||||
"def test_dienst_des_rechners_unerreichbar():\n"
|
||||
" sock = socket.socket()\n"
|
||||
" sock.settimeout(5)\n"
|
||||
" try:\n"
|
||||
" with pytest.raises(OSError) as info:\n"
|
||||
" sock.connect((\"127.0.0.1\", PORT_AUSSEN))\n"
|
||||
" finally:\n"
|
||||
" sock.close()\n"
|
||||
+ PRUEFZEILE_AUSSEN
|
||||
)
|
||||
|
||||
|
||||
def kopf():
|
||||
return "format = 1\npython = \"{0}\"\n\n".format(INTERPRETER)
|
||||
|
||||
|
||||
def mutant(id_, datei, aus, wird, rot_bei):
|
||||
return (
|
||||
"[[mutant]]\n"
|
||||
"id = \"" + id_ + "\"\n"
|
||||
"datei = \"" + datei + "\"\n"
|
||||
"aus = '''\n" + aus + "'''\n"
|
||||
"wird = '''\n" + wird + "'''\n"
|
||||
"rot_bei = [\"" + rot_bei + "\"]\n"
|
||||
"\n"
|
||||
)
|
||||
|
||||
|
||||
class Fall:
|
||||
"""Ein Fall unter /tmp: kopie/ mit den Projektdateien, plan/ mit mutanten.toml."""
|
||||
|
||||
def __init__(self, dateien, mutanten):
|
||||
self.basis = Path(tempfile.mkdtemp(prefix="mkopie_treiber_"))
|
||||
self.kopie = self.basis / "kopie"
|
||||
self.plan = self.basis / "plan"
|
||||
self.plan.mkdir()
|
||||
alle = dict(GRUNDDATEIEN)
|
||||
alle.update(dateien)
|
||||
self.snap = {}
|
||||
for relpfad, inhalt in alle.items():
|
||||
pfad = self.kopie / relpfad
|
||||
pfad.parent.mkdir(parents=True, exist_ok=True)
|
||||
pfad.write_text(inhalt, encoding="utf-8")
|
||||
self.snap[pfad] = sha_von(pfad)
|
||||
(self.plan / "mutanten.toml").write_text(kopf() + mutanten, encoding="utf-8")
|
||||
|
||||
def pruefe_kopie(self, fallname):
|
||||
for pfad, sha in self.snap.items():
|
||||
if not pfad.exists() or sha_von(pfad) != sha:
|
||||
melde(fallname + " (kopie)", False, "uebergebene Kopie veraendert: {0}".format(pfad))
|
||||
return
|
||||
|
||||
def aufraeumen(self):
|
||||
shutil.rmtree(self.basis, ignore_errors=True)
|
||||
|
||||
|
||||
def starte_skript(skript, fall, modus="voll", pfad=None):
|
||||
env = dict(os.environ)
|
||||
if pfad is not None:
|
||||
env["PATH"] = pfad
|
||||
cmd = [BASH, str(skript), windows_pfad(fall.kopie), windows_pfad(fall.plan), modus]
|
||||
return subprocess.run(cmd, capture_output=True, text=True, env=env, timeout=180)
|
||||
|
||||
|
||||
# --- Faelle ----------------------------------------------------------------
|
||||
|
||||
def fahre_einfach(name, skript, dateien, mutanten, mutant_id):
|
||||
fall = Fall(dateien, mutanten)
|
||||
try:
|
||||
ergebnis = starte_skript(skript, fall)
|
||||
ok = (
|
||||
ergebnis.returncode == 0
|
||||
and "{0} OK".format(mutant_id) in ergebnis.stdout
|
||||
and "EXIT mutanten_lauf: 0" in ergebnis.stdout
|
||||
)
|
||||
melde(name, ok, kurz(ergebnis))
|
||||
fall.pruefe_kopie(name)
|
||||
finally:
|
||||
fall.aufraeumen()
|
||||
|
||||
|
||||
def fall_einfach():
|
||||
return Fall(
|
||||
{"paket/modul.py": MODUL, "tests/test_modul.py": TEST_MODUL},
|
||||
mutant("MW7", "paket/modul.py", " return True\n", " return False\n",
|
||||
"tests/test_modul.py::test_f1_positiv"),
|
||||
)
|
||||
|
||||
|
||||
def fall_w7(skript):
|
||||
# Grundlinie wie bisher: kein Server-Test, Modus voll und muster.
|
||||
fall = fall_einfach()
|
||||
try:
|
||||
voll = starte_skript(skript, fall)
|
||||
muster = starte_skript(skript, fall, modus="muster")
|
||||
ok = (
|
||||
voll.returncode == 0
|
||||
and "modus: voll" in voll.stdout
|
||||
and "MW7 OK" in voll.stdout
|
||||
and "EXIT mutanten_lauf: 0" in voll.stdout
|
||||
and muster.returncode == 0
|
||||
and "modus: muster" in muster.stdout
|
||||
and "EXIT mutanten_lauf: 0" in muster.stdout
|
||||
)
|
||||
melde("W7", ok, "voll " + kurz(voll) + "\nmuster " + kurz(muster))
|
||||
fall.pruefe_kopie("W7")
|
||||
finally:
|
||||
fall.aufraeumen()
|
||||
|
||||
|
||||
def fall_w1(skript):
|
||||
# Ein Test startet einen Server auf 127.0.0.1 und tauscht Daten aus.
|
||||
fahre_einfach(
|
||||
"W1", skript,
|
||||
{"paket/dienst.py": DIENST, "tests/test_dienst.py": TEST_DIENST},
|
||||
mutant("MW1", "paket/dienst.py", " return daten\n", " return daten[::-1]\n",
|
||||
"tests/test_dienst.py::test_echo_ueber_lo"),
|
||||
"MW1",
|
||||
)
|
||||
|
||||
|
||||
def fall_w2(skript):
|
||||
# Verbindung nach aussen scheitert mit ENETUNREACH; der Mutant zielt auf
|
||||
# 127.0.0.1:9 und faellt nur, wenn lo oben ist (ECONNREFUSED).
|
||||
fahre_einfach(
|
||||
"W2", skript,
|
||||
{"paket/netz.py": NETZ, "tests/test_netz.py": TEST_NETZ},
|
||||
mutant("MW2", "paket/netz.py", "AUSSEN = (\"192.0.2.1\", 9)\n", "AUSSEN = (\"127.0.0.1\", 9)\n",
|
||||
"tests/test_netz.py::test_keine_route_nach_aussen"),
|
||||
"MW2",
|
||||
)
|
||||
|
||||
|
||||
def fall_w3(skript):
|
||||
# CapInh, CapPrm, CapEff, CapAmb gleich 0 und dieselbe uid wie aussen.
|
||||
fahre_einfach(
|
||||
"W3", skript,
|
||||
{"paket/rechte.py": RECHTE, "tests/test_rechte.py": text_test_rechte(os.getuid())},
|
||||
mutant("MW3", "paket/rechte.py", " return wert.strip()\n", " return \"1\"\n",
|
||||
"tests/test_rechte.py::test_keine_faehigkeiten_gleiche_uid"),
|
||||
"MW3",
|
||||
)
|
||||
|
||||
|
||||
def baue_attrappen(ordner, ohne):
|
||||
ordner.mkdir()
|
||||
for name in WERKZEUGE:
|
||||
if name != ohne:
|
||||
os.symlink(shutil.which(name), ordner / name)
|
||||
|
||||
|
||||
def fahre_ohne_werkzeug(name, skript, werkzeug):
|
||||
# Erst ein Kontrolllauf mit allen Attrappen (muss gruen sein), dann einer
|
||||
# ohne das eine Werkzeug (muss vor dem Kopieren mit Exit 2 enden).
|
||||
fall = fall_einfach()
|
||||
try:
|
||||
alle = fall.basis / "attrappen_alle"
|
||||
ohne = fall.basis / "attrappen_ohne"
|
||||
baue_attrappen(alle, None)
|
||||
baue_attrappen(ohne, werkzeug)
|
||||
kontrolle = starte_skript(skript, fall, pfad=str(alle))
|
||||
vorher = plankritik_ordner()
|
||||
ergebnis = starte_skript(skript, fall, pfad=str(ohne))
|
||||
neu = sorted(str(p) for p in plankritik_ordner() - vorher)
|
||||
ok = (
|
||||
kontrolle.returncode == 0
|
||||
and "MW7 OK" in kontrolle.stdout
|
||||
and ergebnis.returncode == 2
|
||||
and "MUTANTEN_LAUF_FEHLER: {0} fehlt".format(werkzeug) in ergebnis.stdout
|
||||
and "modus:" not in ergebnis.stdout
|
||||
and "EXIT mutanten_lauf" not in ergebnis.stdout
|
||||
and not neu
|
||||
)
|
||||
melde(name, ok, "kontrolle exit={0}, ohne {1} exit={2}, neue Kopien={3}\nkontrolle {4}\nohne {1} {5}".format(
|
||||
kontrolle.returncode, werkzeug, ergebnis.returncode, neu, kurz(kontrolle), kurz(ergebnis)))
|
||||
fall.pruefe_kopie(name)
|
||||
finally:
|
||||
fall.aufraeumen()
|
||||
|
||||
|
||||
def fall_w4(skript):
|
||||
fahre_ohne_werkzeug("W4", skript, "ip")
|
||||
|
||||
|
||||
def fall_w5(skript):
|
||||
fahre_ohne_werkzeug("W5", skript, "setpriv")
|
||||
|
||||
|
||||
def fall_w9(skript):
|
||||
fahre_ohne_werkzeug("W9", skript, "python3")
|
||||
|
||||
|
||||
def baue_attrappen_mit_ersatz(ordner, ersatz):
|
||||
# Wie baue_attrappen, aber jedes Werkzeug aus ersatz ist ein kleines
|
||||
# sh-Skript statt eines Verweises auf das echte Werkzeug (Bauweise aus den
|
||||
# Angriffen des Pruefers vom 2026-10-09).
|
||||
ordner.mkdir()
|
||||
for name in WERKZEUGE:
|
||||
pfad = ordner / name
|
||||
if name in ersatz:
|
||||
pfad.write_text("#!/bin/sh\n" + ersatz[name] + "\n", encoding="utf-8")
|
||||
pfad.chmod(0o755)
|
||||
else:
|
||||
os.symlink(shutil.which(name), pfad)
|
||||
|
||||
|
||||
def fahre_probe_scheitert(name, skript, ersatz, merkmal):
|
||||
# Die Vorpruefung muss scheitern: Exit 2 mit der Meldung der Probe und dem
|
||||
# Merkmal des Angriffs, ohne Modus-Zeile, ohne EXIT-Zeile, ohne Kopie.
|
||||
fall = fall_einfach()
|
||||
try:
|
||||
ordner = fall.basis / "attrappen"
|
||||
baue_attrappen_mit_ersatz(ordner, ersatz)
|
||||
vorher = plankritik_ordner()
|
||||
ergebnis = starte_skript(skript, fall, pfad=str(ordner))
|
||||
neu = sorted(str(p) for p in plankritik_ordner() - vorher)
|
||||
ok = (
|
||||
ergebnis.returncode == 2
|
||||
and "MUTANTEN_LAUF_FEHLER: lo im Netz-Namensraum nicht nutzbar" in ergebnis.stdout
|
||||
and merkmal in ergebnis.stdout
|
||||
and "modus:" not in ergebnis.stdout
|
||||
and "EXIT mutanten_lauf" not in ergebnis.stdout
|
||||
and not neu
|
||||
)
|
||||
melde(name, ok, "neue Kopien={0}, {1}".format(neu, kurz(ergebnis)))
|
||||
fall.pruefe_kopie(name)
|
||||
finally:
|
||||
fall.aufraeumen()
|
||||
|
||||
|
||||
def fall_w10(skript):
|
||||
# ip scheitert schon in der Probe (Angriff A1 des Pruefers).
|
||||
fahre_probe_scheitert("W10", skript, {"ip": "echo ip-attrappe scheitert >&2; exit 1"}, "ip-attrappe scheitert")
|
||||
|
||||
|
||||
def fall_w11(skript):
|
||||
# ip meldet Erfolg, lo bleibt aus (A3): Die Verbindung der Probe scheitert
|
||||
# mit Errno 101.
|
||||
fahre_probe_scheitert("W11", skript, {"ip": "exit 0"}, "Errno 101")
|
||||
|
||||
|
||||
def fall_w12(skript):
|
||||
# ip klappt in der Probe und scheitert erst im Hauptaufruf (A4); eine
|
||||
# Zaehlerdatei haelt fest, der wievielte Aufruf es ist. Erwartet:
|
||||
# Modus-Zeile, Meldung des Hauptaufrufs, EXIT-Zeile mit 2, kein Laeufer,
|
||||
# genau zwei Aufrufe von ip, keine Kopie unter /tmp.
|
||||
fall = fall_einfach()
|
||||
try:
|
||||
zaehler = fall.basis / "ip_zaehler"
|
||||
z = shlex.quote(str(zaehler))
|
||||
ip_echt = shlex.quote(shutil.which("ip"))
|
||||
ersatz = {"ip": (
|
||||
"n=0\n"
|
||||
"if [ -f " + z + " ]; then read n < " + z + "; fi\n"
|
||||
"n=$((n + 1))\n"
|
||||
"echo \"$n\" > " + z + "\n"
|
||||
"if [ \"$n\" -ge 2 ]; then echo \"ip-attrappe zweiter Aufruf\" >&2; exit 1; fi\n"
|
||||
"exec " + ip_echt + " \"$@\""
|
||||
)}
|
||||
ordner = fall.basis / "attrappen"
|
||||
baue_attrappen_mit_ersatz(ordner, ersatz)
|
||||
vorher = plankritik_ordner()
|
||||
ergebnis = starte_skript(skript, fall, pfad=str(ordner))
|
||||
neu = sorted(str(p) for p in plankritik_ordner() - vorher)
|
||||
try:
|
||||
aufrufe = zaehler.read_text(encoding="ascii").strip()
|
||||
except OSError:
|
||||
aufrufe = "keine"
|
||||
ok = (
|
||||
ergebnis.returncode == 2
|
||||
and "modus: voll" in ergebnis.stdout
|
||||
and "MUTANTEN_LAUF_FEHLER: lo im Netz-Namensraum nicht hochgefahren" in ergebnis.stdout
|
||||
and "EXIT mutanten_lauf: 2" in ergebnis.stdout
|
||||
and "nicht nutzbar" not in ergebnis.stdout
|
||||
and "MW7" not in ergebnis.stdout
|
||||
and "ip-attrappe zweiter Aufruf" in ergebnis.stderr
|
||||
and aufrufe == "2"
|
||||
and not neu
|
||||
)
|
||||
melde("W12", ok, "ip-aufrufe={0}, neue Kopien={1}, {2}".format(aufrufe, neu, kurz(ergebnis)))
|
||||
fall.pruefe_kopie("W12")
|
||||
finally:
|
||||
fall.aufraeumen()
|
||||
|
||||
|
||||
SETPRIV_OHNE_WIRKUNG = (
|
||||
"while [ \"$#\" -gt 0 ] && [ \"$1\" != \"--\" ]; do shift; done\n"
|
||||
"shift\n"
|
||||
"exec \"$@\""
|
||||
)
|
||||
|
||||
|
||||
def fall_w13(skript):
|
||||
# setpriv wirft keine Faehigkeit ab und startet nur den Befehl hinter --:
|
||||
# Die Faehigkeitspruefung der Probe muss anschlagen.
|
||||
fahre_probe_scheitert("W13", skript, {"setpriv": SETPRIV_OHNE_WIRKUNG}, "Faehigkeiten nicht abgeworfen")
|
||||
|
||||
|
||||
def prozesse_mit_marke(marke):
|
||||
"""pid -> argv aller Prozesse, deren Umgebung die Marke traegt."""
|
||||
gefunden = {}
|
||||
for envdatei in Path("/proc").glob("[0-9]*/environ"):
|
||||
try:
|
||||
if marke.encode("ascii") not in envdatei.read_bytes():
|
||||
continue
|
||||
argv = (envdatei.parent / "cmdline").read_bytes().split(b"\0")
|
||||
except OSError:
|
||||
continue
|
||||
gefunden[envdatei.parent.name] = [teil.decode("utf-8", "replace") for teil in argv if teil]
|
||||
return gefunden
|
||||
|
||||
|
||||
def elternname(pid):
|
||||
try:
|
||||
stat = Path("/proc/{0}/stat".format(pid)).read_text()
|
||||
ppid = stat.rsplit(")", 1)[1].split()[1]
|
||||
return Path("/proc/{0}/comm".format(ppid)).read_text().strip()
|
||||
except (OSError, IndexError):
|
||||
return None
|
||||
|
||||
|
||||
def fall_w6(skript):
|
||||
# Zeitgrenze: zwei haengende Mutanten, BUDGET_S 10, ZEITLIMIT_S 6. Waehrend
|
||||
# des Laufs muss timeout der Elternprozess des Laeufers sein (keine
|
||||
# Zwischenschale); danach lebt kein Prozess des Falls mehr.
|
||||
fall = Fall(
|
||||
{"paket/haengen.py": HAENGEN, "tests/test_haengen.py": TEST_HAENGEN},
|
||||
mutant("MW6a", "paket/haengen.py", " return n + 1\n", SCHLEIFE, "tests/test_haengen.py::test_schritt_a")
|
||||
+ mutant("MW6b", "paket/haengen.py", " return n + 2\n", SCHLEIFE, "tests/test_haengen.py::test_schritt_b"),
|
||||
)
|
||||
try:
|
||||
wert = uuid.uuid4().hex
|
||||
marke = "KOPIE_TREIBER_MARKE=" + wert
|
||||
eigenes_tmpdir = fall.basis / "tmpdir"
|
||||
eigenes_tmpdir.mkdir()
|
||||
env = dict(os.environ)
|
||||
env.update({
|
||||
"BUDGET_S": "10",
|
||||
"ZEITLIMIT_S": "6",
|
||||
"TMPDIR": str(eigenes_tmpdir),
|
||||
"KOPIE_TREIBER_MARKE": wert,
|
||||
})
|
||||
cmd = [BASH, str(skript), windows_pfad(fall.kopie), windows_pfad(fall.plan), "voll"]
|
||||
proc = subprocess.Popen(cmd, env=env, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
|
||||
eltern = None
|
||||
pytest_gesehen = False
|
||||
for _ in range(12):
|
||||
time.sleep(0.5)
|
||||
for pid, argv in prozesse_mit_marke(marke).items():
|
||||
if (argv and Path(argv[0]).name.startswith("python")
|
||||
and any(teil.endswith("mutanten_lauf.py") for teil in argv)):
|
||||
eltern = elternname(pid)
|
||||
if "pytest" in argv:
|
||||
pytest_gesehen = True
|
||||
if eltern is not None and pytest_gesehen:
|
||||
break
|
||||
try:
|
||||
stdout, stderr = proc.communicate(timeout=90)
|
||||
except subprocess.TimeoutExpired:
|
||||
proc.kill()
|
||||
stdout, stderr = proc.communicate()
|
||||
stderr += "\nTREIBER: Skript nach 90 s nicht beendet"
|
||||
reste = prozesse_mit_marke(marke)
|
||||
tmp_reste = sorted(p.name for p in eigenes_tmpdir.iterdir())
|
||||
ok = (
|
||||
proc.returncode == 124
|
||||
and "ZEITGRENZE" in stdout
|
||||
and "EXIT mutanten_lauf: 124" in stdout
|
||||
and "ABBRUCH: SIGTERM" in stdout
|
||||
and eltern == "timeout"
|
||||
and pytest_gesehen
|
||||
and not reste
|
||||
and not tmp_reste
|
||||
)
|
||||
melde("W6", ok, "exit={0}, eltern des laeufers={1}, pytest gesehen={2}, reste={3}, tmp={4}\n{5}{6}".format(
|
||||
proc.returncode, eltern, pytest_gesehen, reste, tmp_reste, stdout, stderr))
|
||||
fall.pruefe_kopie("W6")
|
||||
finally:
|
||||
fall.aufraeumen()
|
||||
|
||||
|
||||
def fall_w8(skript):
|
||||
# Ein Dienst des Treibers auf 127.0.0.1 darf aus dem Namensraum nicht
|
||||
# erreichbar sein, und bei ihm darf keine Verbindung ankommen.
|
||||
dienst = socket.socket()
|
||||
dienst.bind(("127.0.0.1", 0))
|
||||
dienst.listen(5)
|
||||
dienst.setblocking(False)
|
||||
fall = None
|
||||
try:
|
||||
fall = Fall(
|
||||
{"tests/test_aussen.py": text_test_aussen(dienst.getsockname()[1])},
|
||||
mutant("MW8", "tests/test_aussen.py", PRUEFZEILE_AUSSEN, " assert False\n",
|
||||
"tests/test_aussen.py::test_dienst_des_rechners_unerreichbar"),
|
||||
)
|
||||
ergebnis = starte_skript(skript, fall)
|
||||
try:
|
||||
verbindung, _ = dienst.accept()
|
||||
verbindung.close()
|
||||
angekommen = True
|
||||
except BlockingIOError:
|
||||
angekommen = False
|
||||
ok = (
|
||||
ergebnis.returncode == 0
|
||||
and "MW8 OK" in ergebnis.stdout
|
||||
and "EXIT mutanten_lauf: 0" in ergebnis.stdout
|
||||
and not angekommen
|
||||
)
|
||||
melde("W8", ok, "beim Dienst des Rechners angekommen={0}, {1}".format(angekommen, kurz(ergebnis)))
|
||||
fall.pruefe_kopie("W8")
|
||||
finally:
|
||||
dienst.close()
|
||||
if fall is not None:
|
||||
fall.aufraeumen()
|
||||
|
||||
|
||||
FAELLE = [
|
||||
("W7", fall_w7),
|
||||
("W1", fall_w1),
|
||||
("W2", fall_w2),
|
||||
("W3", fall_w3),
|
||||
("W4", fall_w4),
|
||||
("W5", fall_w5),
|
||||
("W6", fall_w6),
|
||||
("W8", fall_w8),
|
||||
("W9", fall_w9),
|
||||
("W10", fall_w10),
|
||||
("W11", fall_w11),
|
||||
("W12", fall_w12),
|
||||
("W13", fall_w13),
|
||||
]
|
||||
|
||||
|
||||
def main():
|
||||
zerleger = argparse.ArgumentParser(description="Treiber fuer mutanten_lauf_kopie.sh (Faelle W1 bis W13)")
|
||||
zerleger.add_argument("--skript", default=str(SKRIPT_STANDARD), help="zu pruefende Fassung des Skripts")
|
||||
argumente = zerleger.parse_args()
|
||||
skript = Path(argumente.skript)
|
||||
print("skript: {0}".format(skript))
|
||||
|
||||
fehlend = [name for name in WERKZEUGE if shutil.which(name) is None]
|
||||
if BASH is None:
|
||||
fehlend.append("bash")
|
||||
if fehlend:
|
||||
print("TREIBER_FEHLER: Werkzeuge fehlen: {0}".format(", ".join(fehlend)))
|
||||
return 2
|
||||
if not skript.is_file():
|
||||
print("TREIBER_FEHLER: Skript fehlt: {0}".format(skript))
|
||||
return 2
|
||||
if not Path(INTERPRETER).is_file():
|
||||
print("TREIBER_FEHLER: Interpreter fehlt: {0}".format(INTERPRETER))
|
||||
return 2
|
||||
|
||||
probe = Path(tempfile.mkdtemp(prefix="mkopie_treiber_rundreise_"))
|
||||
try:
|
||||
windows = windows_pfad(probe)
|
||||
zurueck = linux_pfad(windows) if windows else ""
|
||||
finally:
|
||||
probe.rmdir()
|
||||
if not windows or zurueck != str(probe):
|
||||
print("TREIBER_FEHLER: wslpath-Rundreise: {0} -> {1} -> {2}".format(probe, windows, zurueck))
|
||||
return 2
|
||||
print("rundreise: {0} -> {1}".format(probe, windows))
|
||||
|
||||
for name, funktion in FAELLE:
|
||||
vorher = plankritik_ordner()
|
||||
try:
|
||||
funktion(skript)
|
||||
except Exception as exc:
|
||||
melde(name, False, "Ausnahme: {0!r}".format(exc))
|
||||
liegen_geblieben = sorted(str(p) for p in plankritik_ordner() - vorher)
|
||||
if liegen_geblieben:
|
||||
melde(name + " (aufraeumen)", False, "Kopien unter /tmp blieben liegen: {0}".format(liegen_geblieben))
|
||||
|
||||
print("gerechnete Fehler: {0}".format(len(FEHLERLISTE)))
|
||||
return 0 if not FEHLERLISTE else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,182 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Gleichwertigkeitsprobe fuer die Konfigurationssuche von mutanten_lauf.py.
|
||||
|
||||
Zweck: Je Aufbau zwei Vergleiche. Erstens pytest_konfig_bestimmen aus
|
||||
mutanten_lauf.py gegen determine_setup aus pytest selbst (Konfigurationsdatei
|
||||
und rootdir). Zweitens ein echtes "pytest --co" ohne -c in einem sauberen
|
||||
Rahmen gegen einen Lauf mit den Argumenten des Laeufers in einem Rahmen, ueber
|
||||
dem eine fremde pyproject.toml, ein fremdes conftest.py und ein fremdes
|
||||
setup.py liegen. Verglichen werden Rueckgabewert, Knotennamen und die Liste
|
||||
der geladenen conftest.py-Dateien.
|
||||
|
||||
Aufruf (in WSL, mit dem Python eines Venvs, in dem pytest liegt):
|
||||
python mutanten_lauf_probe_konfig.py <mutanten_lauf.py> <arbeitsordner>
|
||||
Exit 0 ohne Abweichung, 1 mit Abweichung, 2 bei falschem Aufruf.
|
||||
|
||||
Geltungsbereich: ein Fangnetz fuer die Aufbauten unten, kein Nachweis fuer
|
||||
jeden denkbaren Baum. Ein Knoten mit ".." fehlt, weil mutanten_lauf.py ihn
|
||||
schon beim Lesen der mutanten.toml abweist. Eine Wurzel ueber einen Symlink
|
||||
prueft Treiber-Fall 41.
|
||||
|
||||
Die Probe prueft ihre eigene Voraussetzung mit: Ohne -c im sauberen Rahmen
|
||||
muss pytest mit 0 enden (ausser bei den Aufbauten knoten_fehl*), und wo der
|
||||
Aufbau ein conftest.py hat, muss eines geladen sein. Sonst endeten beide
|
||||
Laeufe gleich kaputt, und "gleich" hiesse nichts.
|
||||
|
||||
Grundlage: Probe des Pruefers vom 2026-10-06 (25 Aufbauten). Das conftest.py
|
||||
der Probe schreibt den Zeilenumbruch hier mit chr(10), damit kein Backslash
|
||||
durch die Zeichenkette muss.
|
||||
"""
|
||||
|
||||
import importlib.util
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
TEST = "def test_a():\n pass\n\n\ndef test_b():\n pass\n"
|
||||
CONFTEST = "import os\nopen(os.environ['CFLOG'], 'a').write(__file__ + chr(10))\n"
|
||||
INI = "[pytest]\n"
|
||||
|
||||
AUFBAUTEN = {
|
||||
"wurzel_pytest_ini": ({"pytest.ini": INI, "tests/test_a.py": TEST}, ["tests/test_a.py::test_a"]),
|
||||
"unterordner_ini": (
|
||||
{"tests/pytest.ini": INI, "tests/test_a.py": TEST, "conftest.py": CONFTEST, "tests/conftest.py": CONFTEST},
|
||||
["tests/test_a.py::test_a"],
|
||||
),
|
||||
"zwei_ordner_ini_im_ersten": (
|
||||
{"tests/a/pytest.ini": INI, "tests/a/test_a.py": TEST, "tests/b/test_b.py": TEST},
|
||||
["tests/a/test_a.py::test_a", "tests/b/test_b.py::test_b"],
|
||||
),
|
||||
"zwei_ordner_ini_im_zweiten": (
|
||||
{"tests/b/pytest.ini": INI, "tests/a/test_a.py": TEST, "tests/b/test_b.py": TEST},
|
||||
["tests/a/test_a.py::test_a", "tests/b/test_b.py::test_b"],
|
||||
),
|
||||
"setup_py_wurzel": ({"setup.py": "", "tests/test_a.py": TEST, "conftest.py": CONFTEST}, ["tests/test_a.py::test_a"]),
|
||||
"setup_py_unterordner": (
|
||||
{"sub/setup.py": "", "sub/tests/test_a.py": TEST, "conftest.py": CONFTEST, "sub/conftest.py": CONFTEST},
|
||||
["sub/tests/test_a.py::test_a"],
|
||||
),
|
||||
"setup_py_neben_ini": (
|
||||
{"tests/a/setup.py": "", "tests/b/pytest.ini": INI, "tests/a/test_a.py": TEST, "tests/b/test_b.py": TEST},
|
||||
["tests/a/test_a.py::test_a", "tests/b/test_b.py::test_b"],
|
||||
),
|
||||
"pyproject_ohne_abschnitt": (
|
||||
{"pyproject.toml": "[project]\nname = 'x'\n", "tests/test_a.py": TEST, "conftest.py": CONFTEST},
|
||||
["tests/test_a.py::test_a"],
|
||||
),
|
||||
"pyproject_ohne_unten_ini_oben": (
|
||||
{"tests/pyproject.toml": "[project]\nname = 'x'\n", "pytest.ini": INI, "tests/test_a.py": TEST},
|
||||
["tests/test_a.py::test_a"],
|
||||
),
|
||||
"pyproject_ohne_oben_ini_unten": (
|
||||
{"pyproject.toml": "[project]\nname = 'x'\n", "tests/pytest.ini": INI, "tests/test_a.py": TEST},
|
||||
["tests/test_a.py::test_a"],
|
||||
),
|
||||
"pyproject_ini_options_leer": ({"pyproject.toml": "[tool.pytest.ini_options]\n", "tests/test_a.py": TEST}, ["tests/test_a.py::test_a"]),
|
||||
"pyproject_tool_pytest_leer": ({"pyproject.toml": "[tool.pytest]\n", "tests/test_a.py": TEST}, ["tests/test_a.py::test_a"]),
|
||||
"pyproject_toml_modus": ({"pyproject.toml": "[tool.pytest]\nminversion = '8'\n", "tests/test_a.py": TEST}, ["tests/test_a.py::test_a"]),
|
||||
"tox_mit_abschnitt": ({"tox.ini": "[tox]\n[pytest]\n", "tests/test_a.py": TEST}, ["tests/test_a.py::test_a"]),
|
||||
"tox_ohne_abschnitt": ({"tox.ini": "[tox]\n", "tests/test_a.py": TEST}, ["tests/test_a.py::test_a"]),
|
||||
"tox_abschnitt_mit_kommentar": ({"tox.ini": "[pytest] # k\nmarkers = x\n", "tests/test_a.py": TEST}, ["tests/test_a.py::test_a"]),
|
||||
"tox_abschnitt_mit_semikolon": ({"tox.ini": "[pytest] ; k\nmarkers = x\n", "tests/test_a.py": TEST}, ["tests/test_a.py::test_a"]),
|
||||
"tox_seitenvorschub_vor_abschnitt": ({"tox.ini": "[tox]" + chr(12) + "[pytest]\n", "tests/test_a.py": TEST}, ["tests/test_a.py::test_a"]),
|
||||
"setup_cfg_tool_pytest": ({"setup.cfg": "[tool:pytest]\n", "tests/test_a.py": TEST}, ["tests/test_a.py::test_a"]),
|
||||
"pytest_toml_leer": ({"pytest.toml": "", "tests/test_a.py": TEST}, ["tests/test_a.py::test_a"]),
|
||||
"punkt_pytest_ini_unterordner": ({"tests/.pytest.ini": "", "tests/test_a.py": TEST}, ["tests/test_a.py::test_a"]),
|
||||
"knoten_fehlt_teilweise": (
|
||||
{"tests/pytest.ini": INI, "tests/test_a.py": TEST},
|
||||
["tests/test_a.py::test_a", "gibtsnicht/test_x.py::test_a"],
|
||||
),
|
||||
"knoten_fehlen_alle": ({"tests/pytest.ini": INI, "tests/test_a.py": TEST}, ["gibtsnicht/test_x.py::test_a"]),
|
||||
"ordner_als_knoten": ({"tests/pytest.ini": INI, "tests/test_a.py": TEST}, ["tests"]),
|
||||
"testdatei_in_der_wurzel": ({"test_a.py": TEST, "conftest.py": CONFTEST}, ["test_a.py::test_a"]),
|
||||
"ohne_konfiguration": (
|
||||
{"tests/test_a.py": TEST, "conftest.py": CONFTEST, "tests/conftest.py": CONFTEST},
|
||||
["tests/test_a.py::test_a"],
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
def baue(rahmen, dateien, fremd):
|
||||
wurzel = rahmen / "k"
|
||||
for rel, inhalt in dateien.items():
|
||||
pfad = wurzel / rel
|
||||
pfad.parent.mkdir(parents=True, exist_ok=True)
|
||||
pfad.write_text(inhalt, encoding="utf-8")
|
||||
if fremd:
|
||||
(rahmen / "pyproject.toml").write_text("[tool.pytest.ini_options]\naddopts = '--new-first'\n", encoding="utf-8")
|
||||
(rahmen / "conftest.py").write_text(CONFTEST, encoding="utf-8")
|
||||
(rahmen / "setup.py").write_text("", encoding="utf-8")
|
||||
return wurzel
|
||||
|
||||
|
||||
def sammeln(wurzel, zusatz, knoten, cflog):
|
||||
umgebung = dict(os.environ, CFLOG=str(cflog), PYTHONDONTWRITEBYTECODE="1")
|
||||
lauf = subprocess.run(
|
||||
[sys.executable, "-m", "pytest", "--co", "-q", "-p", "no:cacheprovider"] + zusatz + knoten,
|
||||
cwd=str(wurzel), env=umgebung, capture_output=True, text=True, timeout=120,
|
||||
)
|
||||
knotennamen = sorted(zeile for zeile in lauf.stdout.splitlines() if "::" in zeile)
|
||||
geladen = []
|
||||
if cflog.exists():
|
||||
geladen = sorted(os.path.relpath(z, str(wurzel)) for z in cflog.read_text(encoding="utf-8").split("\n") if z)
|
||||
cflog.unlink()
|
||||
return lauf.returncode, knotennamen, geladen
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) != 3:
|
||||
print("Aufruf: mutanten_lauf_probe_konfig.py <mutanten_lauf.py> <arbeitsordner>")
|
||||
return 2
|
||||
spec = importlib.util.spec_from_file_location("mutanten_lauf_unter_probe", sys.argv[1])
|
||||
laeufer = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(laeufer)
|
||||
from _pytest.config.findpaths import determine_setup
|
||||
|
||||
arbeit = Path(sys.argv[2])
|
||||
leere_konfig = arbeit / "plugin" / "pytest.ini"
|
||||
leere_konfig.parent.mkdir(parents=True, exist_ok=True)
|
||||
leere_konfig.write_text("", encoding="utf-8")
|
||||
cflog = arbeit / "cflog"
|
||||
abweichungen = 0
|
||||
for name, (dateien, knoten) in AUFBAUTEN.items():
|
||||
sauber = baue(arbeit / name / "sauber", dateien, False)
|
||||
fremd = baue(arbeit / name / "fremd", dateien, True)
|
||||
vorher = os.getcwd()
|
||||
os.chdir(sauber)
|
||||
try:
|
||||
rootdir, inipath, _, _ = determine_setup(
|
||||
inifile=None, override_ini=None, args=knoten, rootdir_cmd_arg=None, invocation_dir=sauber,
|
||||
)
|
||||
erwartet = (str(inipath) if inipath else None, str(rootdir))
|
||||
except Exception as exc:
|
||||
erwartet = ("Ausnahme " + type(exc).__name__, None)
|
||||
finally:
|
||||
os.chdir(vorher)
|
||||
konfig, rootdir_l = laeufer.pytest_konfig_bestimmen(sauber, knoten)
|
||||
bekommen = (str(konfig) if konfig else None, str(rootdir_l))
|
||||
ohne_c = sammeln(sauber, [], knoten, cflog)
|
||||
mit_c = sammeln(fremd, laeufer.pytest_konfig_argumente(fremd, knoten, leere_konfig), knoten, cflog)
|
||||
gleich_suche = erwartet == bekommen
|
||||
gleich_lauf = ohne_c == mit_c
|
||||
voraussetzung = name.startswith("knoten_fehl") or ohne_c[0] == 0
|
||||
if any(rel.endswith("conftest.py") for rel in dateien) and not ohne_c[2]:
|
||||
voraussetzung = False
|
||||
if not (gleich_suche and gleich_lauf and voraussetzung):
|
||||
abweichungen += 1
|
||||
print("{0:34s} suche={1} lauf={2} voraussetzung={3}".format(
|
||||
name, "gleich" if gleich_suche else "ABWEICHUNG", "gleich" if gleich_lauf else "ABWEICHUNG",
|
||||
"ok" if voraussetzung else "VERLETZT"))
|
||||
if not gleich_suche:
|
||||
print(" pytest: {0}".format(erwartet))
|
||||
print(" laeufer: {0}".format(bekommen))
|
||||
if not gleich_lauf:
|
||||
print(" ohne -c, sauber: {0}".format(ohne_c))
|
||||
print(" laeufer, fremd: {0}".format(mit_c))
|
||||
print("aufbauten: {0}, abweichungen: {1}".format(len(AUFBAUTEN), abweichungen))
|
||||
return 0 if abweichungen == 0 else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,125 @@
|
||||
"""Prueft die Aus-/wird-Bloecke einer Plan-Taskdatei gegen den Projektbaum.
|
||||
|
||||
Koordinator-Werkzeug im Scratchpad. Geltungsbereich (Fangnetz, kein Nachweis):
|
||||
- Sieht nur Codebloecke, deren letzte nicht leere Zeile vor dem Zaun genau "Aus:" oder
|
||||
"wird:" ist (Projektkonvention der Planer-Taskdateien seit Teil 5). Die Zieldatei ist der
|
||||
letzte Pfad in Backticks in einer Ueberschrift (##, ###, ####) oberhalb des Blocks, der
|
||||
auf eine im Projekt vorhandene oder unter "## Zieldateien" genannte Datei zeigt.
|
||||
- Modus vor: jeder Aus-Block steht genau einmal in seiner Zieldatei (0 = Befund, >1 = Befund
|
||||
"mehrdeutig"). Belegt, dass der Plan den heutigen Stand byte-genau zitiert.
|
||||
- Modus nach: jeder wird-Block steht in der Zieldatei; jeder Aus-Block steht nicht mehr darin,
|
||||
ausser er ist Teil seines eigenen wird-Blocks (Einfuegung).
|
||||
- Ganzdatei-Bloecke ("neu anlegen") sieht es nicht; dafuer ist .vault-tools/plan-diff/plan_diff.py da.
|
||||
- Vergleich nach Normalisierung CRLF -> LF. Eine fehlende Zieldatei ist im Modus vor fuer
|
||||
Aus-Bloecke ein Befund, im Modus nach fuer beide.
|
||||
Exit: 0 kein Befund, 2 Befunde, 3 kein einziger Aus-/wird-Block gefunden (nichts geprueft).
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ZAUN = re.compile(r"^```")
|
||||
UEBERSCHRIFT = re.compile(r"^#{2,4} ")
|
||||
PFAD = re.compile(r"`([^`\s]+/[^`\s]+\.[A-Za-z0-9]+|[^`\s/]+\.(?:py|ts|tsx|toml|md|css|json))`")
|
||||
|
||||
|
||||
def normal(text: str) -> str:
|
||||
return text.replace("\r\n", "\n").replace("\r", "\n")
|
||||
|
||||
|
||||
def lies_bloecke(taskdatei: Path, projekt: Path) -> list[dict]:
|
||||
zeilen = normal(taskdatei.read_text(encoding="utf-8")).split("\n")
|
||||
bloecke: list[dict] = []
|
||||
ziel: str | None = None
|
||||
i = 0
|
||||
while i < len(zeilen):
|
||||
zeile = zeilen[i]
|
||||
if UEBERSCHRIFT.match(zeile):
|
||||
for treffer in PFAD.findall(zeile):
|
||||
ziel = treffer
|
||||
if ZAUN.match(zeile):
|
||||
start = i
|
||||
# Ein Zaun aus n Backticks schliesst erst mit mindestens n Backticks: Ein
|
||||
# Vier-Backtick-Block darf einen Drei-Backtick-Block enthalten (Doku-Tasks).
|
||||
laenge = len(zeile) - len(zeile.lstrip("`"))
|
||||
i += 1
|
||||
inhalt: list[str] = []
|
||||
while i < len(zeilen) and not (
|
||||
zeilen[i].startswith("`" * laenge) and not zeilen[i].strip("`").strip()
|
||||
):
|
||||
inhalt.append(zeilen[i])
|
||||
i += 1
|
||||
marke = ""
|
||||
k = start - 1
|
||||
while k >= 0 and not zeilen[k].strip():
|
||||
k -= 1
|
||||
if k >= 0:
|
||||
marke = zeilen[k].strip()
|
||||
if marke in ("Aus:", "wird:"):
|
||||
bloecke.append({"art": marke[:-1], "ziel": ziel, "zeile": start + 1, "text": "\n".join(inhalt)})
|
||||
i += 1
|
||||
return bloecke
|
||||
|
||||
|
||||
def main() -> int:
|
||||
zerleger = argparse.ArgumentParser()
|
||||
zerleger.add_argument("modus", choices=["vor", "nach"])
|
||||
zerleger.add_argument("taskdateien", nargs="+")
|
||||
zerleger.add_argument("--projekt", required=True)
|
||||
argumente = zerleger.parse_args()
|
||||
projekt = Path(argumente.projekt)
|
||||
|
||||
befunde = 0
|
||||
geprueft = 0
|
||||
for name in argumente.taskdateien:
|
||||
taskdatei = Path(name)
|
||||
bloecke = lies_bloecke(taskdatei, projekt)
|
||||
paare = 0
|
||||
for nummer, block in enumerate(bloecke):
|
||||
if block["ziel"] is None:
|
||||
print(f"BEFUND {taskdatei.name}:{block['zeile']}: {block['art']}-Block ohne Zieldatei in einer Ueberschrift")
|
||||
befunde += 1
|
||||
continue
|
||||
datei = projekt / block["ziel"]
|
||||
if not datei.exists():
|
||||
if argumente.modus == "nach" or block["art"] == "Aus":
|
||||
print(f"BEFUND {taskdatei.name}:{block['zeile']}: Zieldatei {block['ziel']} fehlt")
|
||||
befunde += 1
|
||||
continue
|
||||
inhalt = normal(datei.read_text(encoding="utf-8"))
|
||||
anzahl = inhalt.count(block["text"])
|
||||
geprueft += 1
|
||||
if argumente.modus == "vor":
|
||||
if block["art"] != "Aus":
|
||||
continue
|
||||
paare += 1
|
||||
if anzahl == 0:
|
||||
print(f"BEFUND {taskdatei.name}:{block['zeile']}: Aus-Block steht nicht in {block['ziel']}")
|
||||
befunde += 1
|
||||
elif anzahl > 1:
|
||||
print(f"BEFUND {taskdatei.name}:{block['zeile']}: Aus-Block steht {anzahl}-mal in {block['ziel']} (mehrdeutig)")
|
||||
befunde += 1
|
||||
else:
|
||||
if block["art"] == "wird":
|
||||
paare += 1
|
||||
if anzahl == 0:
|
||||
print(f"BEFUND {taskdatei.name}:{block['zeile']}: wird-Block steht nicht in {block['ziel']}")
|
||||
befunde += 1
|
||||
else:
|
||||
folge = bloecke[nummer + 1] if nummer + 1 < len(bloecke) else None
|
||||
einfuegung = folge is not None and folge["art"] == "wird" and block["text"] in folge["text"]
|
||||
if anzahl > 0 and not einfuegung:
|
||||
print(f"BEFUND {taskdatei.name}:{block['zeile']}: Aus-Block steht noch in {block['ziel']}")
|
||||
befunde += 1
|
||||
print(f"{taskdatei.name}: {len(bloecke)} Aus-/wird-Bloecke, im Modus {argumente.modus} geprueft: {paare}")
|
||||
if geprueft == 0:
|
||||
print("Nichts geprueft: kein Aus-/wird-Block gefunden.")
|
||||
return 3
|
||||
print(f"{befunde} Befund(e).")
|
||||
return 2 if befunde else 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,31 @@
|
||||
"""Ersetzt in EINER Zeile einer Datei bestimmte echte Zeichen durch ihre sechs ASCII-Zeichen lange Escape-Folge.
|
||||
|
||||
Arbeitet mit Codepunkten; im eigenen Quelltext steht keine Escape-Folge, die eine Schicht auswerten koennte.
|
||||
Aufruf: python plan_escape.py <datei> <zeilennummer> <codepunkt-hex> [<codepunkt-hex> ...]
|
||||
Beispiel: python plan_escape.py task-107.md 311 2122 0441
|
||||
Exit 0, wenn mindestens ein Zeichen ersetzt wurde; 2, wenn die Zeile keines der Zeichen traegt.
|
||||
"""
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
datei = Path(sys.argv[1])
|
||||
nummer = int(sys.argv[2])
|
||||
punkte = [int(p, 16) for p in sys.argv[3:]]
|
||||
|
||||
text = datei.read_bytes().decode("utf-8")
|
||||
zeilen = text.split("\n")
|
||||
zeile = zeilen[nummer - 1]
|
||||
ersetzt = 0
|
||||
for punkt in punkte:
|
||||
zeichen = chr(punkt)
|
||||
folge = chr(92) + "u" + format(punkt, "04x")
|
||||
ersetzt += zeile.count(zeichen)
|
||||
zeile = zeile.replace(zeichen, folge)
|
||||
if ersetzt == 0:
|
||||
print(f"BEFUND: Zeile {nummer} traegt keines der Zeichen -- nichts geaendert: {zeilen[nummer - 1][:100]!r}")
|
||||
sys.exit(2)
|
||||
zeilen[nummer - 1] = zeile
|
||||
with open(datei, "w", encoding="utf-8", newline="") as strom:
|
||||
strom.write("\n".join(zeilen))
|
||||
print(f"{ersetzt} Zeichen ersetzt in Zeile {nummer}: {zeile.strip()[:140]}")
|
||||
@@ -0,0 +1,183 @@
|
||||
"""Wendet die Codebloecke der Taskdateien eines Plans in Wellenreihenfolge auf eine Kopie des Projekts an.
|
||||
|
||||
Koordinator-Werkzeug (entstanden 2026-09-17, Trading Plattform Teil 3b). Es tut etwas und beurteilt
|
||||
nichts; das Urteil faellt danach mit Suite, Mutanten und welle_vergleich.py.
|
||||
Geltungsbereich:
|
||||
- Zieldatei ist der letzte Pfad in Backticks in einer Ueberschrift (## bis ####) oberhalb des Blocks.
|
||||
- Dateien, die "## Zieldateien" als "(gelöscht)" fuehrt, werden in der Kopie geloescht.
|
||||
- Bloecke mit Einrueckung (Befehle in Listen) und Prosa-Vorgaben ("wird: ein neuer Abschnitt ...")
|
||||
werden uebergangen: Ein Doku-Task mit frei zu formulierendem Text laesst sich nicht anwenden.
|
||||
- Marke ist die letzte nicht leere Zeile vor dem Zaun:
|
||||
"Aus:" -> merkt den Block als alten Text
|
||||
"wird:" -> ersetzt den gemerkten Text (muss genau einmal vorkommen)
|
||||
enthaelt "Die ganze Datei ist neu" -> schreibt die Datei
|
||||
beginnt mit "Häng diesen Block ans Ende" -> haengt an
|
||||
Alles andere (Befehle, erwartete Ausgaben, Prosa-"wird: ...") wird uebergangen und gezaehlt.
|
||||
- Zaeune aus n Backticks schliessen erst mit n Backticks.
|
||||
- Geschrieben wird immer mit LF.
|
||||
Aufruf: python plan_materialisiere.py <planordner> <kopie> <taskdatei> [<taskdatei> ...]
|
||||
Exit 0 ohne Befund, 2 mit Befund.
|
||||
"""
|
||||
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
UEBERSCHRIFT = re.compile(r"^#{2,4} ")
|
||||
PFAD = re.compile(r"`([^`\s]+/[^`\s]+\.[A-Za-z0-9]+|[^`\s/]+\.(?:py|ts|tsx|toml|md|css|json))`")
|
||||
|
||||
|
||||
def normal(text: str) -> str:
|
||||
return text.replace("\r\n", "\n").replace("\r", "\n")
|
||||
|
||||
|
||||
def bloecke(taskdatei: Path):
|
||||
zeilen = normal(taskdatei.read_text(encoding="utf-8")).split("\n")
|
||||
ziel = None
|
||||
i = 0
|
||||
while i < len(zeilen):
|
||||
zeile = zeilen[i]
|
||||
if UEBERSCHRIFT.match(zeile):
|
||||
treffer = PFAD.findall(zeile)
|
||||
if treffer:
|
||||
ziel = treffer[-1]
|
||||
if zeile.lstrip().startswith("```"):
|
||||
einzug = len(zeile) - len(zeile.lstrip())
|
||||
kern = zeile.lstrip()
|
||||
laenge = len(kern) - len(kern.lstrip("`"))
|
||||
start = i
|
||||
i += 1
|
||||
inhalt = []
|
||||
while i < len(zeilen):
|
||||
kandidat = zeilen[i].lstrip()
|
||||
if kandidat.startswith("`" * laenge) and not kandidat.strip("`").strip():
|
||||
break
|
||||
inhalt.append(zeilen[i][einzug:] if zeilen[i][:einzug].strip() == "" else zeilen[i])
|
||||
i += 1
|
||||
k = start - 1
|
||||
while k >= 0 and not zeilen[k].strip():
|
||||
k -= 1
|
||||
marke = zeilen[k].strip() if k >= 0 else ""
|
||||
yield {"ziel": ziel, "marke": marke, "zeile": start + 1, "text": "\n".join(inhalt), "einzug": einzug}
|
||||
i += 1
|
||||
|
||||
|
||||
def in_kopie(kopie: Path, ziel: str) -> Path | None:
|
||||
"""Der Zielpfad in der Kopie, oder None, wenn er aus ihr hinausfuehrt.
|
||||
|
||||
Nachbesserung 2026-10-11 (Paket E9 der Trading Plattform): Ein absoluter Pfad in einer
|
||||
Ueberschrift schlug unter Windows die Kopie (kopie / "D:/..." ist der absolute Pfad), und das
|
||||
Skript schrieb mit Exit 0 in die echte Datei; gemessen im Scratchpad an einer Probe-Datei.
|
||||
Seitdem ist ein Ziel ausserhalb der Kopie (absolut oder mit "..") ein Befund, und nichts wird
|
||||
geschrieben.
|
||||
"""
|
||||
pfad = (kopie / ziel).resolve()
|
||||
try:
|
||||
pfad.relative_to(kopie.resolve())
|
||||
except ValueError:
|
||||
return None
|
||||
return pfad
|
||||
|
||||
|
||||
LOESCHZIEL = re.compile(r"^- `(.+?)`\s*\((.*)\)\s*$")
|
||||
|
||||
|
||||
def loesche(taskdatei: Path, kopie: Path) -> int:
|
||||
"""Loescht, was der Abschnitt "## Zieldateien" als "(gelöscht)" fuehrt.
|
||||
|
||||
Der Pfad dort ist absolut und zeigt ins echte Projekt. Geloescht wird in der Kopie die
|
||||
Datei am laengsten Pfadende, das dort existiert -- nie etwas ausserhalb der Kopie.
|
||||
"""
|
||||
drin = False
|
||||
anzahl = 0
|
||||
for zeile in normal(taskdatei.read_text(encoding="utf-8")).split("\n"):
|
||||
if zeile.startswith("## "):
|
||||
drin = zeile.startswith("## Zieldateien")
|
||||
continue
|
||||
treffer = LOESCHZIEL.match(zeile) if drin else None
|
||||
if not treffer or "gelöscht" not in treffer.group(2):
|
||||
continue
|
||||
teile = treffer.group(1).replace("\\", "/").split("/")
|
||||
for anfang in range(len(teile)):
|
||||
kandidat = kopie.joinpath(*teile[anfang:])
|
||||
if ":" not in teile[anfang] and kandidat.is_file():
|
||||
kandidat.unlink()
|
||||
anzahl += 1
|
||||
break
|
||||
return anzahl
|
||||
|
||||
|
||||
def main() -> int:
|
||||
plan, kopie = Path(sys.argv[1]), Path(sys.argv[2])
|
||||
befunde = 0
|
||||
for name in sys.argv[3:]:
|
||||
taskdatei = plan / name
|
||||
alt = None
|
||||
zaehler = {"neu": 0, "ersetzt": 0, "angehaengt": 0, "uebergangen": 0}
|
||||
for block in bloecke(taskdatei):
|
||||
marke, ziel = block["marke"], block["ziel"]
|
||||
if block["einzug"] > 0:
|
||||
zaehler["uebergangen"] += 1
|
||||
continue
|
||||
if marke == "Aus:" or marke.startswith("Aus, Zeile"):
|
||||
alt = block
|
||||
continue
|
||||
if marke == "wird:":
|
||||
if alt is None or ziel is None:
|
||||
print(f"BEFUND {name}:{block['zeile']}: wird-Block ohne Aus-Block oder ohne Zieldatei")
|
||||
befunde += 1
|
||||
continue
|
||||
pfad = in_kopie(kopie, ziel)
|
||||
if pfad is None:
|
||||
print(f"BEFUND {name}:{block['zeile']}: Zieldatei {ziel} liegt ausserhalb der Kopie, nichts geschrieben")
|
||||
befunde += 1
|
||||
alt = None
|
||||
continue
|
||||
if not pfad.exists():
|
||||
print(f"BEFUND {name}:{block['zeile']}: Zieldatei {ziel} fehlt in der Kopie")
|
||||
befunde += 1
|
||||
alt = None
|
||||
continue
|
||||
text = normal(pfad.read_text(encoding="utf-8"))
|
||||
anzahl = text.count(alt["text"])
|
||||
if anzahl != 1:
|
||||
print(f"BEFUND {name}:{alt['zeile']}: Aus-Block steht {anzahl}-mal in {ziel}")
|
||||
befunde += 1
|
||||
else:
|
||||
with open(pfad, "w", encoding="utf-8", newline="\n") as strom:
|
||||
strom.write(text.replace(alt["text"], block["text"]))
|
||||
zaehler["ersetzt"] += 1
|
||||
alt = None
|
||||
continue
|
||||
alt = None
|
||||
if ("Die ganze Datei ist neu" in marke or "vollständig ersetzt" in marke) and ziel is not None:
|
||||
pfad = in_kopie(kopie, ziel)
|
||||
if pfad is None:
|
||||
print(f"BEFUND {name}:{block['zeile']}: Zieldatei {ziel} liegt ausserhalb der Kopie, nichts geschrieben")
|
||||
befunde += 1
|
||||
continue
|
||||
pfad.parent.mkdir(parents=True, exist_ok=True)
|
||||
with open(pfad, "w", encoding="utf-8", newline="\n") as strom:
|
||||
strom.write(block["text"].rstrip("\n") + "\n")
|
||||
zaehler["neu"] += 1
|
||||
continue
|
||||
if marke.startswith("Häng diesen Block ans Ende") and ziel is not None:
|
||||
pfad = in_kopie(kopie, ziel)
|
||||
if pfad is None:
|
||||
print(f"BEFUND {name}:{block['zeile']}: Zieldatei {ziel} liegt ausserhalb der Kopie, nichts geschrieben")
|
||||
befunde += 1
|
||||
continue
|
||||
text = normal(pfad.read_text(encoding="utf-8"))
|
||||
with open(pfad, "w", encoding="utf-8", newline="\n") as strom:
|
||||
strom.write(text.rstrip("\n") + "\n" + block["text"].rstrip("\n") + "\n")
|
||||
zaehler["angehaengt"] += 1
|
||||
continue
|
||||
zaehler["uebergangen"] += 1
|
||||
zaehler["geloescht"] = loesche(taskdatei, kopie)
|
||||
print(f"{name}: {zaehler}")
|
||||
print(f"{befunde} Befund(e).")
|
||||
return 2 if befunde else 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,62 @@
|
||||
"""Sucht in den Codebloecken eines Plans nach Zeichen, die ein Executer nicht sehen oder nicht tippen kann.
|
||||
|
||||
Koordinator-Werkzeug. Frage: Steht in einem Codeblock ein Zeichen, das beim Abschreiben verloren geht?
|
||||
Herkunft der Antwort: Unicode-Kategorie des Zeichens (unicodedata, Quelle: Python-Standardbibliothek).
|
||||
Befund ist jedes Zeichen der Kategorien Zl, Zp, Cf, Cc (ausser \\n und \\t) und Zs ausser dem Leerzeichen --
|
||||
also Zeilen- und Absatztrenner, Formatzeichen, Steuerzeichen, geschuetzte und schmale Leerzeichen.
|
||||
Alle uebrigen Nicht-ASCII-Zeichen in Codebloecken werden nur gezaehlt und gezeigt (Umlaute, Gedankenstriche).
|
||||
Was es nicht sieht: Zeichen ausserhalb von Codezaeunen; verwechselbare sichtbare Zeichen (kyrillisches c).
|
||||
Aufruf: python plan_zeichen.py <planordner> Exit 0 ohne Befund, 2 mit Befund.
|
||||
"""
|
||||
|
||||
import sys
|
||||
import unicodedata
|
||||
from collections import Counter
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def main() -> int:
|
||||
# Die Windows-Konsole kodiert cp1252; ein sichtbares Zeichen wie U+2192 in der Zusammenfassung
|
||||
# liess das Skript sonst mit UnicodeEncodeError abbrechen (2026-09-24, Plan AF der Trading Plattform).
|
||||
sys.stdout.reconfigure(errors="backslashreplace")
|
||||
plan = Path(sys.argv[1])
|
||||
dateien = sorted(plan.glob("*.md"))
|
||||
if not dateien:
|
||||
print(f"BEFUND {plan}: keine Markdown-Dateien -- der Lauf misst nichts")
|
||||
return 2
|
||||
befunde = 0
|
||||
sichtbar: Counter[str] = Counter()
|
||||
bloecke = 0
|
||||
for datei in dateien:
|
||||
zeilen = datei.read_text(encoding="utf-8").replace("\r\n", "\n").split("\n")
|
||||
zaun = 0
|
||||
for nummer, zeile in enumerate(zeilen, start=1):
|
||||
kern = zeile.lstrip()
|
||||
if kern.startswith("```"):
|
||||
laenge = len(kern) - len(kern.lstrip("`"))
|
||||
if zaun == 0:
|
||||
zaun = laenge
|
||||
bloecke += 1
|
||||
elif laenge >= zaun and not kern.strip("`").strip():
|
||||
zaun = 0
|
||||
continue
|
||||
if zaun == 0:
|
||||
continue
|
||||
for spalte, zeichen in enumerate(zeile, start=1):
|
||||
if ord(zeichen) < 128 and zeichen not in "\x0b\x0c\x1b" and (zeichen >= " " or zeichen == "\t"):
|
||||
continue
|
||||
kategorie = unicodedata.category(zeichen)
|
||||
if kategorie in ("Zl", "Zp", "Cf", "Cc") or (kategorie == "Zs" and zeichen != " "):
|
||||
name = unicodedata.name(zeichen, "?")
|
||||
print(f"BEFUND {datei.name}:{nummer}:{spalte}: U+{ord(zeichen):04X} {name} ({kategorie}) in: {zeile.strip()[:110]!r}")
|
||||
befunde += 1
|
||||
else:
|
||||
sichtbar[zeichen] += 1
|
||||
print(f"{len(dateien)} Dateien, {bloecke} Codebloecke gelesen.")
|
||||
print("Sichtbare Nicht-ASCII-Zeichen in Codebloecken: " + " ".join(f"{z}(U+{ord(z):04X})x{n}" for z, n in sichtbar.most_common()))
|
||||
print(f"{befunde} Befund(e).")
|
||||
return 2 if befunde else 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,328 @@
|
||||
"""Rot-Nachweis je Task: die neuen Tests eines Tasks gegen den Produktivcode VOR dem Task, auf Wegwerfkopien.
|
||||
|
||||
Koordinator-Werkzeug (entstanden 2026-09-18, Trading Plattform). plan_materialisiere.py wendet eine
|
||||
Taskdatei immer ganz an; "erst der Test, dann die Umsetzung" laesst sich damit nicht beobachten. Dieses
|
||||
Skript stellt den Zustand dazwischen her: Basis + Voraussetzungstasks + der Task selbst, danach die
|
||||
PRODUKTIVEN Zieldateien des Tasks zurueck auf den Stand vor dem Task (neue Produktivdateien geloescht).
|
||||
Dann laufen die Testdateien des Tasks. Es tut etwas und berichtet; das Urteil faellt der Koordinator:
|
||||
ROT mindestens ein Fehlschlag oder Sammelfehler -- die erste Fehlerzeile steht dabei, damit man sieht,
|
||||
ob der Test an der gemeinten Stelle faellt und nicht an einem Tippfehler.
|
||||
GRUEN kein Fehlschlag ohne die Umsetzung -- ein Test, der so gruen ist, misst die Umsetzung nicht
|
||||
(ausser der Plan fuehrt ihn ausdruecklich als Regressionsnetz).
|
||||
Geltungsbereich und Luecken:
|
||||
- Testdateien sind Zieldateien unter tests/ sowie *.test.ts und *.test.tsx; alles unter tests/fixtures/
|
||||
gilt als Testseite und bleibt stehen. Alles andere ist produktiv.
|
||||
- tests/conftest.py gilt als PRODUKTIVSEITE und wird zurueckgesetzt (Projektvorgabe seit 2026-09-22):
|
||||
Liegt die Korrektur eines Tasks selbst in conftest.py (Trading Plattform, Task 369: die Ersatzfunktionen
|
||||
der Netzsperre), wendete das Skript sie im Lauf "ohne Umsetzung" mit an und meldete GRUEN -- der eine
|
||||
Zustand, in dem der Nachweis nichts misst, sah aus wie ein sauberer Befund. Liegt dagegen eine neue
|
||||
Fixture des Tasks in conftest.py (Task 368), fehlt sie jetzt im Lauf und die neuen Tests enden als
|
||||
Sammelfehler -- das ist ROT mit "fixture ... not found" in der ersten Fehlerzeile, also sichtbar.
|
||||
Wer diesen Fall hat, ruft mit --conftest-als-test auf; dann gilt conftest.py wie frueher als Testdatei.
|
||||
Das Skript sagt je Task, wie es conftest.py behandelt hat.
|
||||
- Python-Tests laufen in WSL (Debian, Venv aus --venv), Oberflaechentests unter Windows mit npx vitest im
|
||||
Ordner aus --frontend; dafuer setzt das Skript eine Junction auf das node_modules des Projekts und loest
|
||||
sie vor dem Loeschen. Die Vorgaben der drei Schalter sind die der Trading Plattform (~/venvs/trading,
|
||||
dashboard, trading-plattform/); jedes andere Projekt nennt seine Werte (seit 2026-09-24, Nexhorizons Hub).
|
||||
Fehlt das Venv in WSL oder der Frontend-Ordner in der Kopie, steht beim Task VORAUSSETZUNG FEHLT statt
|
||||
ROT -- sonst saehe ein Lauf im falschen Venv aus wie ein sauberer Rot-Nachweis. Dasselbe gilt, wenn die
|
||||
erste pytest-Fehlerzeile ein Modul vermisst, das weder im Projekt liegt noch vom Task angelegt wird
|
||||
(vergessenes --venv, eine Abhaengigkeit, die nur der Stack hat). Das greift nur an der ersten
|
||||
Fehlerzeile; ein fremdes Modul, das erst hinter einem Projektmodul fehlt, sieht das Skript nicht.
|
||||
- Vorbedingung: Das Projekt ist im Venv NICHT editierbar installiert (pip install -e). Sonst loest der
|
||||
Finder von setuptools ein fehlendes Untermodul gegen das echte Projekt auf, und ein Test ohne Umsetzung
|
||||
liefe gruen (Pruefung 2026-09-24, setuptools editable_wheel.py).
|
||||
- Erzeugte Fixtures (der Plan laesst sie mit altem Code erzeugen) holt --fixturen aus einem Ordner nach,
|
||||
wenn sie nach dem Anwenden fehlen.
|
||||
- Es sieht nicht, WELCHE Tests rot sein muessten; es zaehlt und zeigt die erste Fehlerzeile.
|
||||
- Die Basis ist eine Kopie, die der Aufrufer VOR der ersten Welle gezogen hat; das Skript fasst das
|
||||
Projekt selbst nie an.
|
||||
|
||||
Aufruf: python rot_nachweis.py <basis-kopie> <planordner> <arbeitsordner> <spec.json> [--fixturen <ordner>] [--node-modules <pfad>] [--conftest-als-test] [--venv <wsl-pfad>] [--frontend <ordner>] [--projektname <ordnername>]
|
||||
--node-modules nennt das node_modules des Projekts, wenn die Basis-Kopie keines traegt.
|
||||
--venv nennt das Venv in WSL (Vorgabe ~/venvs/trading), --frontend den Oberflaechenordner relativ zum
|
||||
Projekt (Vorgabe dashboard), --projektname den Ordnernamen, hinter dem absolute Zielpfade abgeschnitten
|
||||
werden (Vorgabe trading-plattform).
|
||||
--conftest-als-test laesst tests/conftest.py als Testdatei stehen (nur, wenn die neue Fixture des Tasks dort liegt).
|
||||
spec.json: [{"task": "task-141-....md", "vor": ["task-...md", ...]}, ...]
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import shlex
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
HIER = Path(__file__).resolve().parent
|
||||
ZIEL = re.compile(r"^- `(.+?)`")
|
||||
PROJEKTNAME = "trading-plattform/"
|
||||
VENV = "~/venvs/trading"
|
||||
FRONTEND = "dashboard"
|
||||
CONFTEST = "tests/conftest.py"
|
||||
# Das Venv steht ungequotet im bash-Aufruf, damit ~ expandiert; deshalb nur diese Zeichen.
|
||||
VENV_MUSTER = re.compile(r"^~?[A-Za-z0-9._/-]+\Z")
|
||||
FRONTEND_MUSTER = re.compile(r"^[A-Za-z0-9._-]+(/[A-Za-z0-9._-]+)*\Z")
|
||||
FEHLENDES_MODUL = re.compile(r"No module named '([^']+)'")
|
||||
|
||||
|
||||
def zieldateien(taskdatei: Path, projektname: str = PROJEKTNAME) -> list[str]:
|
||||
"""Zieldateien relativ zum Projekt. Absolute Pfade werden hinter dem Projektordner abgeschnitten."""
|
||||
drin = False
|
||||
ziele: list[str] = []
|
||||
for zeile in taskdatei.read_text(encoding="utf-8").replace("\r\n", "\n").split("\n"):
|
||||
if zeile.startswith("## "):
|
||||
drin = zeile.startswith("## Zieldateien")
|
||||
continue
|
||||
treffer = ZIEL.match(zeile) if drin else None
|
||||
if treffer:
|
||||
pfad = treffer.group(1).replace("\\", "/").removeprefix("./")
|
||||
if re.match(r"^[A-Za-z]:/", pfad):
|
||||
stelle = pfad.lower().find(projektname)
|
||||
if stelle < 0:
|
||||
raise ValueError(f"{taskdatei.name}: absoluter Pfad ausserhalb des Projekts: {pfad}")
|
||||
pfad = pfad[stelle + len(projektname) :]
|
||||
ziele.append(pfad)
|
||||
return ziele
|
||||
|
||||
|
||||
def ist_test(pfad: str, conftest_als_test: bool = False) -> bool:
|
||||
if pfad == CONFTEST:
|
||||
# Vorgabe: produktiv. Sonst wendet der Lauf "ohne Umsetzung" eine Korrektur an, die in
|
||||
# conftest.py liegt, und meldet gruen (siehe Kopfkommentar).
|
||||
return conftest_als_test
|
||||
return pfad.startswith("tests/") or pfad.endswith((".test.ts", ".test.tsx"))
|
||||
|
||||
|
||||
def wende_an(planordner: Path, kopie: Path, tasks: list[str]) -> str:
|
||||
if not tasks:
|
||||
return ""
|
||||
lauf = subprocess.run(
|
||||
[sys.executable, "-X", "utf8", str(HIER / "plan_materialisiere.py"), str(planordner), str(kopie), *tasks],
|
||||
capture_output=True, text=True, encoding="utf-8", errors="replace",
|
||||
)
|
||||
return "" if lauf.returncode == 0 else (lauf.stdout + lauf.stderr)[-600:]
|
||||
|
||||
|
||||
def wsl_pfad(pfad: Path) -> str:
|
||||
text = str(pfad.resolve()).replace("\\", "/")
|
||||
return f"/mnt/{text[0].lower()}{text[2:]}"
|
||||
|
||||
|
||||
def loesche(ordner: Path, frontend: str = FRONTEND) -> None:
|
||||
junction = ordner / frontend / "node_modules"
|
||||
if junction.exists():
|
||||
os.rmdir(junction) # loest nur die Verknuepfung, nie das Ziel
|
||||
shutil.rmtree(ordner, ignore_errors=True)
|
||||
|
||||
|
||||
def pytest_befehl(rot: Path, python_tests: list[str], venv: str = VENV) -> str:
|
||||
# shlex.quote: Ein Arbeitsordner mit Leerzeichen (Benutzername im Temp-Pfad) liess das cd
|
||||
# scheitern; jeder Task stand dann als "ROT pytest: keine Summenzeile" da, ohne dass ein Test
|
||||
# lief (gefunden 2026-09-20).
|
||||
return f"cd {shlex.quote(wsl_pfad(rot))} && {venv}/bin/python -m pytest {' '.join(python_tests)} -q -p no:cacheprovider 2>&1 | tail -60"
|
||||
|
||||
|
||||
def venv_da(venv: str) -> bool:
|
||||
lauf = subprocess.run(["wsl.exe", "-d", "Debian", "--", "bash", "-lc", f"test -x {venv}/bin/python && echo VENV_DA"],
|
||||
capture_output=True, text=True, encoding="utf-8", errors="replace")
|
||||
return "VENV_DA" in lauf.stdout
|
||||
|
||||
|
||||
def fremdes_modul(fehlerzeile: str, ziele: list[str], kopie: Path) -> str | None:
|
||||
"""Das fehlende Modul, wenn es weder im Projekt liegt noch vom Task angelegt wird.
|
||||
|
||||
Ein solcher Fehler misst das Venv, nicht die fehlende Umsetzung: ein vergessenes --venv
|
||||
(No module named 'argon2' im Trading-Venv) oder eine Abhaengigkeit, die nur der Stack hat
|
||||
(playwright). Gefunden 2026-09-24 in der Pruefung des Werkzeugs.
|
||||
"""
|
||||
treffer = FEHLENDES_MODUL.search(fehlerzeile)
|
||||
if not treffer:
|
||||
return None
|
||||
oben = treffer.group(1).split(".")[0]
|
||||
eigene = set()
|
||||
for ziel in ziele:
|
||||
eigene.update(Path(ziel).parts[:-1])
|
||||
eigene.add(Path(ziel).stem)
|
||||
if oben in eigene or any(kopie.rglob(f"{oben}.py")) or any(p.is_dir() for p in kopie.rglob(oben)):
|
||||
return None
|
||||
return treffer.group(1)
|
||||
|
||||
|
||||
def nimm_wert(argumente: list[str], schalter: str) -> str | None:
|
||||
if schalter not in argumente:
|
||||
return None
|
||||
stelle = argumente.index(schalter)
|
||||
if stelle + 1 >= len(argumente) or argumente[stelle + 1].startswith("--"):
|
||||
raise ValueError(f"{schalter} ohne Wert")
|
||||
wert = argumente[stelle + 1]
|
||||
del argumente[stelle : stelle + 2]
|
||||
return wert
|
||||
|
||||
|
||||
def erste_fehlerzeile(ausgabe: str) -> str:
|
||||
for zeile in ausgabe.splitlines():
|
||||
kern = zeile.strip()
|
||||
if kern.startswith(("E ", "FAILED", "ERROR", "AssertionError", "Error:", "TypeError", "ReferenceError")) or " FAIL " in zeile:
|
||||
return kern[:220]
|
||||
return ""
|
||||
|
||||
|
||||
def main() -> int:
|
||||
argumente = sys.argv[1:]
|
||||
try:
|
||||
fixturen_arg = nimm_wert(argumente, "--fixturen")
|
||||
node_modules_wert = nimm_wert(argumente, "--node-modules")
|
||||
venv = nimm_wert(argumente, "--venv") or VENV
|
||||
frontend = (nimm_wert(argumente, "--frontend") or FRONTEND).replace("\\", "/").strip("/")
|
||||
projektname = (nimm_wert(argumente, "--projektname") or PROJEKTNAME).replace("\\", "/").strip("/").lower() + "/"
|
||||
except ValueError as fehler:
|
||||
print(f"AUFRUF FALSCH: {fehler}")
|
||||
return 3
|
||||
fixturen = Path(fixturen_arg) if fixturen_arg else None
|
||||
node_modules_arg = Path(node_modules_wert) if node_modules_wert else None
|
||||
if not VENV_MUSTER.match(venv):
|
||||
print(f"AUFRUF FALSCH: --venv {venv!r} -- erlaubt sind nur ~, Buchstaben, Ziffern und ._/-")
|
||||
return 3
|
||||
if not FRONTEND_MUSTER.match(frontend) or ".." in frontend.split("/"):
|
||||
print(f"AUFRUF FALSCH: --frontend {frontend!r} -- ein Ordner relativ zum Projekt, ohne ..")
|
||||
return 3
|
||||
conftest_als_test = False
|
||||
if "--conftest-als-test" in argumente:
|
||||
conftest_als_test = True
|
||||
argumente.remove("--conftest-als-test")
|
||||
if len(argumente) != 4:
|
||||
print(__doc__)
|
||||
return 3
|
||||
print(f"Einstellungen: venv={venv} frontend={frontend} projektname={projektname}")
|
||||
basis, planordner, arbeit, spec_pfad = (Path(a) for a in argumente)
|
||||
if not basis.is_dir() or not planordner.is_dir() or not spec_pfad.is_file():
|
||||
print("VORAUSSETZUNG FEHLT: Basis-Kopie, Planordner oder Spec")
|
||||
return 3
|
||||
arbeit.mkdir(parents=True, exist_ok=True)
|
||||
# Den langen Namen erzwingen: Kommt der Arbeitsordner als Windows-Kurzname (PASCAL~1), startet
|
||||
# vitest dort, loest die Dateien aber ueber den langen Namen auf und meldet "Cannot find module"
|
||||
# fuer die eigene Testdatei -- ein ROT, das nichts misst (2026-09-24, Hub-Tasks 24, 25, 29, 37
|
||||
# und Trading-Task 395).
|
||||
arbeit = arbeit.resolve()
|
||||
node_modules = None
|
||||
for kandidat in (node_modules_arg, basis / frontend / "node_modules"):
|
||||
if kandidat is not None and kandidat.exists():
|
||||
node_modules = kandidat
|
||||
break
|
||||
venv_geprueft: bool | None = None # erst pruefen, wenn ein Task Python-Tests hat
|
||||
ergebnisse = []
|
||||
for eintrag in json.loads(spec_pfad.read_text(encoding="utf-8")):
|
||||
task, voraus = eintrag["task"], eintrag.get("vor", [])
|
||||
name = task.split("-")[1] if "-" in task else task
|
||||
vor, rot = arbeit / f"{name}_vor", arbeit / f"{name}_rot"
|
||||
for ordner in (vor, rot):
|
||||
loesche(ordner, frontend)
|
||||
shutil.copytree(basis, vor, ignore=shutil.ignore_patterns("node_modules", "__pycache__"))
|
||||
fehler = wende_an(planordner, vor, voraus)
|
||||
shutil.copytree(vor, rot)
|
||||
fehler = fehler or wende_an(planordner, rot, [task])
|
||||
if fehler:
|
||||
ergebnisse.append((task, "ANWENDEN GESCHEITERT", fehler.replace("\n", " | ")))
|
||||
print(f"{task}: ANWENDEN GESCHEITERT: {fehler[-300:]}")
|
||||
continue
|
||||
ziele = zieldateien(planordner / task, projektname)
|
||||
tests = [z for z in ziele if ist_test(z, conftest_als_test) and not z.startswith("tests/fixtures/")]
|
||||
if CONFTEST in ziele:
|
||||
# Sichtbar machen, was mit conftest.py geschah -- der Koordinator liest die erste Fehlerzeile
|
||||
# anders, wenn eine Fixture fehlt, als wenn eine Korrektur zurueckgesetzt wurde.
|
||||
wie = "als Testdatei stehen gelassen (--conftest-als-test)" if conftest_als_test else "als Produktivseite auf den Stand vor dem Task zurueckgesetzt"
|
||||
print(f"{task}: Hinweis: {CONFTEST} {wie}")
|
||||
for ziel in ziele:
|
||||
if ist_test(ziel, conftest_als_test):
|
||||
if not (rot / ziel).exists() and fixturen is not None and (fixturen / Path(ziel).name).is_file():
|
||||
(rot / ziel).parent.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copyfile(fixturen / Path(ziel).name, rot / ziel)
|
||||
continue
|
||||
if (vor / ziel).is_file():
|
||||
shutil.copyfile(vor / ziel, rot / ziel)
|
||||
elif (rot / ziel).is_file():
|
||||
(rot / ziel).unlink()
|
||||
# Ordner, die erst der Task angelegt hat, gehen mit, sobald sie leer sind: Ein leeres
|
||||
# hub/auth/ liest Python als Namespace-Paket, und die Fehlerzeile nannte dann das
|
||||
# Untermodul statt des fehlenden Pakets (2026-09-24, Nexhorizons Hub, Tasks 7, 9, 11).
|
||||
ordner = (rot / ziel).parent
|
||||
while ordner != rot and not (vor / ordner.relative_to(rot)).exists() and not any(ordner.iterdir()):
|
||||
ordner.rmdir()
|
||||
ordner = ordner.parent
|
||||
python_tests = [t for t in tests if t.endswith(".py")]
|
||||
ober_tests = [t for t in tests if not t.endswith(".py")]
|
||||
zeilen = []
|
||||
if python_tests and venv_geprueft is None:
|
||||
venv_geprueft = venv_da(venv)
|
||||
if python_tests and not venv_geprueft:
|
||||
zeilen.append(f"VORAUSSETZUNG FEHLT: {venv}/bin/python ist in WSL nicht ausfuehrbar")
|
||||
elif python_tests:
|
||||
befehl = pytest_befehl(rot, python_tests, venv)
|
||||
lauf = subprocess.run(["wsl.exe", "-d", "Debian", "--", "bash", "-lc", befehl],
|
||||
capture_output=True, text=True, encoding="utf-8", errors="replace")
|
||||
ausgabe = lauf.stdout
|
||||
summe = next((z.strip() for z in reversed(ausgabe.splitlines()) if re.search(r"\d+ (passed|failed|error)", z)), "keine Summenzeile")
|
||||
# Gruen ist nur ein Lauf mit Summenzeile und ohne Fehlschlag. Ohne Summenzeile hat pytest gar
|
||||
# nicht gesammelt (etwa ein ImportError in tests/conftest.py) -- das ist rot, nicht gruen.
|
||||
# Gefunden am 2026-09-18: Task 172 stand als GRUEN da, obwohl kein einziger Test lief.
|
||||
rot_ja = bool(re.search(r"\d+ (failed|error)", summe)) or summe == "keine Summenzeile"
|
||||
erste = erste_fehlerzeile(ausgabe)
|
||||
fremd = fremdes_modul(erste, ziele, rot) if rot_ja else None
|
||||
if fremd:
|
||||
zeilen.append(f"VORAUSSETZUNG FEHLT: Modul {fremd!r} liegt weder im Projekt noch im Venv {venv} | pytest: {summe} | {erste}")
|
||||
else:
|
||||
zeilen.append(("ROT " if rot_ja else "GRUEN") + f" pytest: {summe} | {erste}")
|
||||
if ober_tests:
|
||||
if node_modules is None:
|
||||
zeilen.append("VORAUSSETZUNG FEHLT: node_modules in der Basis-Kopie")
|
||||
elif not (rot / frontend).is_dir():
|
||||
zeilen.append(f"VORAUSSETZUNG FEHLT: Frontend-Ordner {frontend} fehlt in der Kopie")
|
||||
else:
|
||||
import _winapi
|
||||
_winapi.CreateJunction(str(node_modules.resolve()), str(rot / frontend / "node_modules"))
|
||||
relativ = [t.removeprefix(f"{frontend}/") for t in ober_tests]
|
||||
lauf = subprocess.run(["npx.cmd", "vitest", "run", *relativ], cwd=rot / frontend,
|
||||
capture_output=True, text=True, encoding="utf-8", errors="replace")
|
||||
ausgabe = re.sub(r"\x1b\[[0-9;]*m", "", lauf.stdout + lauf.stderr)
|
||||
summe = next((z.strip() for z in ausgabe.splitlines() if z.strip().startswith("Tests ")), "")
|
||||
if not summe:
|
||||
# Ohne Summenzeile ist vitest nicht bis zum Ende gekommen (Importfehler, Startfehler):
|
||||
# das Ende der Ausgabe zeigen, statt "rot" zu behaupten, ohne zu wissen woran.
|
||||
summe = "keine Summenzeile; Ende der Ausgabe: " + " / ".join(z.strip() for z in ausgabe.splitlines()[-6:] if z.strip())[:300]
|
||||
rot_ja = "failed" in summe or lauf.returncode != 0
|
||||
if "Denied ID" in ausgabe:
|
||||
# Vite liefert keine Datei ausserhalb seines Wurzelordners, und durch die Junction
|
||||
# liegt node_modules ausserhalb der Kopie: Ein Test mit "?raw" aus node_modules
|
||||
# scheitert dann als ganze Datei (2026-09-24, Nexhorizons Hub, Task 42).
|
||||
zeilen.append(f"VORAUSSETZUNG FEHLT: Vite verweigert eine Datei hinter der Junction auf node_modules; mit echter Kopie von node_modules fahren | vitest: {summe}")
|
||||
else:
|
||||
# Bei einer Datei, die schon am Import scheitert, steht vor der Fehlermeldung nur
|
||||
# die FAIL-Zeile; die Meldung selbst zeigt erst, ob der Grund der gemeinte ist.
|
||||
erste = erste_fehlerzeile(ausgabe)
|
||||
meldung = next((z.strip()[:220] for z in ausgabe.splitlines()
|
||||
if z.strip().startswith(("Error:", "AssertionError", "TypeError", "ReferenceError", "SyntaxError"))), "")
|
||||
zusatz = f" | {meldung}" if meldung and meldung not in erste else ""
|
||||
zeilen.append(("ROT " if rot_ja else "GRUEN") + f" vitest: {summe} | {erste}{zusatz}")
|
||||
if not tests:
|
||||
zeilen.append("ohne Testdatei unter den Zieldateien")
|
||||
for zeile in zeilen:
|
||||
print(f"{task}: {zeile}")
|
||||
ergebnisse.append((task, zeile[:5].strip(), zeile))
|
||||
for ordner in (vor, rot):
|
||||
loesche(ordner, frontend)
|
||||
gruen = [e for e in ergebnisse if e[1] == "GRUEN"]
|
||||
ohne_lauf = [e for e in ergebnisse if e[1] in ("VORAU", "ANWENDEN GESCHEITERT")]
|
||||
print(f"\n{len(ergebnisse)} Laeufe, davon {len(gruen)} gruen ohne Umsetzung und {len(ohne_lauf)} ohne Lauf "
|
||||
"(Voraussetzung fehlt oder Anwenden gescheitert).")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
sys.exit(main())
|
||||
except Exception as ausnahme: # noqa: BLE001 - ein Absturz des Werkzeugs ist selbst ein Befund
|
||||
print(f"BEFUND: das Werkzeug ist abgebrochen: {ausnahme!r}")
|
||||
sys.exit(2)
|
||||
@@ -0,0 +1,180 @@
|
||||
"""Gegenpruefung nach einer Executer-Welle: Menge der geaenderten Dateien und Byte-Vergleich gegen die Kopie.
|
||||
|
||||
Koordinator-Werkzeug im Scratchpad. Fragen und Herkunft der Antwort:
|
||||
1. Welche Dateien hat die Welle geaendert? Quelle: `git status --porcelain -z` im Projekt.
|
||||
2. Welche durfte sie aendern? Quelle: Abschnitt "## Zieldateien" der uebergebenen Taskdateien.
|
||||
3. Ist jede Zieldatei byte-gleich mit dem Plan? Quelle: die Kopie `mat`, auf der plan_materialisiere.py
|
||||
alle Codebloecke des Plans angewendet hat.
|
||||
Was es NICHT sieht (Fangnetz, kein Nachweis):
|
||||
- Dateien, die .gitignore ausblendet (data/, artifacts/, node_modules/): Ein Executer, der dorthin
|
||||
schreibt, bleibt unsichtbar.
|
||||
- Ob die Kopie selbst sauber ist. Eine verschmutzte Kopie erzeugt Fehlalarm, kein falsches Gruen.
|
||||
- Prosa-Dateien (--prosa): nur Mengenpruefung, kein Byte-Vergleich; die liest der Koordinator selbst.
|
||||
- Bereits committete Aenderungen: Verglichen wird der Arbeitsbaum gegen HEAD.
|
||||
Eine fehlende Voraussetzung (Kopie fehlt, Taskdatei ohne Zieldateien, git laeuft nicht) ist ein Befund.
|
||||
Aufruf: python welle_vergleich.py --projekt <pfad> --mat <pfad> [--prosa <rel> ...] <taskdatei> [...]
|
||||
Exit 0 ohne Befund, 2 mit Befund.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
import traceback
|
||||
from pathlib import Path, PureWindowsPath
|
||||
|
||||
# Seit 2026-09-18 ist die Klammer hinter dem Pfad wahlfrei, und dahinter darf Text stehen
|
||||
# ("- `pfad` -- nur die Konstante X"): Plaene nennen Zieldateien nicht immer mit "(neu)".
|
||||
ZIEL = re.compile(r"^- `(.+?)`(?:\s*\((.*?)\))?")
|
||||
|
||||
|
||||
def zieldateien(taskdatei: Path, projekt: Path, befunde: list[str]) -> dict[str, bool]:
|
||||
"""Relativer Pfad -> True, wenn die Datei geloescht werden soll."""
|
||||
try:
|
||||
zeilen = taskdatei.read_text(encoding="utf-8").replace("\r\n", "\n").split("\n")
|
||||
except OSError as fehler:
|
||||
befunde.append(f"{taskdatei.name}: nicht lesbar ({fehler})")
|
||||
return {}
|
||||
drin = False
|
||||
ziele: dict[str, bool] = {}
|
||||
for zeile in zeilen:
|
||||
if zeile.startswith("## "):
|
||||
drin = zeile.startswith("## Zieldateien")
|
||||
continue
|
||||
if not drin:
|
||||
continue
|
||||
treffer = ZIEL.match(zeile)
|
||||
if not treffer:
|
||||
continue
|
||||
absolut = PureWindowsPath(treffer.group(1))
|
||||
if absolut.drive == "" and absolut.root == "":
|
||||
# Seit 2026-09-18: Ein Pfad ohne Laufwerk und ohne Wurzel gilt relativ zum Projekt
|
||||
# (Plaene fuer einen Git-Worktree nennen keine absoluten Pfade). Ein ".." fuehrte
|
||||
# aus dem Projekt hinaus und ist ein Befund wie ein fremder absoluter Pfad.
|
||||
if ".." in absolut.parts:
|
||||
befunde.append(f"{taskdatei.name}: Zieldatei {absolut} liegt nicht unter dem Projekt {projekt}")
|
||||
continue
|
||||
rel = absolut
|
||||
else:
|
||||
try:
|
||||
rel = absolut.relative_to(PureWindowsPath(str(projekt)))
|
||||
except ValueError:
|
||||
befunde.append(f"{taskdatei.name}: Zieldatei {absolut} liegt nicht unter dem Projekt {projekt}")
|
||||
continue
|
||||
klammer = treffer.group(2) or ""
|
||||
ziele[rel.as_posix()] = "gelöscht" in klammer or "geloescht" in klammer
|
||||
if not ziele:
|
||||
befunde.append(f"{taskdatei.name}: keine Zieldateien gefunden -- ohne sie misst der Lauf nichts")
|
||||
return ziele
|
||||
|
||||
|
||||
def geaendert(projekt: Path, befunde: list[str]) -> dict[str, str]:
|
||||
"""Relativer Pfad -> zweistelliger Status aus git."""
|
||||
try:
|
||||
ergebnis = subprocess.run(
|
||||
["git", "-C", str(projekt), "status", "--porcelain", "-z", "--untracked-files=all"],
|
||||
capture_output=True,
|
||||
stdin=subprocess.DEVNULL,
|
||||
)
|
||||
except OSError as fehler:
|
||||
befunde.append(f"git laesst sich nicht starten ({fehler})")
|
||||
return {}
|
||||
if ergebnis.returncode != 0:
|
||||
befunde.append(f"git status endet mit {ergebnis.returncode}: {ergebnis.stderr.decode('utf-8', 'replace').strip()}")
|
||||
return {}
|
||||
teile = ergebnis.stdout.decode("utf-8", "replace").split("\0")
|
||||
aus: dict[str, str] = {}
|
||||
i = 0
|
||||
while i < len(teile):
|
||||
eintrag = teile[i]
|
||||
i += 1
|
||||
if len(eintrag) < 4:
|
||||
continue
|
||||
status, pfad = eintrag[:2], eintrag[3:]
|
||||
if status[0] in "RC":
|
||||
alt = teile[i] if i < len(teile) else "?"
|
||||
i += 1
|
||||
befunde.append(f"{pfad}: git meldet Umbenennung oder Kopie aus {alt} -- im Plan nicht vorgesehen")
|
||||
aus[pfad] = status
|
||||
return aus
|
||||
|
||||
|
||||
def erste_abweichung(echt: bytes, kopie: bytes) -> str:
|
||||
if echt.replace(b"\r\n", b"\n") == kopie.replace(b"\r\n", b"\n"):
|
||||
return "nur die Zeilenenden weichen ab (CRLF gegen LF)"
|
||||
a, b = echt.split(b"\n"), kopie.split(b"\n")
|
||||
for nummer, (links, rechts) in enumerate(zip(a, b), start=1):
|
||||
if links != rechts:
|
||||
return f"Zeile {nummer}: Baum {links[:160]!r} | Plan {rechts[:160]!r}"
|
||||
return f"Laenge: Baum {len(a)} Zeilen, Plan {len(b)} Zeilen; die gemeinsamen Zeilen sind gleich"
|
||||
|
||||
|
||||
def pruefe(args: argparse.Namespace, befunde: list[str]) -> None:
|
||||
projekt, mat = Path(args.projekt), Path(args.mat)
|
||||
if not projekt.is_dir():
|
||||
befunde.append(f"Projektordner {projekt} fehlt")
|
||||
return
|
||||
if not mat.is_dir():
|
||||
befunde.append(f"Kopie {mat} fehlt -- ohne sie gibt es keinen Byte-Vergleich")
|
||||
return
|
||||
prosa = {p.replace("\\", "/") for p in args.prosa}
|
||||
ziele: dict[str, bool] = {}
|
||||
for name in args.taskdateien:
|
||||
ziele.update(zieldateien(Path(name), projekt, befunde))
|
||||
status = geaendert(projekt, befunde)
|
||||
|
||||
for pfad in sorted(set(status) - set(ziele)):
|
||||
befunde.append(f"{pfad}: geaendert ({status[pfad]!r}), liegt aber ausserhalb der Zieldateien dieser Welle")
|
||||
for pfad in sorted(prosa - set(ziele)):
|
||||
befunde.append(f"{pfad}: als Prosa uebergeben, steht aber in keiner Taskdatei als Zieldatei")
|
||||
|
||||
gleich = 0
|
||||
for pfad, loeschen in sorted(ziele.items()):
|
||||
echt, kopie = projekt / pfad, mat / pfad
|
||||
if loeschen:
|
||||
if echt.exists():
|
||||
befunde.append(f"{pfad}: soll geloescht sein, steht aber noch im Baum")
|
||||
if kopie.exists():
|
||||
befunde.append(f"{pfad}: soll geloescht sein, steht aber noch in der Kopie")
|
||||
if not echt.exists() and "D" not in status.get(pfad, ""):
|
||||
befunde.append(f"{pfad}: fehlt im Baum, git meldet aber keine Loeschung ({status.get(pfad)!r})")
|
||||
continue
|
||||
if pfad not in status:
|
||||
befunde.append(f"{pfad}: Zieldatei ist unveraendert gegenueber HEAD")
|
||||
continue
|
||||
if not echt.is_file():
|
||||
befunde.append(f"{pfad}: Zieldatei fehlt im Baum")
|
||||
continue
|
||||
if pfad in prosa:
|
||||
continue
|
||||
if not kopie.is_file():
|
||||
befunde.append(f"{pfad}: fehlt in der Kopie -- kein Byte-Vergleich moeglich")
|
||||
continue
|
||||
a, b = echt.read_bytes(), kopie.read_bytes()
|
||||
if a == b:
|
||||
gleich += 1
|
||||
else:
|
||||
befunde.append(f"{pfad}: weicht vom Plan ab -- {erste_abweichung(a, b)}")
|
||||
print(f"{len(ziele)} Zieldateien, {len(status)} geaenderte Dateien laut git, {gleich} byte-gleich mit der Kopie, {len(prosa & set(ziele))} nur als Menge geprueft.")
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--projekt", required=True)
|
||||
parser.add_argument("--mat", required=True)
|
||||
parser.add_argument("--prosa", nargs="*", default=[])
|
||||
parser.add_argument("taskdateien", nargs="+")
|
||||
args = parser.parse_args()
|
||||
befunde: list[str] = []
|
||||
try:
|
||||
pruefe(args, befunde)
|
||||
except Exception: # Ein Absturz ist selbst ein Befund; die gesammelten Befunde gehen trotzdem raus.
|
||||
befunde.append("unerwarteter Abbruch: " + traceback.format_exc().strip().splitlines()[-1])
|
||||
for befund in befunde:
|
||||
print("BEFUND " + befund)
|
||||
print(f"{len(befunde)} Befund(e).")
|
||||
return 2 if befunde else 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,212 @@
|
||||
"""Regressionsnachweis fuer welle_vergleich.py: Grundlinie zuerst, dann jeder Angriffsfall einzeln auf
|
||||
einer frischen Wegwerfkopie. Je Fall werden Exit-Code und ausloesende Meldung geprueft.
|
||||
Aufruf: python welle_vergleich_treiber.py Exit 0, wenn jeder Fall so ausgeht wie erwartet.
|
||||
"""
|
||||
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
WERKZEUG = Path(__file__).parent / "welle_vergleich.py"
|
||||
|
||||
|
||||
def git(ordner: Path, *args: str) -> None:
|
||||
subprocess.run(["git", "-C", str(ordner), *args], check=True, capture_output=True, stdin=subprocess.DEVNULL)
|
||||
|
||||
|
||||
def schreibe(pfad: Path, text: str) -> None:
|
||||
pfad.parent.mkdir(parents=True, exist_ok=True)
|
||||
pfad.write_bytes(text.encode("utf-8"))
|
||||
|
||||
|
||||
def baue(wurzel: Path) -> tuple[Path, Path, Path]:
|
||||
"""Repo mit drei committeten Dateien, danach die 'Welle': a.py geaendert, sub/neu.py neu, weg.py geloescht,
|
||||
doku.md geaendert (Prosa). Die Kopie traegt denselben Stand."""
|
||||
projekt, mat = wurzel / "projekt", wurzel / "mat"
|
||||
projekt.mkdir()
|
||||
git(projekt, "init", "-q")
|
||||
git(projekt, "config", "user.email", "[email protected]")
|
||||
git(projekt, "config", "user.name", "Treiber")
|
||||
git(projekt, "config", "core.autocrlf", "false")
|
||||
schreibe(projekt / "a.py", "eins\nzwei\ndrei\n")
|
||||
schreibe(projekt / "b.py", "bleibt\n")
|
||||
schreibe(projekt / "weg.py", "weg\n")
|
||||
schreibe(projekt / "doku.md", "alt\n")
|
||||
git(projekt, "add", "-A")
|
||||
git(projekt, "commit", "-q", "-m", "basis")
|
||||
schreibe(projekt / "a.py", "EINS\nzwei\nDREI\n")
|
||||
schreibe(projekt / "sub" / "neu.py", "neu eins\nneu zwei\n")
|
||||
(projekt / "weg.py").unlink()
|
||||
schreibe(projekt / "doku.md", "neue Prosa des Executers\n")
|
||||
schreibe(mat / "a.py", "EINS\nzwei\nDREI\n")
|
||||
schreibe(mat / "b.py", "bleibt\n")
|
||||
schreibe(mat / "sub" / "neu.py", "neu eins\nneu zwei\n")
|
||||
schreibe(mat / "doku.md", "anderer Stand in der Kopie\n")
|
||||
task = wurzel / "task-01.md"
|
||||
schreibe(
|
||||
task,
|
||||
"# Task\n\n## Zieldateien\n\n"
|
||||
f"- `{projekt}\\a.py` (geändert)\n"
|
||||
f"- `{projekt}\\sub\\neu.py` (neu)\n"
|
||||
f"- `{projekt}\\weg.py` (gelöscht)\n"
|
||||
f"- `{projekt}\\doku.md` (geändert)\n\n"
|
||||
"Keine andere Datei wird angefasst.\n\n## Regeln\n\n- `nicht/gemeint.py` (kein Ziel)\n",
|
||||
)
|
||||
return projekt, mat, task
|
||||
|
||||
|
||||
def fall_grundlinie(projekt, mat, task, wurzel):
|
||||
pass
|
||||
|
||||
|
||||
def fall_fremde_datei(projekt, mat, task, wurzel):
|
||||
schreibe(projekt / "fremd.py", "x\n")
|
||||
|
||||
|
||||
def fall_ziel_unveraendert(projekt, mat, task, wurzel):
|
||||
schreibe(projekt / "a.py", "eins\nzwei\ndrei\n")
|
||||
|
||||
|
||||
def fall_abweichung_erste_zeile(projekt, mat, task, wurzel):
|
||||
schreibe(projekt / "a.py", "Eins\nzwei\nDREI\n")
|
||||
|
||||
|
||||
def fall_abweichung_letzte_zeile(projekt, mat, task, wurzel):
|
||||
schreibe(projekt / "a.py", "EINS\nzwei\nDrei\n")
|
||||
|
||||
|
||||
def fall_abweichung_letzte_zieldatei(projekt, mat, task, wurzel):
|
||||
schreibe(projekt / "sub" / "neu.py", "neu eins\nneu ZWEI\n")
|
||||
|
||||
|
||||
def fall_crlf(projekt, mat, task, wurzel):
|
||||
schreibe(projekt / "a.py", "EINS\r\nzwei\r\nDREI\r\n")
|
||||
|
||||
|
||||
def fall_fehlt_in_kopie(projekt, mat, task, wurzel):
|
||||
(mat / "sub" / "neu.py").unlink()
|
||||
|
||||
|
||||
def fall_ohne_zieldateien(projekt, mat, task, wurzel):
|
||||
schreibe(task, "# Task\n\n## Regeln\n\nnichts\n")
|
||||
|
||||
|
||||
def fall_kopie_fehlt(projekt, mat, task, wurzel):
|
||||
shutil.rmtree(mat)
|
||||
|
||||
|
||||
def fall_nicht_geloescht(projekt, mat, task, wurzel):
|
||||
schreibe(projekt / "weg.py", "weg\n")
|
||||
|
||||
|
||||
def fall_prosa_unveraendert(projekt, mat, task, wurzel):
|
||||
schreibe(projekt / "doku.md", "alt\n")
|
||||
|
||||
|
||||
def fall_ziel_ausserhalb(projekt, mat, task, wurzel):
|
||||
text = task.read_text(encoding="utf-8")
|
||||
schreibe(task, text.replace("## Regeln", "") .replace("Keine andere Datei", "- `C:\\anderswo\\x.py` (neu)\n\nKeine andere Datei"))
|
||||
|
||||
|
||||
def fall_laengere_datei(projekt, mat, task, wurzel):
|
||||
schreibe(projekt / "a.py", "EINS\nzwei\nDREI\nvier\n")
|
||||
|
||||
|
||||
def _relativ(task, projekt):
|
||||
"""Schreibt die Zieldateien der Taskdatei relativ zum Projekt, eine davon mit Schraegstrich."""
|
||||
text = task.read_text(encoding="utf-8")
|
||||
schreibe(task, text.replace(f"{projekt}\\", "").replace("sub\\neu.py", "sub/neu.py"))
|
||||
|
||||
|
||||
def fall_relativ_grundlinie(projekt, mat, task, wurzel):
|
||||
_relativ(task, projekt)
|
||||
|
||||
|
||||
def fall_relativ_abweichung(projekt, mat, task, wurzel):
|
||||
_relativ(task, projekt)
|
||||
schreibe(projekt / "sub" / "neu.py", "neu eins\nneu ZWEI\n")
|
||||
|
||||
|
||||
def _ohne_klammer(task):
|
||||
"""Nimmt den Zieldateien die Klammer (ausser der Loeschmarke) und haengt einer Zeile Text an."""
|
||||
text = task.read_text(encoding="utf-8")
|
||||
text = text.replace("a.py` (geändert)", "a.py` — **nur die erste und die letzte Zeile**")
|
||||
schreibe(task, text.replace(" (neu)", "").replace("doku.md` (geändert)", "doku.md`"))
|
||||
|
||||
|
||||
def fall_ohne_klammer_grundlinie(projekt, mat, task, wurzel):
|
||||
_ohne_klammer(task)
|
||||
|
||||
|
||||
def fall_ohne_klammer_abweichung(projekt, mat, task, wurzel):
|
||||
_ohne_klammer(task)
|
||||
schreibe(projekt / "a.py", "Eins\nzwei\nDREI\n")
|
||||
|
||||
|
||||
def fall_relativ_nach_aussen(projekt, mat, task, wurzel):
|
||||
_relativ(task, projekt)
|
||||
text = task.read_text(encoding="utf-8")
|
||||
schreibe(task, text.replace("Keine andere Datei", "- `../anderswo/x.py` (neu)\n\nKeine andere Datei"))
|
||||
|
||||
|
||||
FAELLE = [
|
||||
("Grundlinie", fall_grundlinie, 0, "0 Befund(e)."),
|
||||
("fremde Datei im Baum", fall_fremde_datei, 2, "fremd.py: geaendert"),
|
||||
("Zieldatei unveraendert", fall_ziel_unveraendert, 2, "a.py: Zieldatei ist unveraendert"),
|
||||
("Abweichung in der ersten Zeile", fall_abweichung_erste_zeile, 2, "a.py: weicht vom Plan ab -- Zeile 1"),
|
||||
("Abweichung in der letzten Zeile", fall_abweichung_letzte_zeile, 2, "a.py: weicht vom Plan ab -- Zeile 3"),
|
||||
("Abweichung in der letzten Zieldatei", fall_abweichung_letzte_zieldatei, 2, "sub/neu.py: weicht vom Plan ab -- Zeile 2"),
|
||||
("CRLF statt LF", fall_crlf, 2, "nur die Zeilenenden weichen ab"),
|
||||
("Datei fehlt in der Kopie", fall_fehlt_in_kopie, 2, "sub/neu.py: fehlt in der Kopie"),
|
||||
("Taskdatei ohne Zieldateien", fall_ohne_zieldateien, 2, "keine Zieldateien gefunden"),
|
||||
("Kopie fehlt", fall_kopie_fehlt, 2, "fehlt -- ohne sie gibt es keinen Byte-Vergleich"),
|
||||
("Loeschung nicht ausgefuehrt", fall_nicht_geloescht, 2, "weg.py: soll geloescht sein, steht aber noch im Baum"),
|
||||
("Prosa-Datei unveraendert", fall_prosa_unveraendert, 2, "doku.md: Zieldatei ist unveraendert"),
|
||||
("Zieldatei ausserhalb des Projekts", fall_ziel_ausserhalb, 2, "liegt nicht unter dem Projekt"),
|
||||
# Die angehaengte Zeile steht der leeren Restzeile der Kopie gegenueber, deshalb "Zeile 4" und nicht "Laenge".
|
||||
("Datei laenger als der Plan", fall_laengere_datei, 2, "a.py: weicht vom Plan ab -- Zeile 4: Baum b'vier'"),
|
||||
# Seit 2026-09-18: Zieldateien relativ zum Projekt (Plaene fuer einen Git-Worktree).
|
||||
("Zieldateien relativ zum Projekt", fall_relativ_grundlinie, 0, "0 Befund(e)."),
|
||||
("relative Zieldatei weicht ab", fall_relativ_abweichung, 2, "sub/neu.py: weicht vom Plan ab -- Zeile 2"),
|
||||
("relative Zieldatei fuehrt nach aussen", fall_relativ_nach_aussen, 2, "liegt nicht unter dem Projekt"),
|
||||
# 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"),
|
||||
]
|
||||
|
||||
|
||||
def main() -> int:
|
||||
fehler = 0
|
||||
for name, angriff, exit_erwartet, meldung in FAELLE:
|
||||
wurzel = Path(tempfile.mkdtemp(prefix="wv_"))
|
||||
try:
|
||||
projekt, mat, task = baue(wurzel)
|
||||
angriff(projekt, mat, task, wurzel)
|
||||
lauf = subprocess.run(
|
||||
[sys.executable, "-X", "utf8", str(WERKZEUG), "--projekt", str(projekt), "--mat", str(mat),
|
||||
"--prosa", "doku.md", "--", str(task)],
|
||||
capture_output=True, text=True, encoding="utf-8", errors="replace", stdin=subprocess.DEVNULL,
|
||||
)
|
||||
ausgabe = lauf.stdout + lauf.stderr
|
||||
gut = lauf.returncode == exit_erwartet and meldung in ausgabe
|
||||
if name != "Grundlinie" and gut:
|
||||
# Genau ein Befund je Fall, ausser wo der Angriff zwangslaeufig zwei ausloest.
|
||||
pass
|
||||
print(f"{'ok ' if gut else 'FEHLER'} {name}: Exit {lauf.returncode} (erwartet {exit_erwartet})")
|
||||
if not gut:
|
||||
fehler += 1
|
||||
print(" erwartete Meldung: " + meldung)
|
||||
print(" " + ausgabe.strip().replace("\n", "\n "))
|
||||
if name == "Grundlinie":
|
||||
print("Die Grundlinie ist nicht sauber -- der Rest misst nichts.")
|
||||
return 2
|
||||
finally:
|
||||
shutil.rmtree(wurzel, ignore_errors=True)
|
||||
print(f"{fehler} Fall/Faelle mit falschem Ausgang.")
|
||||
return 2 if fehler else 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in new issue
Block a user