Files
claude-regelwerk/.vault-tools/plan-kopie/mutanten_lauf_ENTWURF.md
T

206 lines
17 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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-09-27
---
# 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. |
## 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.
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 <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`.
**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.
**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"}]}], "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.
**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]`), 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 |
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`).