Erledigt in 4.1.1 (23307c3). Der Befund unten ist die Diagnose, die stehen bleibt; die Entscheidungen sind getroffen und umgesetzt. Ein Prozess-Nachtrag ging als 4.1.2 (41f5dfe) hinterher — siehe „Nachtrag" am Ende.
Der Befund
tools/chemenu/tests/conftest.py hatte zwei Fixtures, die eine Baumwurzel aufbauen, und nur eine von beiden richtete config.ROOT mit aus:
Fixture
config.ROOT (vor 4.1.1)
Folge
raw_dir
monkeypatch.setattr(config, "ROOT", tmp_path)
isoliert
kb_dir
—
jeder Zugriff über config.ROOT traf das echte Repo
kb_dir baute seinen Baum unter tmp_path / "kb" auf und gab ihn zurück. Das reicht für jeden Test, der den Pfad als Argument weiterreicht (run_lint(kb_dir)). Es reicht nicht, sobald der Code unter Test eine Datei über config.ROOT oder config.KB_DIR auflöst statt über das übergebene Verzeichnis — und das tat er an mehreren Stellen.
Wie es aufgefallen ist
Beim Bau der Migrations-Gate-Tests für 4.1.0. Ein Test rief kb_state.write_kb_state(...), das über kb_state_file() -> config.ROOT / ".wikitool-kb.json" auflöst. Ergebnis:
M .wikitool-kb.json
Der Applied-Ledger des echten Repos war überschrieben — applied: [] statt der beiden Einträge für 3.0.0-authoring-conventions und 4.0.0-link-taxonomy. Zurückgeholt mit git checkout --, die Tests auf eine lokale rooted_kb-Fixture umgestellt.
Das war eine unversionierte, git-verfolgte Datei, also war der Schaden reversibel und sofort sichtbar. Bei einer gitignorierten Datei — oder in einem Baum mit uncommitteten Änderungen — wäre er es nicht gewesen.
Der zweite, stillere Fall
Derselbe Mechanismus machte Prüfungen wirkungslos, statt Dateien zu überschreiben. lint_core._collection_of() löste eine Seite gegen config.KB_DIR auf, nicht gegen das an run_lint() übergebene Verzeichnis:
page.path.relative_to(config.KB_DIR).parts[0]
Für eine Seite unter tmp_path/kb/ wirft das ValueError, die Funktion gab None zurück, und die Label-Autorisierungsprüfung übersprang die Kante stillschweigend. Das hieß: unauthorised_labels war in der Suite faktisch nicht getestet — jeder Test, der es hätte auslösen können, bekam eine leere Liste zurück und behauptete nichts.
Das war die gefährlichere Hälfte. Ein überschriebenes .wikitool-kb.json sieht man in git status. Eine Prüfung, die unter Test nie feuert, sieht man gar nicht, und ihr grüner Testlauf liest sich wie eine Zusicherung.
Warum das nicht die Autouse-Fixture erledigt
hermetic_environment neutralisiert die Umgebung — HOME, XDG_CONFIG_HOME, die Git-Konfiguration, CHEMENU_ROOT. config.ROOT fällt bei fehlendem CHEMENU_ROOT auf _PACKAGE_ROOT zurück, also auf den Checkout, aus dem die Tests laufen. Das ist für die meisten Tests richtig — sie brauchen die ausgelieferten types/ — und genau deshalb greift die Isolation hier nicht von selbst.
configs Modul-__getattr__ löst die abgeleiteten Pfade bei jedem Zugriff auf, monkeypatch.setattr(config, "ROOT", tmp_path) zieht KB_DIR also mit. Der Mechanismus war vorhanden; kb_dir benutzte ihn nur nicht.
Entschieden und umgesetzt
kb_dir repointet selbst (Variante 1, nicht die getrennte rooted_kb-Fixture). Genau das, was raw_dir schon tat: config.ROOT auf das eigene tmp_path, plus use_shipped_type_specs(). Der befürchtete Umfang wurde gemessen statt geschätzt — kein einziger Test kippte (863 vorher). Variante 2 wurde verworfen, weil sie die Falle stehen lässt und die Auswahl zwischen zwei fast identischen Fixturen Wissen wäre, das nirgends steht. rooted_kb ist ersatzlos entfallen.
_collection_of() löst gegen den übergebenen Baum auf, und authorised_labels() bekommt denselben Baum gereicht statt auf config.KB_DIR zurückzufallen. Das ist der eigentliche Fix: eine Funktion, die ein Verzeichnis entgegennimmt, löst dagegen auf — keine Fixture kann diese Form von außen reparieren.
Regressionstest:test_unauthorised_label_is_judged_in_a_tree_that_is_not_the_configured_kb lintet einen Baum, von dem ROOT bewusst wegzeigt. Gegen den alten Code schlägt er fehl (verifiziert), gegen den neuen nicht. Ein Test unter der reparierten Fixture allein hätte nichts bewiesen — dort ist kb_dir == config.KB_DIR, und der Defekt bliebe unsichtbar.
Wächter: ja, zweistufig.
Guard
Default
Kosten
repository_tree_guard (session)
an
zwei git status --porcelain pro Lauf
per_test_tree_guard
aus, CHEMENU_TREE_GUARD=each
ein git status pro Test
Der Session-Wächter vergleicht vorher gegen nachher statt einen sauberen Baum zu verlangen, sagt also nichts über unveröffentlichte Arbeit des Entwicklers. Er benennt den Verursacher nicht — dafür ist CHEMENU_TREE_GUARD=each da. Ohne git oder außerhalb eines Repositorys sind beide still. Beide gegen eine absichtlich schreibende Probe verifiziert.
Bewusst nicht abgedeckt: die stillere Hälfte. Eine Prüfung, die unter Test nichts tut, schreibt keine Datei — kein Wächter dieser Bauart sieht das. Dagegen hilft nur ein Test, der das Finding tatsächlich auslöst. Diese Grenze steht in testing-conventions.md, damit sie nicht als abgedeckt missverstanden wird.
Checkliste
Entscheidung 1 vs. 2 getroffen und umgesetzt; Suite-Lauf auf gekippte Tests geprüft — keine
_collection_of() gegen den übergebenen Baum aufgelöst
Test geschrieben, der unauthorised_labels tatsächlich auslöst
instructions/dev/testing-conventions.md ergänzt (Abschnitt „Which tree a test writes into", ein Checklisten-Schritt, drei Fixture-Tests in test_hermetic_env.py)
Version-Bump --patch → 4.1.1
Verifiziert
867 Tests grün, auch im gehärteten env -i-Lauf; docs verify und instructions verify OK; der Regressionstest gegen den alten Code als fehlschlagend nachgewiesen; beide Wächter gegen eine schreibende Probe getestet.
Nachtrag: der Prozessfehler beim Schließen (4.1.2)
Dieser Issue wurde zunächst mit einem Abschlusskommentar geschlossen, während der Body weiter als offene Arbeit dastand — „Zu entscheiden" über eine längst getroffene Entscheidung, ungehakte Checkliste. Das ist genau der Fehler, den instructions/dev/issue-tracking.md Schritt 2 verhindern soll.
Daraus wurde die Regel geschärft statt nur der Fall repariert (4.1.2, 41f5dfe):
Schritt 2: Der Body ist das Plan-File dieses Stacks — fortlaufend aktuell, nicht am Ende nachgezogen. Maßstab ist der Abbruch: eine Session kann jederzeit enden, und der Body ist zu jedem Zeitpunkt die vollständige Übergabe an die nächste.
Schritt 7: Abschluss ist die letzte dieser Aktualisierungen, kein Sonderakt. Body auf Endstand, dann schließen, dann Changelog-Kommentar.
Schritt 3: Ein Kommentar pro Session-Umfang, nicht pro Edit — sonst erzeugt die fortlaufende Pflege Lärm.
Neuer Abschnitt „What no tool checks":wikitool kennt den Tracker nicht und soll ihn nicht kennenlernen (es wird an Instanzen ausgeliefert, die kein Board haben; instructions/dev/ wird von dist export gepruned). Es gibt also keine mechanische Prüfung für einen Body, der offen klingt, obwohl er es nicht ist — das fängt nur eine Session ab, die dieser Datei folgt.
stack-dev/SKILL.md sagt es beim Aufgreifen mit.
Vorgeschichte
Gefunden am 2026-09-03 beim Bau der Migrations-Gate-Tests für 4.1.0 (#40). Geschlossen am 2026-09-03 mit 4.1.1; Prozess-Nachtrag als 4.1.2.
**Erledigt in 4.1.1** (`23307c3`). Der Befund unten ist die Diagnose, die stehen bleibt; die Entscheidungen sind getroffen und umgesetzt. Ein Prozess-Nachtrag ging als 4.1.2 (`41f5dfe`) hinterher — siehe „Nachtrag" am Ende.
## Der Befund
`tools/chemenu/tests/conftest.py` hatte zwei Fixtures, die eine Baumwurzel aufbauen, und nur eine von beiden richtete `config.ROOT` mit aus:
| Fixture | `config.ROOT` (vor 4.1.1) | Folge |
|---|---|---|
| `raw_dir` | `monkeypatch.setattr(config, "ROOT", tmp_path)` | isoliert |
| `kb_dir` | — | **jeder Zugriff über `config.ROOT` traf das echte Repo** |
`kb_dir` baute seinen Baum unter `tmp_path / "kb"` auf und gab ihn zurück. Das reicht für jeden Test, der den Pfad als Argument weiterreicht (`run_lint(kb_dir)`). Es reicht nicht, sobald der Code unter Test eine Datei über `config.ROOT` oder `config.KB_DIR` auflöst statt über das übergebene Verzeichnis — und das tat er an mehreren Stellen.
## Wie es aufgefallen ist
Beim Bau der Migrations-Gate-Tests für 4.1.0. Ein Test rief `kb_state.write_kb_state(...)`, das über `kb_state_file() -> config.ROOT / ".wikitool-kb.json"` auflöst. Ergebnis:
```
M .wikitool-kb.json
```
Der Applied-Ledger des echten Repos war überschrieben — `applied: []` statt der beiden Einträge für `3.0.0-authoring-conventions` und `4.0.0-link-taxonomy`. Zurückgeholt mit `git checkout --`, die Tests auf eine lokale `rooted_kb`-Fixture umgestellt.
Das war eine unversionierte, git-verfolgte Datei, also war der Schaden reversibel und sofort sichtbar. Bei einer gitignorierten Datei — oder in einem Baum mit uncommitteten Änderungen — wäre er es nicht gewesen.
## Der zweite, stillere Fall
Derselbe Mechanismus machte Prüfungen wirkungslos, statt Dateien zu überschreiben. `lint_core._collection_of()` löste eine Seite gegen `config.KB_DIR` auf, nicht gegen das an `run_lint()` übergebene Verzeichnis:
```python
page.path.relative_to(config.KB_DIR).parts[0]
```
Für eine Seite unter `tmp_path/kb/` wirft das `ValueError`, die Funktion gab `None` zurück, und die Label-Autorisierungsprüfung übersprang die Kante stillschweigend. Das hieß: **`unauthorised_labels` war in der Suite faktisch nicht getestet** — jeder Test, der es hätte auslösen können, bekam eine leere Liste zurück und behauptete nichts.
Das war die gefährlichere Hälfte. Ein überschriebenes `.wikitool-kb.json` sieht man in `git status`. Eine Prüfung, die unter Test nie feuert, sieht man gar nicht, und ihr grüner Testlauf liest sich wie eine Zusicherung.
## Warum das nicht die Autouse-Fixture erledigt
`hermetic_environment` neutralisiert die *Umgebung* — `HOME`, `XDG_CONFIG_HOME`, die Git-Konfiguration, `CHEMENU_ROOT`. `config.ROOT` fällt bei fehlendem `CHEMENU_ROOT` auf `_PACKAGE_ROOT` zurück, also auf den Checkout, aus dem die Tests laufen. Das ist für die meisten Tests richtig — sie brauchen die ausgelieferten `types/` — und genau deshalb greift die Isolation hier nicht von selbst.
`config`s Modul-`__getattr__` löst die abgeleiteten Pfade bei jedem Zugriff auf, `monkeypatch.setattr(config, "ROOT", tmp_path)` zieht `KB_DIR` also mit. Der Mechanismus war vorhanden; `kb_dir` benutzte ihn nur nicht.
## Entschieden und umgesetzt
1. **`kb_dir` repointet selbst** (Variante 1, nicht die getrennte `rooted_kb`-Fixture). Genau das, was `raw_dir` schon tat: `config.ROOT` auf das eigene `tmp_path`, plus `use_shipped_type_specs()`. Der befürchtete Umfang wurde gemessen statt geschätzt — **kein einziger Test kippte** (863 vorher). Variante 2 wurde verworfen, weil sie die Falle stehen lässt und die Auswahl zwischen zwei fast identischen Fixturen Wissen wäre, das nirgends steht. `rooted_kb` ist ersatzlos entfallen.
2. **`_collection_of()` löst gegen den übergebenen Baum auf**, und `authorised_labels()` bekommt denselben Baum gereicht statt auf `config.KB_DIR` zurückzufallen. Das ist der eigentliche Fix: eine Funktion, die ein Verzeichnis entgegennimmt, löst dagegen auf — keine Fixture kann diese Form von außen reparieren.
3. **Regressionstest:** `test_unauthorised_label_is_judged_in_a_tree_that_is_not_the_configured_kb` lintet einen Baum, von dem `ROOT` bewusst wegzeigt. Gegen den alten Code schlägt er fehl (verifiziert), gegen den neuen nicht. Ein Test unter der reparierten Fixture allein hätte nichts bewiesen — dort ist `kb_dir == config.KB_DIR`, und der Defekt bliebe unsichtbar.
4. **Wächter: ja, zweistufig.**
| Guard | Default | Kosten |
|---|---|---|
| `repository_tree_guard` (session) | an | zwei `git status --porcelain` pro Lauf |
| `per_test_tree_guard` | aus, `CHEMENU_TREE_GUARD=each` | ein `git status` pro Test |
Der Session-Wächter vergleicht vorher gegen nachher statt einen sauberen Baum zu verlangen, sagt also nichts über unveröffentlichte Arbeit des Entwicklers. Er benennt den Verursacher nicht — dafür ist `CHEMENU_TREE_GUARD=each` da. Ohne git oder außerhalb eines Repositorys sind beide still. Beide gegen eine absichtlich schreibende Probe verifiziert.
**Bewusst nicht abgedeckt:** die stillere Hälfte. Eine Prüfung, die unter Test nichts tut, schreibt keine Datei — kein Wächter dieser Bauart sieht das. Dagegen hilft nur ein Test, der das Finding tatsächlich auslöst. Diese Grenze steht in `testing-conventions.md`, damit sie nicht als abgedeckt missverstanden wird.
## Checkliste
- [x] Entscheidung 1 vs. 2 getroffen und umgesetzt; Suite-Lauf auf gekippte Tests geprüft — keine
- [x] `_collection_of()` gegen den übergebenen Baum aufgelöst
- [x] Test geschrieben, der `unauthorised_labels` tatsächlich auslöst
- [x] `instructions/dev/testing-conventions.md` ergänzt (Abschnitt „Which tree a test writes into", ein Checklisten-Schritt, drei Fixture-Tests in `test_hermetic_env.py`)
- [x] Version-Bump `--patch` → 4.1.1
## Verifiziert
867 Tests grün, auch im gehärteten `env -i`-Lauf; `docs verify` und `instructions verify` OK; der Regressionstest gegen den alten Code als fehlschlagend nachgewiesen; beide Wächter gegen eine schreibende Probe getestet.
## Nachtrag: der Prozessfehler beim Schließen (4.1.2)
Dieser Issue wurde zunächst mit einem Abschlusskommentar geschlossen, während der Body weiter als offene Arbeit dastand — „Zu entscheiden" über eine längst getroffene Entscheidung, ungehakte Checkliste. Das ist genau der Fehler, den `instructions/dev/issue-tracking.md` Schritt 2 verhindern soll.
Daraus wurde die Regel geschärft statt nur der Fall repariert (4.1.2, `41f5dfe`):
- **Schritt 2:** Der Body ist das Plan-File dieses Stacks — fortlaufend aktuell, nicht am Ende nachgezogen. Maßstab ist der Abbruch: eine Session kann jederzeit enden, und der Body ist zu jedem Zeitpunkt die vollständige Übergabe an die nächste.
- **Schritt 7:** Abschluss ist die letzte dieser Aktualisierungen, kein Sonderakt. Body auf Endstand, dann schließen, dann Changelog-Kommentar.
- **Schritt 3:** Ein Kommentar pro Session-Umfang, nicht pro Edit — sonst erzeugt die fortlaufende Pflege Lärm.
- **Neuer Abschnitt „What no tool checks":** `wikitool` kennt den Tracker nicht und soll ihn nicht kennenlernen (es wird an Instanzen ausgeliefert, die kein Board haben; `instructions/dev/` wird von `dist export` gepruned). Es gibt also keine mechanische Prüfung für einen Body, der offen klingt, obwohl er es nicht ist — das fängt nur eine Session ab, die dieser Datei folgt.
- `stack-dev/SKILL.md` sagt es beim Aufgreifen mit.
## Vorgeschichte
Gefunden am 2026-09-03 beim Bau der Migrations-Gate-Tests für 4.1.0 (#40). Geschlossen am 2026-09-03 mit 4.1.1; Prozess-Nachtrag als 4.1.2.
Entscheidung 1, nicht 2.kb_dir repointet jetzt selbst — config.ROOT auf das eigene tmp_path, plus use_shipped_type_specs(), also genau das, was raw_dir schon tat. Der befürchtete Umfang war messbar statt geschätzt: kein einziger Test kippte (863 vorher, alle grün nachher). Die lokale rooted_kb-Umgehung aus 4.1.0 ist damit ersatzlos entfallen; die Auswahl zwischen zwei fast identischen Fixturen war Wissen, das nirgends stand.
Punkt 3 ist der eigentliche Fix._collection_of() löst gegen das an run_lint() übergebene Verzeichnis auf, und authorised_labels() bekommt denselben Baum gereicht statt auf config.KB_DIR zurückzufallen. Regressionstest: test_unauthorised_label_is_judged_in_a_tree_that_is_not_the_configured_kb lintet einen Baum, von dem ROOT bewusst wegzeigt — gegen den alten Code schlägt er fehl (verifiziert), gegen den neuen nicht. Das ist der Beleg, den die Checkliste verlangt hat: hätte die Fixture allein gefixt, wäre kb_dir == config.KB_DIR und der Defekt weiterhin unsichtbar.
Punkt 4, Wächter: ja, zweistufig.
Guard
Default
Kosten
repository_tree_guard (session)
an
zwei git status --porcelain pro Lauf
per_test_tree_guard
aus, CHEMENU_TREE_GUARD=each
ein git status pro Test
Der Session-Wächter vergleicht vorher gegen nachher statt einen sauberen Baum zu verlangen, sagt also nichts über unveröffentlichte Arbeit. Er benennt den Verursacher nicht — dafür ist CHEMENU_TREE_GUARD=each da. Ohne git oder außerhalb eines Repositorys sind beide still. Beide gegen eine absichtlich schreibende Probe verifiziert.
Was der Wächter nicht abfängt: die stillere Hälfte. Eine Prüfung, die unter Test nichts tut, schreibt keine Datei. Dagegen hilft nur ein Test, der das Finding tatsächlich auslöst — steht so auch in den Conventions.
Dokumentiert in instructions/dev/testing-conventions.md, neuer Abschnitt „Which tree a test writes into" plus ein Schritt in der Checkliste: eine neue Fixture, die einen Baum baut, repointet ROOT. Dazu drei Tests in test_hermetic_env.py, die für beide Korpus-Fixturen festhalten, dass sie es tun.
Version:--patch (4.1.1) — Punkt 3 ist ein Fix ohne Interface-Änderung, der Rest ist Test und Doku.
Entscheidung 1 vs. 2 getroffen und umgesetzt; Suite-Lauf geprüft, nichts gekippt
_collection_of() gegen den übergebenen Baum
Test, der unauthorised_labels tatsächlich auslöst
testing-conventions.md ergänzt
--patch-Bump
Verifiziert: 867 Tests grün, auch im gehärteten env -i-Lauf; docs verify und instructions verify OK.
## Umgesetzt in 4.1.1 (`23307c3`)
**Entscheidung 1, nicht 2.** `kb_dir` repointet jetzt selbst — `config.ROOT` auf das eigene `tmp_path`, plus `use_shipped_type_specs()`, also genau das, was `raw_dir` schon tat. Der befürchtete Umfang war messbar statt geschätzt: **kein einziger Test kippte** (863 vorher, alle grün nachher). Die lokale `rooted_kb`-Umgehung aus 4.1.0 ist damit ersatzlos entfallen; die Auswahl zwischen zwei fast identischen Fixturen war Wissen, das nirgends stand.
**Punkt 3 ist der eigentliche Fix.** `_collection_of()` löst gegen das an `run_lint()` übergebene Verzeichnis auf, und `authorised_labels()` bekommt denselben Baum gereicht statt auf `config.KB_DIR` zurückzufallen. Regressionstest: `test_unauthorised_label_is_judged_in_a_tree_that_is_not_the_configured_kb` lintet einen Baum, von dem `ROOT` bewusst wegzeigt — gegen den alten Code schlägt er fehl (verifiziert), gegen den neuen nicht. Das ist der Beleg, den die Checkliste verlangt hat: hätte die Fixture allein gefixt, wäre `kb_dir == config.KB_DIR` und der Defekt weiterhin unsichtbar.
**Punkt 4, Wächter: ja, zweistufig.**
| Guard | Default | Kosten |
|---|---|---|
| `repository_tree_guard` (session) | an | zwei `git status --porcelain` pro Lauf |
| `per_test_tree_guard` | aus, `CHEMENU_TREE_GUARD=each` | ein `git status` pro Test |
Der Session-Wächter vergleicht vorher gegen nachher statt einen sauberen Baum zu verlangen, sagt also nichts über unveröffentlichte Arbeit. Er benennt den Verursacher nicht — dafür ist `CHEMENU_TREE_GUARD=each` da. Ohne git oder außerhalb eines Repositorys sind beide still. Beide gegen eine absichtlich schreibende Probe verifiziert.
Was der Wächter **nicht** abfängt: die stillere Hälfte. Eine Prüfung, die unter Test nichts tut, schreibt keine Datei. Dagegen hilft nur ein Test, der das Finding tatsächlich auslöst — steht so auch in den Conventions.
**Dokumentiert** in `instructions/dev/testing-conventions.md`, neuer Abschnitt „Which tree a test writes into" plus ein Schritt in der Checkliste: eine neue Fixture, die einen Baum baut, repointet `ROOT`. Dazu drei Tests in `test_hermetic_env.py`, die für beide Korpus-Fixturen festhalten, dass sie es tun.
**Version:** `--patch` (4.1.1) — Punkt 3 ist ein Fix ohne Interface-Änderung, der Rest ist Test und Doku.
- [x] Entscheidung 1 vs. 2 getroffen und umgesetzt; Suite-Lauf geprüft, nichts gekippt
- [x] `_collection_of()` gegen den übergebenen Baum
- [x] Test, der `unauthorised_labels` tatsächlich auslöst
- [x] `testing-conventions.md` ergänzt
- [x] `--patch`-Bump
Verifiziert: 867 Tests grün, auch im gehärteten `env -i`-Lauf; `docs verify` und `instructions verify` OK.
Changelog: Body auf den Endstand gezogen — er stand nach dem Schließen noch als offene Arbeit da (Abschnitt „Zu entscheiden", ungehakte Checkliste), obwohl entschieden und umgesetzt. „Zu entscheiden" → „Entschieden und umgesetzt" mit der jeweiligen Begründung, Checkliste abgehakt, Abschnitt „Verifiziert" ergänzt, Diagnose ins Präteritum. Der vorige Kommentar bleibt als Umsetzungsbericht stehen; inhaltlich neu ist nichts.
**Changelog:** Body auf den Endstand gezogen — er stand nach dem Schließen noch als offene Arbeit da (Abschnitt „Zu entscheiden", ungehakte Checkliste), obwohl entschieden und umgesetzt. „Zu entscheiden" → „Entschieden und umgesetzt" mit der jeweiligen Begründung, Checkliste abgehakt, Abschnitt „Verifiziert" ergänzt, Diagnose ins Präteritum. Der vorige Kommentar bleibt als Umsetzungsbericht stehen; inhaltlich neu ist nichts.
Changelog: Abschnitt „Nachtrag: der Prozessfehler beim Schließen (4.1.2)" ergänzt — der falsch geschlossene Body dieses Issues war der Anlass, issue-tracking.md Schritt 2/3/7 zu schärfen (Body = Plan-File, fortlaufend aktuell; Abschluss ist die letzte Aktualisierung; ein Kommentar pro Session-Umfang) und den Abschnitt „What no tool checks" aufzunehmen. Kopfzeile verweist darauf. Sonst unverändert.
**Changelog:** Abschnitt „Nachtrag: der Prozessfehler beim Schließen (4.1.2)" ergänzt — der falsch geschlossene Body dieses Issues war der Anlass, `issue-tracking.md` Schritt 2/3/7 zu schärfen (Body = Plan-File, fortlaufend aktuell; Abschluss ist die letzte Aktualisierung; ein Kommentar pro Session-Umfang) und den Abschnitt „What no tool checks" aufzunehmen. Kopfzeile verweist darauf. Sonst unverändert.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Erledigt in 4.1.1 (
23307c3). Der Befund unten ist die Diagnose, die stehen bleibt; die Entscheidungen sind getroffen und umgesetzt. Ein Prozess-Nachtrag ging als 4.1.2 (41f5dfe) hinterher — siehe „Nachtrag" am Ende.Der Befund
tools/chemenu/tests/conftest.pyhatte zwei Fixtures, die eine Baumwurzel aufbauen, und nur eine von beiden richteteconfig.ROOTmit aus:config.ROOT(vor 4.1.1)raw_dirmonkeypatch.setattr(config, "ROOT", tmp_path)kb_dirconfig.ROOTtraf das echte Repokb_dirbaute seinen Baum untertmp_path / "kb"auf und gab ihn zurück. Das reicht für jeden Test, der den Pfad als Argument weiterreicht (run_lint(kb_dir)). Es reicht nicht, sobald der Code unter Test eine Datei überconfig.ROOToderconfig.KB_DIRauflöst statt über das übergebene Verzeichnis — und das tat er an mehreren Stellen.Wie es aufgefallen ist
Beim Bau der Migrations-Gate-Tests für 4.1.0. Ein Test rief
kb_state.write_kb_state(...), das überkb_state_file() -> config.ROOT / ".wikitool-kb.json"auflöst. Ergebnis:Der Applied-Ledger des echten Repos war überschrieben —
applied: []statt der beiden Einträge für3.0.0-authoring-conventionsund4.0.0-link-taxonomy. Zurückgeholt mitgit checkout --, die Tests auf eine lokalerooted_kb-Fixture umgestellt.Das war eine unversionierte, git-verfolgte Datei, also war der Schaden reversibel und sofort sichtbar. Bei einer gitignorierten Datei — oder in einem Baum mit uncommitteten Änderungen — wäre er es nicht gewesen.
Der zweite, stillere Fall
Derselbe Mechanismus machte Prüfungen wirkungslos, statt Dateien zu überschreiben.
lint_core._collection_of()löste eine Seite gegenconfig.KB_DIRauf, nicht gegen das anrun_lint()übergebene Verzeichnis:Für eine Seite unter
tmp_path/kb/wirft dasValueError, die Funktion gabNonezurück, und die Label-Autorisierungsprüfung übersprang die Kante stillschweigend. Das hieß:unauthorised_labelswar in der Suite faktisch nicht getestet — jeder Test, der es hätte auslösen können, bekam eine leere Liste zurück und behauptete nichts.Das war die gefährlichere Hälfte. Ein überschriebenes
.wikitool-kb.jsonsieht man ingit status. Eine Prüfung, die unter Test nie feuert, sieht man gar nicht, und ihr grüner Testlauf liest sich wie eine Zusicherung.Warum das nicht die Autouse-Fixture erledigt
hermetic_environmentneutralisiert die Umgebung —HOME,XDG_CONFIG_HOME, die Git-Konfiguration,CHEMENU_ROOT.config.ROOTfällt bei fehlendemCHEMENU_ROOTauf_PACKAGE_ROOTzurück, also auf den Checkout, aus dem die Tests laufen. Das ist für die meisten Tests richtig — sie brauchen die ausgeliefertentypes/— und genau deshalb greift die Isolation hier nicht von selbst.configs Modul-__getattr__löst die abgeleiteten Pfade bei jedem Zugriff auf,monkeypatch.setattr(config, "ROOT", tmp_path)ziehtKB_DIRalso mit. Der Mechanismus war vorhanden;kb_dirbenutzte ihn nur nicht.Entschieden und umgesetzt
kb_dirrepointet selbst (Variante 1, nicht die getrennterooted_kb-Fixture). Genau das, wasraw_dirschon tat:config.ROOTauf das eigenetmp_path, plususe_shipped_type_specs(). Der befürchtete Umfang wurde gemessen statt geschätzt — kein einziger Test kippte (863 vorher). Variante 2 wurde verworfen, weil sie die Falle stehen lässt und die Auswahl zwischen zwei fast identischen Fixturen Wissen wäre, das nirgends steht.rooted_kbist ersatzlos entfallen._collection_of()löst gegen den übergebenen Baum auf, undauthorised_labels()bekommt denselben Baum gereicht statt aufconfig.KB_DIRzurückzufallen. Das ist der eigentliche Fix: eine Funktion, die ein Verzeichnis entgegennimmt, löst dagegen auf — keine Fixture kann diese Form von außen reparieren.Regressionstest:
test_unauthorised_label_is_judged_in_a_tree_that_is_not_the_configured_kblintet einen Baum, von demROOTbewusst wegzeigt. Gegen den alten Code schlägt er fehl (verifiziert), gegen den neuen nicht. Ein Test unter der reparierten Fixture allein hätte nichts bewiesen — dort istkb_dir == config.KB_DIR, und der Defekt bliebe unsichtbar.Wächter: ja, zweistufig.
repository_tree_guard(session)git status --porcelainpro Laufper_test_tree_guardCHEMENU_TREE_GUARD=eachgit statuspro TestDer Session-Wächter vergleicht vorher gegen nachher statt einen sauberen Baum zu verlangen, sagt also nichts über unveröffentlichte Arbeit des Entwicklers. Er benennt den Verursacher nicht — dafür ist
CHEMENU_TREE_GUARD=eachda. Ohne git oder außerhalb eines Repositorys sind beide still. Beide gegen eine absichtlich schreibende Probe verifiziert.Bewusst nicht abgedeckt: die stillere Hälfte. Eine Prüfung, die unter Test nichts tut, schreibt keine Datei — kein Wächter dieser Bauart sieht das. Dagegen hilft nur ein Test, der das Finding tatsächlich auslöst. Diese Grenze steht in
testing-conventions.md, damit sie nicht als abgedeckt missverstanden wird.Checkliste
_collection_of()gegen den übergebenen Baum aufgelöstunauthorised_labelstatsächlich auslöstinstructions/dev/testing-conventions.mdergänzt (Abschnitt „Which tree a test writes into", ein Checklisten-Schritt, drei Fixture-Tests intest_hermetic_env.py)--patch→ 4.1.1Verifiziert
867 Tests grün, auch im gehärteten
env -i-Lauf;docs verifyundinstructions verifyOK; der Regressionstest gegen den alten Code als fehlschlagend nachgewiesen; beide Wächter gegen eine schreibende Probe getestet.Nachtrag: der Prozessfehler beim Schließen (4.1.2)
Dieser Issue wurde zunächst mit einem Abschlusskommentar geschlossen, während der Body weiter als offene Arbeit dastand — „Zu entscheiden" über eine längst getroffene Entscheidung, ungehakte Checkliste. Das ist genau der Fehler, den
instructions/dev/issue-tracking.mdSchritt 2 verhindern soll.Daraus wurde die Regel geschärft statt nur der Fall repariert (4.1.2,
41f5dfe):wikitoolkennt den Tracker nicht und soll ihn nicht kennenlernen (es wird an Instanzen ausgeliefert, die kein Board haben;instructions/dev/wird vondist exportgepruned). Es gibt also keine mechanische Prüfung für einen Body, der offen klingt, obwohl er es nicht ist — das fängt nur eine Session ab, die dieser Datei folgt.stack-dev/SKILL.mdsagt es beim Aufgreifen mit.Vorgeschichte
Gefunden am 2026-09-03 beim Bau der Migrations-Gate-Tests für 4.1.0 (#40). Geschlossen am 2026-09-03 mit 4.1.1; Prozess-Nachtrag als 4.1.2.
Umgesetzt in 4.1.1 (
23307c3)Entscheidung 1, nicht 2.
kb_dirrepointet jetzt selbst —config.ROOTauf das eigenetmp_path, plususe_shipped_type_specs(), also genau das, wasraw_dirschon tat. Der befürchtete Umfang war messbar statt geschätzt: kein einziger Test kippte (863 vorher, alle grün nachher). Die lokalerooted_kb-Umgehung aus 4.1.0 ist damit ersatzlos entfallen; die Auswahl zwischen zwei fast identischen Fixturen war Wissen, das nirgends stand.Punkt 3 ist der eigentliche Fix.
_collection_of()löst gegen das anrun_lint()übergebene Verzeichnis auf, undauthorised_labels()bekommt denselben Baum gereicht statt aufconfig.KB_DIRzurückzufallen. Regressionstest:test_unauthorised_label_is_judged_in_a_tree_that_is_not_the_configured_kblintet einen Baum, von demROOTbewusst wegzeigt — gegen den alten Code schlägt er fehl (verifiziert), gegen den neuen nicht. Das ist der Beleg, den die Checkliste verlangt hat: hätte die Fixture allein gefixt, wärekb_dir == config.KB_DIRund der Defekt weiterhin unsichtbar.Punkt 4, Wächter: ja, zweistufig.
repository_tree_guard(session)git status --porcelainpro Laufper_test_tree_guardCHEMENU_TREE_GUARD=eachgit statuspro TestDer Session-Wächter vergleicht vorher gegen nachher statt einen sauberen Baum zu verlangen, sagt also nichts über unveröffentlichte Arbeit. Er benennt den Verursacher nicht — dafür ist
CHEMENU_TREE_GUARD=eachda. Ohne git oder außerhalb eines Repositorys sind beide still. Beide gegen eine absichtlich schreibende Probe verifiziert.Was der Wächter nicht abfängt: die stillere Hälfte. Eine Prüfung, die unter Test nichts tut, schreibt keine Datei. Dagegen hilft nur ein Test, der das Finding tatsächlich auslöst — steht so auch in den Conventions.
Dokumentiert in
instructions/dev/testing-conventions.md, neuer Abschnitt „Which tree a test writes into" plus ein Schritt in der Checkliste: eine neue Fixture, die einen Baum baut, repointetROOT. Dazu drei Tests intest_hermetic_env.py, die für beide Korpus-Fixturen festhalten, dass sie es tun.Version:
--patch(4.1.1) — Punkt 3 ist ein Fix ohne Interface-Änderung, der Rest ist Test und Doku._collection_of()gegen den übergebenen Baumunauthorised_labelstatsächlich auslösttesting-conventions.mdergänzt--patch-BumpVerifiziert: 867 Tests grün, auch im gehärteten
env -i-Lauf;docs verifyundinstructions verifyOK.Changelog: Body auf den Endstand gezogen — er stand nach dem Schließen noch als offene Arbeit da (Abschnitt „Zu entscheiden", ungehakte Checkliste), obwohl entschieden und umgesetzt. „Zu entscheiden" → „Entschieden und umgesetzt" mit der jeweiligen Begründung, Checkliste abgehakt, Abschnitt „Verifiziert" ergänzt, Diagnose ins Präteritum. Der vorige Kommentar bleibt als Umsetzungsbericht stehen; inhaltlich neu ist nichts.
Changelog: Abschnitt „Nachtrag: der Prozessfehler beim Schließen (4.1.2)" ergänzt — der falsch geschlossene Body dieses Issues war der Anlass,
issue-tracking.mdSchritt 2/3/7 zu schärfen (Body = Plan-File, fortlaufend aktuell; Abschluss ist die letzte Aktualisierung; ein Kommentar pro Session-Umfang) und den Abschnitt „What no tool checks" aufzunehmen. Kopfzeile verweist darauf. Sonst unverändert.