Root-Auflösung ist importzeit-gebunden: chemenu kann nur den Checkout bedienen, in dem es selbst liegt #31
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
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.