60 Verwendungen von config.ROOT über 19 Module (Tests ausgenommen). Damit kann jeder
In-Process-Konsument nur genau den Checkout bedienen, in dem das Paket selbst liegt.
Die Umschaltung zur Laufzeit ist heute nur halb möglich, und das ist der
gefährlichere Teil: monkeypatch.setattr(config, "ROOT", tmp_path)
(tools/chemenu/tests/conftest.py:115) repointet ROOT, aber nicht KB_DIR/RAW_DIR
— die sind längst gebunden. Die Testsuite kommt damit durch, weil das kb_dir-Fixture
den Pfad zusätzlich explizit durchreicht. Wer sich auf ROOT allein verlässt, baut
etwas, das scheinbar auf einem Zielbaum arbeitet und in Wahrheit aus dem
Entwickler-Checkout antwortet.
Diese Fehlerklasse ist im Repo schon einmal aufgetreten und dokumentiert: das raw_dir-Fixture (conftest.py:96-120) existiert genau deshalb, nachdem ein Test
monatelang nur grün war, weil dieser Checkout zufällig ein raw/documents/ hatte.
Zweiter Teil desselben Problems: die Bibliotheksgrenze kennt keine Exceptions
_util.fail() (tools/chemenu/commands/_util.py:41-47) wirft typer.Exit und setzt
ein modulglobales _declined. Für die CLI ist das richtig. Für einen
In-Process-Aufrufer heißt es: ein Validierungsfehler kommt als Exit-Code statt als
Exception an, und globaler Zustand überlebt zwischen Aufrufen.
Warum das eigenständig zählt
Nicht bloß Vorarbeit für den MCP-Server (#19). Es ist der Schritt, der chemenu von
einem Skript, das in seinem eigenen Verzeichnis lebt, zu einer Bibliothek macht, die man
auf einen Korpus zeigen kann. Nutzen unabhängig von MCP:
mehrere Korpora aus einem Checkout bedienen
Tests, die den Zielbaum nicht mehr auf zwei Wegen gleichzeitig einstellen müssen
dist/migrate-Werkzeuge, die auf eine Zielinstanz zeigen können statt auf sich selbst
Skizze
Root-Auflösung als Funktion statt als Konstante. Reihenfolge: expliziter Parameter →
Env-Var (CHEMENU_ROOT) → Walk-up wie bisher. Der Walk-up bleibt Default, damit tools/wikitool sich unverändert verhält.
Abgeleitete Pfade nicht mehr zur Importzeit binden.
An der Bibliotheksgrenze Exceptions statt typer.Exit; der CLI-Adapter übersetzt sie
weiterhin in fail() → ERROR-Zeile, Exit 1, Budget-Refund. _declined ist danach nur
noch für den CLI-Pfad zuständig.
Die neue Env-Var muss nach _WIKITOOL_ENV — siehe #23 und instructions/dev/testing-conventions.md Schritt 4.
Akzeptanzkriterien
Ein In-Process-Aufruf von run_search/run_lint lässt sich auf einen beliebigen
Korpusbaum richten, ohne dass irgendein Pfad des Entwickler-Checkouts gelesen wird
Ein Test beweist genau das (heute liefe er über KB_DIR auf den echten Baum)
CHEMENU_ROOT in _WIKITOOL_ENV registriert, #23-konform
## Befund
`config.ROOT` wird aus dem Ort der Datei abgeleitet, und alle Pfade darunter werden
**zur Importzeit** gebunden:
```python
# tools/chemenu/config.py:15-22
ROOT = Path(__file__).resolve().parents[2]
RAW_DIR = ROOT / "raw"
KB_DIR = ROOT / "kb"
```
60 Verwendungen von `config.ROOT` über 19 Module (Tests ausgenommen). Damit kann jeder
In-Process-Konsument nur genau den Checkout bedienen, in dem das Paket selbst liegt.
Die Umschaltung zur Laufzeit ist heute nur **halb** möglich, und das ist der
gefährlichere Teil: `monkeypatch.setattr(config, "ROOT", tmp_path)`
(`tools/chemenu/tests/conftest.py:115`) repointet `ROOT`, aber nicht `KB_DIR`/`RAW_DIR`
— die sind längst gebunden. Die Testsuite kommt damit durch, weil das `kb_dir`-Fixture
den Pfad zusätzlich explizit durchreicht. Wer sich auf `ROOT` allein verlässt, baut
etwas, das scheinbar auf einem Zielbaum arbeitet und in Wahrheit aus dem
Entwickler-Checkout antwortet.
Diese Fehlerklasse ist im Repo schon einmal aufgetreten und dokumentiert: das
`raw_dir`-Fixture (`conftest.py:96-120`) existiert genau deshalb, nachdem ein Test
monatelang nur grün war, weil dieser Checkout zufällig ein `raw/documents/` hatte.
## Zweiter Teil desselben Problems: die Bibliotheksgrenze kennt keine Exceptions
`_util.fail()` (`tools/chemenu/commands/_util.py:41-47`) wirft `typer.Exit` und setzt
ein modulglobales `_declined`. Für die CLI ist das richtig. Für einen
In-Process-Aufrufer heißt es: ein Validierungsfehler kommt als Exit-Code statt als
Exception an, und globaler Zustand überlebt zwischen Aufrufen.
## Warum das eigenständig zählt
Nicht bloß Vorarbeit für den MCP-Server (#19). Es ist der Schritt, der `chemenu` von
einem Skript, das in seinem eigenen Verzeichnis lebt, zu einer Bibliothek macht, die man
auf einen Korpus **zeigen** kann. Nutzen unabhängig von MCP:
- mehrere Korpora aus einem Checkout bedienen
- Tests, die den Zielbaum nicht mehr auf zwei Wegen gleichzeitig einstellen müssen
- `dist`/`migrate`-Werkzeuge, die auf eine Zielinstanz zeigen können statt auf sich selbst
## Skizze
- Root-Auflösung als Funktion statt als Konstante. Reihenfolge: expliziter Parameter →
Env-Var (`CHEMENU_ROOT`) → Walk-up wie bisher. Der Walk-up bleibt Default, damit
`tools/wikitool` sich unverändert verhält.
- Abgeleitete Pfade nicht mehr zur Importzeit binden.
- An der Bibliotheksgrenze Exceptions statt `typer.Exit`; der CLI-Adapter übersetzt sie
weiterhin in `fail()` → ERROR-Zeile, Exit 1, Budget-Refund. `_declined` ist danach nur
noch für den CLI-Pfad zuständig.
- Die neue Env-Var muss nach `_WIKITOOL_ENV` — siehe #23 und
`instructions/dev/testing-conventions.md` Schritt 4.
## Akzeptanzkriterien
- [ ] Ein In-Process-Aufruf von `run_search`/`run_lint` lässt sich auf einen beliebigen
Korpusbaum richten, ohne dass irgendein Pfad des Entwickler-Checkouts gelesen wird
- [ ] Ein Test beweist genau das (heute liefe er über `KB_DIR` auf den echten Baum)
- [ ] `CHEMENU_ROOT` in `_WIKITOOL_ENV` registriert, #23-konform
- [ ] Kernfunktionen melden Validierungsfehler als Exception; CLI-Verhalten (ERROR-Zeile,
Exit 1, Budget-Refund) unverändert
- [ ] Ohne gesetzte Env-Var verhält sich `tools/wikitool` unverändert
- [ ] Version-Bump `--minor` (neue Fähigkeit, rückwärtskompatibel), Changelog-Eintrag
Bestätigt und auf dem kritischen Pfad, Stand 2026-09-01.
Als ich dieses Issue anlegte, hatte ich angemerkt, dass es an Wert verlöre, falls wikitool nach
Go migriert wird — die Bibliotheksgrenze in Python zu ziehen wäre dann weggeworfene Arbeit. Diese
Frage ist entschieden: Python bleibt, Go ist vom Tisch (Begründung im Kommentar zu #19). Der
Vorbehalt entfällt damit ersatzlos.
Zwei Präzisierungen aus der Zwischenzeit:
Der Umfang ist kleiner, als der Issue-Text vermuten lässt. Der reine Lese-Kern
(search/, page.py, kb_scan.py, frontmatter_io.py, type_resolver.py, config.py, kb_collections.py) importiert an Fremdcode genau zwei Pakete — yaml und jsonschema. Alles
andere ist Standardbibliothek. Die „~14 transitiven Pakete" sind ein pip list-Artefakt des
CLI-Kopfs, nicht der Import-Graph.
Aber die Naht ist noch nicht gezogen.run_search() und run_lint() liegen in Modulen, die typer auf Modulebene importieren (commands/search.py:26, commands/lint.py:17, commands/types_cmd.py:16); über _util kommt rich dazu. Wer die Funktionen heute in-process
aufruft, zieht den CLI-Kopf mit. Genau das ist der zweite Teil dieses Issues, und er ist die
Voraussetzung dafür, dass die Zweier-Bilanz oben für den MCP-Server aus #19 tatsächlich gilt.
Reihenfolge: dieses Issue kommt nach#33 (die Härtung fasst dieselben Dateien an, aber
oberflächlich — sie zuerst zu machen spart einen Konflikt) und vor#19.
Bestätigt und auf dem kritischen Pfad, Stand 2026-09-01.
Als ich dieses Issue anlegte, hatte ich angemerkt, dass es an Wert verlöre, falls `wikitool` nach
Go migriert wird — die Bibliotheksgrenze in Python zu ziehen wäre dann weggeworfene Arbeit. Diese
Frage ist entschieden: **Python bleibt, Go ist vom Tisch** (Begründung im Kommentar zu #19). Der
Vorbehalt entfällt damit ersatzlos.
Zwei Präzisierungen aus der Zwischenzeit:
**Der Umfang ist kleiner, als der Issue-Text vermuten lässt.** Der reine Lese-Kern
(`search/`, `page.py`, `kb_scan.py`, `frontmatter_io.py`, `type_resolver.py`, `config.py`,
`kb_collections.py`) importiert an Fremdcode genau zwei Pakete — `yaml` und `jsonschema`. Alles
andere ist Standardbibliothek. Die „~14 transitiven Pakete" sind ein `pip list`-Artefakt des
CLI-Kopfs, nicht der Import-Graph.
**Aber die Naht ist noch nicht gezogen.** `run_search()` und `run_lint()` liegen in Modulen, die
`typer` auf Modulebene importieren (`commands/search.py:26`, `commands/lint.py:17`,
`commands/types_cmd.py:16`); über `_util` kommt `rich` dazu. Wer die Funktionen heute in-process
aufruft, zieht den CLI-Kopf mit. Genau das ist der zweite Teil dieses Issues, und er ist die
Voraussetzung dafür, dass die Zweier-Bilanz oben für den MCP-Server aus #19 tatsächlich gilt.
Reihenfolge: dieses Issue kommt **nach** #33 (die Härtung fasst dieselben Dateien an, aber
oberflächlich — sie zuerst zu machen spart einen Konflikt) und **vor** #19.
Umgesetzt in 2.3.0. Alle Akzeptanzkriterien erfüllt.
Root-Auflösung nach Präzedenz.config.resolve_root(): expliziter Parameter → $CHEMENU_ROOT → Walk-up. Der Walk-up bleibt Default, tools/wikitool verhält sich ohne die
Variable unverändert (gegengeprüft).
Nichts ist mehr zur Importzeit gebunden.ROOT und alle abgeleiteten Pfade werden bei jedem
Zugriff aufgelöst (PEP 562 __getattr__ auf dem Modul). Damit folgen KB_DIR/RAW_DIR dem
aktuellen ROOT — auch einem zugewiesenen — und der halb-umgezeigte Zustand ist nicht mehr
konstruierbar. Genau die Fehlerklasse, die schlimmer war als die Einschränkung selbst.
Eine Stolperfalle, die dabei sichtbar wurde und dokumentiert gehört:monkeypatch merkt sich
den alten Wert, indem es ihn liest — also auflöst — und schreibt ihn beim Aufräumen als echtes
Attribut zurück. Der Cleanup baute die stale Bindung damit wieder auf, und der nächste Test erbte
ein KB_DIR, das auf den vorigen Baum zeigte. Deshalb config.reset(), von der autouse-Fixture
auf beiden Seiten gerufen.
Bibliotheksgrenze gezogen. Der reine Kern liegt jetzt in search/service.py, lint_core.py
und types_core.py — kein typer, kein rich. Die Module unter commands/ sind die
Terminal-Adapter darüber und re-exportieren die Namen, damit kein bestehender Import bricht. Ein
Test importiert chemenu.api in einem frischen Interpreter und prüft sys.modules: weder chemenu.commands.* noch typer/rich/click.
chemenu.api.Corpus als In-Process-Einstieg: nimmt einen Root, liefert exakt die --json-Formen der CLI, stempelt jede Antwort mit Commit-SHA und Zeitstempel.
Der Beweis-Test patcht Path.read_text und Path.rglob und lässt jeden Zugriff unterhalb
des Entwickler-Checkouts durchfallen, während ein fremder Root gesetzt ist. Vor der lazy
Auflösung hätte er nicht grün werden können.
Exceptions statt typer.Exit.chemenu/errors.py mit ChemenuError → ValidationError / BackendError; PredicateError, FrontmatterError, UnknownBackend und
die beiden Ripgrep* hängen darunter. ValidationError erbt zusätzlich ValueError, weil PredicateError vorher eines war und Aufrufer es so fangen. CLI-Verhalten unverändert: ERROR-Zeile, Exit 1, Budget-Refund.
CHEMENU_ROOT ist in _WIKITOOL_ENV registriert, #23-konform — aber von Hand. Die fehlende Erzwingung bleibt genau das, was #23 beschreibt; dieses Issue hat sie nicht mit gelöst.
Was der Umbau nebenbei aufgedeckt hat — zwei Abhängigkeiten, die nur durch Zufall hielten:
TypeResolver.repo_root war beim Import gebunden. Jetzt folgt er ROOT — womit Fixtures, die ROOT auf einen tmp-Baum zeigen, die mitgelieferten Type-Specs ausdrücklich benennen
müssen (use_shipped_type_specs). Vorher zeigte TYPES_DIR einfach weiter auf diesen
Checkout, und die Tests liefen auf einer Abhängigkeit, die niemand aufgeschrieben hatte —
dieselbe Form wie das Loch, für das raw_dir geschrieben wurde, eine Ebene tiefer.
resolve() reichte den Root nicht ans Backend durch: ein Aufrufer konnte run_search einen
Korpus übergeben, während RipgrepBackend weiter config.KB_DIR durchlief. Die Anfrage aus
dem einen Baum beantwortet, die Seiten aus dem anderen gelesen, ohne dass irgendetwas das
gesagt hätte. Behoben.
Eine Ehrlichkeit zur Thread-Sicherheit:config.rooted() ist prozessweit, solange der Block
offen ist, und damit nicht thread-sicher — ein Aufrufer, der mehrere Korpora gleichzeitig bedient,
hält das Lock. Nötig, weil nicht alles einen Root als Argument nimmt; der modulglobale TypeResolver muss types/ finden. Steht so im Docstring, statt still zu bleiben.
Umgesetzt in **2.3.0**. Alle Akzeptanzkriterien erfüllt.
**Root-Auflösung nach Präzedenz.** `config.resolve_root()`: expliziter Parameter →
`$CHEMENU_ROOT` → Walk-up. Der Walk-up bleibt Default, `tools/wikitool` verhält sich ohne die
Variable unverändert (gegengeprüft).
**Nichts ist mehr zur Importzeit gebunden.** `ROOT` und alle abgeleiteten Pfade werden bei jedem
Zugriff aufgelöst (PEP 562 `__getattr__` auf dem Modul). Damit folgen `KB_DIR`/`RAW_DIR` dem
aktuellen `ROOT` — auch einem zugewiesenen — und der halb-umgezeigte Zustand ist nicht mehr
konstruierbar. Genau die Fehlerklasse, die schlimmer war als die Einschränkung selbst.
**Eine Stolperfalle, die dabei sichtbar wurde und dokumentiert gehört:** `monkeypatch` merkt sich
den alten Wert, indem es ihn *liest* — also auflöst — und schreibt ihn beim Aufräumen als echtes
Attribut zurück. Der Cleanup baute die stale Bindung damit wieder auf, und der nächste Test erbte
ein `KB_DIR`, das auf den vorigen Baum zeigte. Deshalb `config.reset()`, von der autouse-Fixture
auf beiden Seiten gerufen.
**Bibliotheksgrenze gezogen.** Der reine Kern liegt jetzt in `search/service.py`, `lint_core.py`
und `types_core.py` — kein `typer`, kein `rich`. Die Module unter `commands/` sind die
Terminal-Adapter darüber und re-exportieren die Namen, damit kein bestehender Import bricht. Ein
Test importiert `chemenu.api` in einem frischen Interpreter und prüft `sys.modules`: weder
`chemenu.commands.*` noch `typer`/`rich`/`click`.
**`chemenu.api.Corpus`** als In-Process-Einstieg: nimmt einen Root, liefert exakt die
`--json`-Formen der CLI, stempelt jede Antwort mit Commit-SHA und Zeitstempel.
**Der Beweis-Test** patcht `Path.read_text` und `Path.rglob` und lässt jeden Zugriff unterhalb
des Entwickler-Checkouts durchfallen, während ein fremder Root gesetzt ist. Vor der lazy
Auflösung hätte er nicht grün werden können.
**Exceptions statt `typer.Exit`.** `chemenu/errors.py` mit `ChemenuError` →
`ValidationError` / `BackendError`; `PredicateError`, `FrontmatterError`, `UnknownBackend` und
die beiden `Ripgrep*` hängen darunter. `ValidationError` erbt zusätzlich `ValueError`, weil
`PredicateError` vorher eines war und Aufrufer es so fangen. CLI-Verhalten unverändert:
`ERROR`-Zeile, Exit 1, Budget-Refund.
**`CHEMENU_ROOT` ist in `_WIKITOOL_ENV` registriert**, #23-konform — aber von Hand. Die fehlende
*Erzwingung* bleibt genau das, was #23 beschreibt; dieses Issue hat sie nicht mit gelöst.
---
**Was der Umbau nebenbei aufgedeckt hat** — zwei Abhängigkeiten, die nur durch Zufall hielten:
1. `TypeResolver.repo_root` war beim Import gebunden. Jetzt folgt er `ROOT` — womit Fixtures, die
`ROOT` auf einen tmp-Baum zeigen, die mitgelieferten Type-Specs **ausdrücklich** benennen
müssen (`use_shipped_type_specs`). Vorher zeigte `TYPES_DIR` einfach weiter auf diesen
Checkout, und die Tests liefen auf einer Abhängigkeit, die niemand aufgeschrieben hatte —
dieselbe Form wie das Loch, für das `raw_dir` geschrieben wurde, eine Ebene tiefer.
2. `resolve()` reichte den Root nicht ans Backend durch: ein Aufrufer konnte `run_search` einen
Korpus übergeben, während `RipgrepBackend` weiter `config.KB_DIR` durchlief. Die Anfrage aus
dem einen Baum beantwortet, die Seiten aus dem anderen gelesen, ohne dass irgendetwas das
gesagt hätte. Behoben.
**Eine Ehrlichkeit zur Thread-Sicherheit:** `config.rooted()` ist prozessweit, solange der Block
offen ist, und damit nicht thread-sicher — ein Aufrufer, der mehrere Korpora gleichzeitig bedient,
hält das Lock. Nötig, weil nicht alles einen Root als Argument nimmt; der modulglobale
`TypeResolver` muss `types/` finden. Steht so im Docstring, statt still zu bleiben.
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.
Befund
config.ROOTwird aus dem Ort der Datei abgeleitet, und alle Pfade darunter werdenzur Importzeit gebunden:
60 Verwendungen von
config.ROOTüber 19 Module (Tests ausgenommen). Damit kann jederIn-Process-Konsument nur genau den Checkout bedienen, in dem das Paket selbst liegt.
Die Umschaltung zur Laufzeit ist heute nur halb möglich, und das ist der
gefährlichere Teil:
monkeypatch.setattr(config, "ROOT", tmp_path)(
tools/chemenu/tests/conftest.py:115) repointetROOT, aber nichtKB_DIR/RAW_DIR— die sind längst gebunden. Die Testsuite kommt damit durch, weil das
kb_dir-Fixtureden Pfad zusätzlich explizit durchreicht. Wer sich auf
ROOTallein verlässt, bautetwas, das scheinbar auf einem Zielbaum arbeitet und in Wahrheit aus dem
Entwickler-Checkout antwortet.
Diese Fehlerklasse ist im Repo schon einmal aufgetreten und dokumentiert: das
raw_dir-Fixture (conftest.py:96-120) existiert genau deshalb, nachdem ein Testmonatelang nur grün war, weil dieser Checkout zufällig ein
raw/documents/hatte.Zweiter Teil desselben Problems: die Bibliotheksgrenze kennt keine Exceptions
_util.fail()(tools/chemenu/commands/_util.py:41-47) wirfttyper.Exitund setztein modulglobales
_declined. Für die CLI ist das richtig. Für einenIn-Process-Aufrufer heißt es: ein Validierungsfehler kommt als Exit-Code statt als
Exception an, und globaler Zustand überlebt zwischen Aufrufen.
Warum das eigenständig zählt
Nicht bloß Vorarbeit für den MCP-Server (#19). Es ist der Schritt, der
chemenuvoneinem Skript, das in seinem eigenen Verzeichnis lebt, zu einer Bibliothek macht, die man
auf einen Korpus zeigen kann. Nutzen unabhängig von MCP:
dist/migrate-Werkzeuge, die auf eine Zielinstanz zeigen können statt auf sich selbstSkizze
Env-Var (
CHEMENU_ROOT) → Walk-up wie bisher. Der Walk-up bleibt Default, damittools/wikitoolsich unverändert verhält.typer.Exit; der CLI-Adapter übersetzt sieweiterhin in
fail()→ ERROR-Zeile, Exit 1, Budget-Refund._declinedist danach nurnoch für den CLI-Pfad zuständig.
_WIKITOOL_ENV— siehe #23 undinstructions/dev/testing-conventions.mdSchritt 4.Akzeptanzkriterien
run_search/run_lintlässt sich auf einen beliebigenKorpusbaum richten, ohne dass irgendein Pfad des Entwickler-Checkouts gelesen wird
KB_DIRauf den echten Baum)CHEMENU_ROOTin_WIKITOOL_ENVregistriert, #23-konformExit 1, Budget-Refund) unverändert
tools/wikitoolunverändert--minor(neue Fähigkeit, rückwärtskompatibel), Changelog-EintragBestätigt und auf dem kritischen Pfad, Stand 2026-09-01.
Als ich dieses Issue anlegte, hatte ich angemerkt, dass es an Wert verlöre, falls
wikitoolnachGo migriert wird — die Bibliotheksgrenze in Python zu ziehen wäre dann weggeworfene Arbeit. Diese
Frage ist entschieden: Python bleibt, Go ist vom Tisch (Begründung im Kommentar zu #19). Der
Vorbehalt entfällt damit ersatzlos.
Zwei Präzisierungen aus der Zwischenzeit:
Der Umfang ist kleiner, als der Issue-Text vermuten lässt. Der reine Lese-Kern
(
search/,page.py,kb_scan.py,frontmatter_io.py,type_resolver.py,config.py,kb_collections.py) importiert an Fremdcode genau zwei Pakete —yamlundjsonschema. Allesandere ist Standardbibliothek. Die „~14 transitiven Pakete" sind ein
pip list-Artefakt desCLI-Kopfs, nicht der Import-Graph.
Aber die Naht ist noch nicht gezogen.
run_search()undrun_lint()liegen in Modulen, dietyperauf Modulebene importieren (commands/search.py:26,commands/lint.py:17,commands/types_cmd.py:16); über_utilkommtrichdazu. Wer die Funktionen heute in-processaufruft, zieht den CLI-Kopf mit. Genau das ist der zweite Teil dieses Issues, und er ist die
Voraussetzung dafür, dass die Zweier-Bilanz oben für den MCP-Server aus #19 tatsächlich gilt.
Reihenfolge: dieses Issue kommt nach #33 (die Härtung fasst dieselben Dateien an, aber
oberflächlich — sie zuerst zu machen spart einen Konflikt) und vor #19.
Teil der Sequenz in #36 (Master: Weg zum MCP-Leseserver) — dort Schritt 2 von 3, zwischen
#33 und #19.
Umgesetzt in 2.3.0. Alle Akzeptanzkriterien erfüllt.
Root-Auflösung nach Präzedenz.
config.resolve_root(): expliziter Parameter →$CHEMENU_ROOT→ Walk-up. Der Walk-up bleibt Default,tools/wikitoolverhält sich ohne dieVariable unverändert (gegengeprüft).
Nichts ist mehr zur Importzeit gebunden.
ROOTund alle abgeleiteten Pfade werden bei jedemZugriff aufgelöst (PEP 562
__getattr__auf dem Modul). Damit folgenKB_DIR/RAW_DIRdemaktuellen
ROOT— auch einem zugewiesenen — und der halb-umgezeigte Zustand ist nicht mehrkonstruierbar. Genau die Fehlerklasse, die schlimmer war als die Einschränkung selbst.
Eine Stolperfalle, die dabei sichtbar wurde und dokumentiert gehört:
monkeypatchmerkt sichden alten Wert, indem es ihn liest — also auflöst — und schreibt ihn beim Aufräumen als echtes
Attribut zurück. Der Cleanup baute die stale Bindung damit wieder auf, und der nächste Test erbte
ein
KB_DIR, das auf den vorigen Baum zeigte. Deshalbconfig.reset(), von der autouse-Fixtureauf beiden Seiten gerufen.
Bibliotheksgrenze gezogen. Der reine Kern liegt jetzt in
search/service.py,lint_core.pyund
types_core.py— keintyper, keinrich. Die Module untercommands/sind dieTerminal-Adapter darüber und re-exportieren die Namen, damit kein bestehender Import bricht. Ein
Test importiert
chemenu.apiin einem frischen Interpreter und prüftsys.modules: wederchemenu.commands.*nochtyper/rich/click.chemenu.api.Corpusals In-Process-Einstieg: nimmt einen Root, liefert exakt die--json-Formen der CLI, stempelt jede Antwort mit Commit-SHA und Zeitstempel.Der Beweis-Test patcht
Path.read_textundPath.rglobund lässt jeden Zugriff unterhalbdes Entwickler-Checkouts durchfallen, während ein fremder Root gesetzt ist. Vor der lazy
Auflösung hätte er nicht grün werden können.
Exceptions statt
typer.Exit.chemenu/errors.pymitChemenuError→ValidationError/BackendError;PredicateError,FrontmatterError,UnknownBackendunddie beiden
Ripgrep*hängen darunter.ValidationErrorerbt zusätzlichValueError, weilPredicateErrorvorher eines war und Aufrufer es so fangen. CLI-Verhalten unverändert:ERROR-Zeile, Exit 1, Budget-Refund.CHEMENU_ROOTist in_WIKITOOL_ENVregistriert, #23-konform — aber von Hand. Die fehlendeErzwingung bleibt genau das, was #23 beschreibt; dieses Issue hat sie nicht mit gelöst.
Was der Umbau nebenbei aufgedeckt hat — zwei Abhängigkeiten, die nur durch Zufall hielten:
TypeResolver.repo_rootwar beim Import gebunden. Jetzt folgt erROOT— womit Fixtures, dieROOTauf einen tmp-Baum zeigen, die mitgelieferten Type-Specs ausdrücklich benennenmüssen (
use_shipped_type_specs). Vorher zeigteTYPES_DIReinfach weiter auf diesenCheckout, und die Tests liefen auf einer Abhängigkeit, die niemand aufgeschrieben hatte —
dieselbe Form wie das Loch, für das
raw_dirgeschrieben wurde, eine Ebene tiefer.resolve()reichte den Root nicht ans Backend durch: ein Aufrufer konnterun_searcheinenKorpus übergeben, während
RipgrepBackendweiterconfig.KB_DIRdurchlief. Die Anfrage ausdem einen Baum beantwortet, die Seiten aus dem anderen gelesen, ohne dass irgendetwas das
gesagt hätte. Behoben.
Eine Ehrlichkeit zur Thread-Sicherheit:
config.rooted()ist prozessweit, solange der Blockoffen ist, und damit nicht thread-sicher — ein Aufrufer, der mehrere Korpora gleichzeitig bedient,
hält das Lock. Nötig, weil nicht alles einen Root als Argument nimmt; der modulglobale
TypeResolvermusstypes/finden. Steht so im Docstring, statt still zu bleiben.