--- 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 `::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 --mutanten [--nur ID,ID,…] [--nur-muster] [--bericht ] [--zeitlimit ] ``` `--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:** ` -m pytest -q -p no:cacheprovider -p _mutanten_lauf_plugin ` 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 | Ausgabe auf stdout, UTF-8, je Mutant eine Zeile: `D1 OK DA::test_x=ROT() …`. 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: Mutanten, OK, LUECKE, UNKLAR, ZEIT, 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=`, `PYTHONPATH` mit dem Plugin-Ordner vorn. **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 | 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`).