Aus der Plattform-Debatte vom 2026-09-01. Der Bedarf: hybrides Retrieval (BM25 + Vektor,
semantische Suche) für einen Korpus in der Größenordnung wenige Tausend Seiten, weil ripgrep allein dort nicht mehr trägt.
Die Naht existiert bereits
tools/chemenu/search/registry.py sagt es im eigenen Docstring:
One registry entry today. It exists so that adding a semantic/vector backend is a new module
plus one line here — not a change to the command, the filters, or the output shape.
Selecting several at once fuses them through RRF.
Dazu search/base.py (Backend-Protokoll, „a backend answers the text half of a query and
nothing else"), search/fuse.py (Reciprocal Rank Fusion — ausdrücklich gebaut, um lexikalische
und semantische Rankings ohne gemeinsame Score-Skala zu kombinieren) und search/ripgrep.py
(betreibt seit Tag eins einen fremdsprachigen Prozess hinter dieser Grenze). Das Suchmodul war
bei Anlage dieses Issues 613 Zeilen groß und ist am 2026-09-15 bei 825 — die Grenze selbst
(registry.py, base.py, fuse.py) ist in der Zeit unverändert geblieben, der Zuwachs liegt
im Backend und im Ausgabepfad.
Frontmatter-Prädikate bleiben in filters.py und laufen nachgelagert — ein Vektor-Backend muss confidence>=0.8 nie kennen.
Was ein neues Backend an Ausgabevertrag erbt
Seit 6.0.0-beta.8 (#100) liegen Trefferdarstellung und Trunkierungsauskunft backend-unabhängig
in run_search/render_table, nicht im rg-Adapter. Ein semantisches Backend erbt das, statt es
zu wiederholen — und darf es nicht unterlaufen:
Ein Backend liefert SearchHit-Werte; die Trefferzeile
(score | kind/subtype | titel | pfad | summary) rendert render_table daraus. Titel und
Pfad müssen echt und vollständig sein — der Titel ist das Argument, das touch/xref/cite
nehmen, der Pfad das, womit die Seite geöffnet wird. Ein Backend, das über Chunks statt über
Seiten rankt, muss also auf die Seite zurückabbilden, bevor es einen Hit zurückgibt.
run_search zählt die Gesamtzahl vor dem Limit und gibt sie im SearchResult mit. Bei
mehreren Backends ist das die Größe der fusionierten Menge, nach fuse.py — das funktioniert
ohne Zutun, solange ein Backend seine vollständige Rangliste zurückgibt und nicht selbst kappt. Ein Backend, das intern ein Top-k zieht, macht total falsch, und zwar still: die Ausgabe
würde wieder eine Vollständigkeit behaupten, die sie nicht hat. Das ist genau der Fehler, den #100 behoben hat, und die einzige echte neue Randbedingung hier. Wenn ein ANN- oder
Reranker-Backend ohne Top-k nicht auskommt, muss es das im SearchResult sichtbar machen,
statt es zu verschlucken.
Das Default-Limit ist eine Konstante (DEFAULT_LIMIT in search/types.py), die CLI, api.search und der MCP-search-Tool teilen. Kein Adapter und kein Backend bringt ein eigenes
mit.
Das ändert nichts an der Kostenschätzung unten und an keinem Akzeptanzkriterium — es sagt nur,
was beim Bauen nicht neu erfunden wird.
Größenordnung: kein ANN-Index nötig
Überschlag für 5.000 Seiten bei ~250-Token-Chunks: grob 20.000–25.000 Chunks, bei 768 Dimensionen
float32 rund 77 MB. Ein linearer Kosinus-Scan darüber liegt im niedrigen zweistelligen
Millisekundenbereich — gegenüber 50–400 ms für das Einbetten der Query selbst.
Ein ANN-Index (HNSW o. ä.) spart also einen Bruchteil eines Vorgangs, der von etwas anderem
dominiert wird, und handelt sich Rekall-Verlust und Indexpflege ein. Brute Force reicht bei
dieser Größenordnung. Das erledigt den größten Teil der Bibliotheksdiskussion, bevor sie
beginnt.
qmd prüfen, bevor etwas nachgebaut wird
kb/entities/tools/qmd.md beschreibt genau das gesuchte Profil. Verifiziert am 2026-09-01 gegen https://github.com/tobi/qmd: qmd ist TypeScript/Bun und löst die Aufgabe über SQLite FTS5 +
sqlite-vec + node-llama-cpp mit GGUF-Modellen (embeddinggemma-300M, Qwen3-Reranker). Es bringt
einen MCP-Server und HTTP-Transport auf localhost:8181 mit.
Damit ist „einhängen statt nachbauen" keine Architekturskizze, sondern eine Konfigurationsfrage.
Zu prüfen, bevor entschieden wird:
Lizenz, Reifegrad, Release-Kadenz, Bus-Faktor des Upstreams
Ob das LLM-Reranking abschaltbar ist. Nicht abschaltbar wäre ein K.-o.-Kriterium: es
kollidiert mit dem Determinismus-Anspruch dieses Stacks (AGENTS.md, „never re-derive, always
compile") und macht dieselbe Anfrage zweimal verschieden beantwortbar.
Ob sich der Index auf kb/ richten lässt, ohne Annahmen über unser Frontmatter zu treffen
Ob es eine vollständige Rangliste herausgibt oder nur ein Top-k — siehe § Ausgabevertrag oben
Die Alternative — ein eigenes Backend in Python — ist bei Brute-Force-Kosinus überschaubar
(NumPy-Dot-Produkt über eine Matrix) und hat den Vorteil, keine Fremdabhängigkeit zu erben. Der
Nachteil ist, dass Embedding-Erzeugung, Chunking und Modellverwaltung dann selbst gebaut werden.
Was der Preis der Grenze ist, ehrlich
Modell-Bindung. Query und Index müssen dasselbe Embedding-Modell benutzen. Ein
Modellwechsel bedeutet Vollreindex. Das ist echte, nicht wegdefinierbare Versionsdrift.
Stille Lücken. Ein veralteter Index liefert weniger, nicht falsch — aber leise. Für einen
Wissenskompiler ist das das schlechtere Fehlerbild. Braucht einen doctor-Check:
Index-Zeitstempel gegen jüngstes modified: im Korpus.
Kaltstart.rg kostet 7 ms; ein Embedding-Prozess kostet Hunderte ms bis Sekunden pro
Start. Das verlangt einen residenten Prozess — den #19 ohnehin mitbringt.
Optionale Abhängigkeit.dist export und doctor müssen sie als optional führen.
Präzedenzfall existiert: RipgrepMissing in search/ripgrep.py.
Ein zweites Ding zum Patchen.
Offene Fragen
Wo kommen die Embeddings her: lokaler Daemon (Ollama/llama-server), In-Process-Bibliothek oder
API? Das entscheidet über Offline-Fähigkeit, Latenz und laufende Kosten.
Chunking-Strategie: ganze Seite, Abschnitt, oder Fenster? Der Korpus hat mit ## -Abschnitten
eine natürliche Struktur, die ein Chunker nutzen könnte. Bei Abschnitt oder Fenster kommt die
Rückabbildung Chunk → Seite dazu, die § Ausgabevertrag verlangt.
Wird der Index gitignoriert (Derivat wie reports/) oder ausgeliefert?
Wechselwirkung mit #6 (Backlink-boosted Ranking): beides sind Ranking-Signale, beide laufen
durch fuse.py.
Akzeptanzkriterien
Entscheidung qmd einhängen vs. eigenes Backend, mit den Prüfpunkten oben belegt
Backend über einen Eintrag in BACKENDS (search/registry.py) erreichbar, --backend wählt es
Kombination mit rg über RRF funktioniert; Frontmatter-Prädikate greifen unverändert
Jeder Treffer trägt einen echten, vollständigen Titel und Pfad; total bleibt die Zahl der
Treffer vor dem Limit, auch bei Fusion zweier Backends
Fehlt die Abhängigkeit, verhält sich wikitool wie bei fehlendem rg: klare Meldung, kein stiller Fallback
doctor meldet Index-Alter gegen den Korpus
An einem Korpus der Zielgröße gemessen, nicht am Demo-Korpus (188 Seiten bei Anlage dieses
Issues, 182 am 2026-09-15)
Auslöser für prio/3: ein Korpus, an dem rg tatsächlich nicht mehr trägt. Vorher ist das
eine Wette auf eine Anforderung, die noch niemand gemessen hat.
Verwandt:#19 (der residente Prozess, in dem der Index lebt), #6 (Ranking-Signale).
Hinweis zum Nachzug
Am 2026-09-15 gegen den Baum geprüft und nachgezogen: § Ausgabevertrag ist neu (Folge von #100);
die Zeilenzahl des Suchmoduls und die Korpusgröße sind als Werte mit Datum stehen geblieben statt
ersetzt zu werden. Der registry.py-Zitatblock wurde gegen die Quelle geprüft und stimmt
weiterhin wörtlich; registry.py, base.py und fuse.py sind seit Anlage des Issues inhaltlich
unverändert.
Aus der Plattform-Debatte vom 2026-09-01. Der Bedarf: hybrides Retrieval (BM25 + Vektor,
semantische Suche) für einen Korpus in der Größenordnung **wenige Tausend Seiten**, weil
`ripgrep` allein dort nicht mehr trägt.
## Die Naht existiert bereits
`tools/chemenu/search/registry.py` sagt es im eigenen Docstring:
> One registry entry today. It exists so that adding a semantic/vector backend is a new module
> plus one line here — not a change to the command, the filters, or the output shape.
> Selecting several at once fuses them through RRF.
Dazu `search/base.py` (Backend-Protokoll, „a backend answers the *text* half of a query and
nothing else"), `search/fuse.py` (Reciprocal Rank Fusion — ausdrücklich gebaut, um lexikalische
und semantische Rankings ohne gemeinsame Score-Skala zu kombinieren) und `search/ripgrep.py`
(betreibt seit Tag eins einen fremdsprachigen Prozess hinter dieser Grenze). Das Suchmodul war
bei Anlage dieses Issues 613 Zeilen groß und ist am 2026-09-15 bei 825 — die Grenze selbst
(`registry.py`, `base.py`, `fuse.py`) ist in der Zeit unverändert geblieben, der Zuwachs liegt
im Backend und im Ausgabepfad.
Frontmatter-Prädikate bleiben in `filters.py` und laufen nachgelagert — ein Vektor-Backend muss
`confidence>=0.8` nie kennen.
## Was ein neues Backend an Ausgabevertrag erbt
Seit `6.0.0-beta.8` (#100) liegen Trefferdarstellung und Trunkierungsauskunft **backend-unabhängig**
in `run_search`/`render_table`, nicht im `rg`-Adapter. Ein semantisches Backend erbt das, statt es
zu wiederholen — und darf es nicht unterlaufen:
- Ein Backend liefert `SearchHit`-Werte; die Trefferzeile
(`score | kind/subtype | titel | pfad | summary`) rendert `render_table` daraus. **Titel und
Pfad müssen echt und vollständig sein** — der Titel ist das Argument, das `touch`/`xref`/`cite`
nehmen, der Pfad das, womit die Seite geöffnet wird. Ein Backend, das über Chunks statt über
Seiten rankt, muss also auf die Seite zurückabbilden, bevor es einen Hit zurückgibt.
- `run_search` zählt die Gesamtzahl **vor** dem Limit und gibt sie im `SearchResult` mit. Bei
mehreren Backends ist das die Größe der fusionierten Menge, nach `fuse.py` — das funktioniert
ohne Zutun, solange ein Backend seine vollständige Rangliste zurückgibt und nicht selbst kappt.
**Ein Backend, das intern ein Top-k zieht, macht `total` falsch**, und zwar still: die Ausgabe
würde wieder eine Vollständigkeit behaupten, die sie nicht hat. Das ist genau der Fehler, den
#100 behoben hat, und die einzige echte neue Randbedingung hier. Wenn ein ANN- oder
Reranker-Backend ohne Top-k nicht auskommt, muss es das im `SearchResult` sichtbar machen,
statt es zu verschlucken.
- Das Default-Limit ist eine Konstante (`DEFAULT_LIMIT` in `search/types.py`), die CLI,
`api.search` und der MCP-`search`-Tool teilen. Kein Adapter und kein Backend bringt ein eigenes
mit.
Das ändert nichts an der Kostenschätzung unten und an keinem Akzeptanzkriterium — es sagt nur,
was beim Bauen nicht neu erfunden wird.
## Größenordnung: kein ANN-Index nötig
Überschlag für 5.000 Seiten bei ~250-Token-Chunks: grob 20.000–25.000 Chunks, bei 768 Dimensionen
float32 rund **77 MB**. Ein linearer Kosinus-Scan darüber liegt im niedrigen zweistelligen
Millisekundenbereich — gegenüber 50–400 ms für das Einbetten der Query selbst.
Ein ANN-Index (HNSW o. ä.) spart also einen Bruchteil eines Vorgangs, der von etwas anderem
dominiert wird, und handelt sich Rekall-Verlust und Indexpflege ein. **Brute Force reicht bei
dieser Größenordnung.** Das erledigt den größten Teil der Bibliotheksdiskussion, bevor sie
beginnt.
## `qmd` prüfen, bevor etwas nachgebaut wird
`kb/entities/tools/qmd.md` beschreibt genau das gesuchte Profil. Verifiziert am 2026-09-01 gegen
<https://github.com/tobi/qmd>: qmd ist **TypeScript/Bun** und löst die Aufgabe über SQLite FTS5 +
sqlite-vec + node-llama-cpp mit GGUF-Modellen (embeddinggemma-300M, Qwen3-Reranker). Es bringt
einen **MCP-Server** und HTTP-Transport auf `localhost:8181` mit.
Damit ist „einhängen statt nachbauen" keine Architekturskizze, sondern eine Konfigurationsfrage.
Zu prüfen, bevor entschieden wird:
- Lizenz, Reifegrad, Release-Kadenz, Bus-Faktor des Upstreams
- Ob das **LLM-Reranking abschaltbar** ist. Nicht abschaltbar wäre ein K.-o.-Kriterium: es
kollidiert mit dem Determinismus-Anspruch dieses Stacks (`AGENTS.md`, „never re-derive, always
compile") und macht dieselbe Anfrage zweimal verschieden beantwortbar.
- Ob sich der Index auf `kb/` richten lässt, ohne Annahmen über unser Frontmatter zu treffen
- Ob es eine vollständige Rangliste herausgibt oder nur ein Top-k — siehe § Ausgabevertrag oben
Die Alternative — ein eigenes Backend in Python — ist bei Brute-Force-Kosinus überschaubar
(NumPy-Dot-Produkt über eine Matrix) und hat den Vorteil, keine Fremdabhängigkeit zu erben. Der
Nachteil ist, dass Embedding-Erzeugung, Chunking und Modellverwaltung dann selbst gebaut werden.
## Was der Preis der Grenze ist, ehrlich
- **Modell-Bindung.** Query und Index müssen dasselbe Embedding-Modell benutzen. Ein
Modellwechsel bedeutet Vollreindex. Das ist echte, nicht wegdefinierbare Versionsdrift.
- **Stille Lücken.** Ein veralteter Index liefert weniger, nicht falsch — aber leise. Für einen
Wissenskompiler ist das das schlechtere Fehlerbild. Braucht einen `doctor`-Check:
Index-Zeitstempel gegen jüngstes `modified:` im Korpus.
- **Kaltstart.** `rg` kostet 7 ms; ein Embedding-Prozess kostet Hunderte ms bis Sekunden pro
Start. Das verlangt einen residenten Prozess — den #19 ohnehin mitbringt.
- **Optionale Abhängigkeit.** `dist export` und `doctor` müssen sie als optional führen.
Präzedenzfall existiert: `RipgrepMissing` in `search/ripgrep.py`.
- **Ein zweites Ding zum Patchen.**
## Offene Fragen
- Wo kommen die Embeddings her: lokaler Daemon (Ollama/llama-server), In-Process-Bibliothek oder
API? Das entscheidet über Offline-Fähigkeit, Latenz und laufende Kosten.
- Chunking-Strategie: ganze Seite, Abschnitt, oder Fenster? Der Korpus hat mit `## `-Abschnitten
eine natürliche Struktur, die ein Chunker nutzen könnte. Bei Abschnitt oder Fenster kommt die
Rückabbildung Chunk → Seite dazu, die § Ausgabevertrag verlangt.
- Wird der Index gitignoriert (Derivat wie `reports/`) oder ausgeliefert?
- Wechselwirkung mit #6 (Backlink-boosted Ranking): beides sind Ranking-Signale, beide laufen
durch `fuse.py`.
## Akzeptanzkriterien
- [ ] Entscheidung `qmd` einhängen vs. eigenes Backend, mit den Prüfpunkten oben belegt
- [ ] Backend über einen Eintrag in `BACKENDS` (`search/registry.py`) erreichbar, `--backend` wählt es
- [ ] Kombination mit `rg` über RRF funktioniert; Frontmatter-Prädikate greifen unverändert
- [ ] Jeder Treffer trägt einen echten, vollständigen Titel und Pfad; `total` bleibt die Zahl der
Treffer vor dem Limit, auch bei Fusion zweier Backends
- [ ] Fehlt die Abhängigkeit, verhält sich `wikitool` wie bei fehlendem `rg`: klare Meldung, kein stiller Fallback
- [ ] `doctor` meldet Index-Alter gegen den Korpus
- [ ] An einem Korpus der Zielgröße gemessen, nicht am Demo-Korpus (188 Seiten bei Anlage dieses
Issues, 182 am 2026-09-15)
**Auslöser für `prio/3`:** ein Korpus, an dem `rg` tatsächlich nicht mehr trägt. Vorher ist das
eine Wette auf eine Anforderung, die noch niemand gemessen hat.
**Verwandt:** #19 (der residente Prozess, in dem der Index lebt), #6 (Ranking-Signale).
## Hinweis zum Nachzug
Am 2026-09-15 gegen den Baum geprüft und nachgezogen: § Ausgabevertrag ist neu (Folge von #100);
die Zeilenzahl des Suchmoduls und die Korpusgröße sind als Werte mit Datum stehen geblieben statt
ersetzt zu werden. Der `registry.py`-Zitatblock wurde gegen die Quelle geprüft und stimmt
weiterhin wörtlich; `registry.py`, `base.py` und `fuse.py` sind seit Anlage des Issues inhaltlich
unverändert.
Changelog: Nachzug nach #100 (6.0.0-beta.8). Neu: § „Was ein neues Backend an Ausgabevertrag erbt" — Trefferzeile und Trunkierungsauskunft liegen jetzt backend-unabhängig in run_search/render_table. Die eine echte neue Randbedingung: ein Backend, das intern ein Top-k zieht, macht total still falsch; wer Chunks statt Seiten rankt, muss vor der Rückgabe auf die Seite zurückabbilden. Daraus je ein Punkt in § Offene Fragen (Chunking), § qmd prüfen (gibt es eine vollständige Rangliste her?) und ein neues Akzeptanzkriterium.
Korrigiert: „das gesamte Suchmodul sind 613 Zeilen" war schon vor #100 veraltet (772) und steht jetzt als datiertes Wertepaar 613 (Anlage) / 825 (2026-09-15), ebenso die Demo-Korpusgröße 188 → 182. Der registry.py-Zitatblock ist gegen die Quelle geprüft und weiterhin wörtlich korrekt; registry.py, base.py und fuse.py sind inhaltlich unverändert, die Grenze trägt also noch.
Keine Entscheidung, kein Label geändert — #35 bleibt kind/decision/prio/waiting, der Auslöser ist unverändert ein Korpus, an dem rg nicht mehr trägt. #6 wurde in derselben Prüfung angesehen und brauchte nichts: es zitiert fuse.pys Sortierschlüssel, der unverändert ist.
**Changelog:** Nachzug nach #100 (`6.0.0-beta.8`). Neu: § „Was ein neues Backend an Ausgabevertrag erbt" — Trefferzeile und Trunkierungsauskunft liegen jetzt backend-unabhängig in `run_search`/`render_table`. Die eine echte neue Randbedingung: ein Backend, das intern ein Top-k zieht, macht `total` still falsch; wer Chunks statt Seiten rankt, muss vor der Rückgabe auf die Seite zurückabbilden. Daraus je ein Punkt in § Offene Fragen (Chunking), § qmd prüfen (gibt es eine vollständige Rangliste her?) und ein neues Akzeptanzkriterium.
Korrigiert: „das gesamte Suchmodul sind 613 Zeilen" war schon vor #100 veraltet (772) und steht jetzt als datiertes Wertepaar 613 (Anlage) / 825 (2026-09-15), ebenso die Demo-Korpusgröße 188 → 182. Der `registry.py`-Zitatblock ist gegen die Quelle geprüft und weiterhin wörtlich korrekt; `registry.py`, `base.py` und `fuse.py` sind inhaltlich unverändert, die Grenze trägt also noch.
Keine Entscheidung, kein Label geändert — #35 bleibt `kind/decision`/`prio/waiting`, der Auslöser ist unverändert ein Korpus, an dem `rg` nicht mehr trägt. #6 wurde in derselben Prüfung angesehen und brauchte nichts: es zitiert `fuse.py`s Sortierschlüssel, der unverändert ist.
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.
Aus der Plattform-Debatte vom 2026-09-01. Der Bedarf: hybrides Retrieval (BM25 + Vektor,
semantische Suche) für einen Korpus in der Größenordnung wenige Tausend Seiten, weil
ripgrepallein dort nicht mehr trägt.Die Naht existiert bereits
tools/chemenu/search/registry.pysagt es im eigenen Docstring:Dazu
search/base.py(Backend-Protokoll, „a backend answers the text half of a query andnothing else"),
search/fuse.py(Reciprocal Rank Fusion — ausdrücklich gebaut, um lexikalischeund semantische Rankings ohne gemeinsame Score-Skala zu kombinieren) und
search/ripgrep.py(betreibt seit Tag eins einen fremdsprachigen Prozess hinter dieser Grenze). Das Suchmodul war
bei Anlage dieses Issues 613 Zeilen groß und ist am 2026-09-15 bei 825 — die Grenze selbst
(
registry.py,base.py,fuse.py) ist in der Zeit unverändert geblieben, der Zuwachs liegtim Backend und im Ausgabepfad.
Frontmatter-Prädikate bleiben in
filters.pyund laufen nachgelagert — ein Vektor-Backend mussconfidence>=0.8nie kennen.Was ein neues Backend an Ausgabevertrag erbt
Seit
6.0.0-beta.8(#100) liegen Trefferdarstellung und Trunkierungsauskunft backend-unabhängigin
run_search/render_table, nicht imrg-Adapter. Ein semantisches Backend erbt das, statt eszu wiederholen — und darf es nicht unterlaufen:
SearchHit-Werte; die Trefferzeile(
score | kind/subtype | titel | pfad | summary) rendertrender_tabledaraus. Titel undPfad müssen echt und vollständig sein — der Titel ist das Argument, das
touch/xref/citenehmen, der Pfad das, womit die Seite geöffnet wird. Ein Backend, das über Chunks statt über
Seiten rankt, muss also auf die Seite zurückabbilden, bevor es einen Hit zurückgibt.
run_searchzählt die Gesamtzahl vor dem Limit und gibt sie imSearchResultmit. Beimehreren Backends ist das die Größe der fusionierten Menge, nach
fuse.py— das funktioniertohne Zutun, solange ein Backend seine vollständige Rangliste zurückgibt und nicht selbst kappt.
Ein Backend, das intern ein Top-k zieht, macht
totalfalsch, und zwar still: die Ausgabewürde wieder eine Vollständigkeit behaupten, die sie nicht hat. Das ist genau der Fehler, den
#100 behoben hat, und die einzige echte neue Randbedingung hier. Wenn ein ANN- oder
Reranker-Backend ohne Top-k nicht auskommt, muss es das im
SearchResultsichtbar machen,statt es zu verschlucken.
DEFAULT_LIMITinsearch/types.py), die CLI,api.searchund der MCP-search-Tool teilen. Kein Adapter und kein Backend bringt ein eigenesmit.
Das ändert nichts an der Kostenschätzung unten und an keinem Akzeptanzkriterium — es sagt nur,
was beim Bauen nicht neu erfunden wird.
Größenordnung: kein ANN-Index nötig
Überschlag für 5.000 Seiten bei ~250-Token-Chunks: grob 20.000–25.000 Chunks, bei 768 Dimensionen
float32 rund 77 MB. Ein linearer Kosinus-Scan darüber liegt im niedrigen zweistelligen
Millisekundenbereich — gegenüber 50–400 ms für das Einbetten der Query selbst.
Ein ANN-Index (HNSW o. ä.) spart also einen Bruchteil eines Vorgangs, der von etwas anderem
dominiert wird, und handelt sich Rekall-Verlust und Indexpflege ein. Brute Force reicht bei
dieser Größenordnung. Das erledigt den größten Teil der Bibliotheksdiskussion, bevor sie
beginnt.
qmdprüfen, bevor etwas nachgebaut wirdkb/entities/tools/qmd.mdbeschreibt genau das gesuchte Profil. Verifiziert am 2026-09-01 gegenhttps://github.com/tobi/qmd: qmd ist TypeScript/Bun und löst die Aufgabe über SQLite FTS5 +
sqlite-vec + node-llama-cpp mit GGUF-Modellen (embeddinggemma-300M, Qwen3-Reranker). Es bringt
einen MCP-Server und HTTP-Transport auf
localhost:8181mit.Damit ist „einhängen statt nachbauen" keine Architekturskizze, sondern eine Konfigurationsfrage.
Zu prüfen, bevor entschieden wird:
kollidiert mit dem Determinismus-Anspruch dieses Stacks (
AGENTS.md, „never re-derive, alwayscompile") und macht dieselbe Anfrage zweimal verschieden beantwortbar.
kb/richten lässt, ohne Annahmen über unser Frontmatter zu treffenDie Alternative — ein eigenes Backend in Python — ist bei Brute-Force-Kosinus überschaubar
(NumPy-Dot-Produkt über eine Matrix) und hat den Vorteil, keine Fremdabhängigkeit zu erben. Der
Nachteil ist, dass Embedding-Erzeugung, Chunking und Modellverwaltung dann selbst gebaut werden.
Was der Preis der Grenze ist, ehrlich
Modellwechsel bedeutet Vollreindex. Das ist echte, nicht wegdefinierbare Versionsdrift.
Wissenskompiler ist das das schlechtere Fehlerbild. Braucht einen
doctor-Check:Index-Zeitstempel gegen jüngstes
modified:im Korpus.rgkostet 7 ms; ein Embedding-Prozess kostet Hunderte ms bis Sekunden proStart. Das verlangt einen residenten Prozess — den #19 ohnehin mitbringt.
dist exportunddoctormüssen sie als optional führen.Präzedenzfall existiert:
RipgrepMissinginsearch/ripgrep.py.Offene Fragen
API? Das entscheidet über Offline-Fähigkeit, Latenz und laufende Kosten.
##-Abschnitteneine natürliche Struktur, die ein Chunker nutzen könnte. Bei Abschnitt oder Fenster kommt die
Rückabbildung Chunk → Seite dazu, die § Ausgabevertrag verlangt.
reports/) oder ausgeliefert?durch
fuse.py.Akzeptanzkriterien
qmdeinhängen vs. eigenes Backend, mit den Prüfpunkten oben belegtBACKENDS(search/registry.py) erreichbar,--backendwählt esrgüber RRF funktioniert; Frontmatter-Prädikate greifen unveränderttotalbleibt die Zahl derTreffer vor dem Limit, auch bei Fusion zweier Backends
wikitoolwie bei fehlendemrg: klare Meldung, kein stiller Fallbackdoctormeldet Index-Alter gegen den KorpusIssues, 182 am 2026-09-15)
Auslöser für
prio/3: ein Korpus, an demrgtatsächlich nicht mehr trägt. Vorher ist daseine Wette auf eine Anforderung, die noch niemand gemessen hat.
Verwandt: #19 (der residente Prozess, in dem der Index lebt), #6 (Ranking-Signale).
Hinweis zum Nachzug
Am 2026-09-15 gegen den Baum geprüft und nachgezogen: § Ausgabevertrag ist neu (Folge von #100);
die Zeilenzahl des Suchmoduls und die Korpusgröße sind als Werte mit Datum stehen geblieben statt
ersetzt zu werden. Der
registry.py-Zitatblock wurde gegen die Quelle geprüft und stimmtweiterhin wörtlich;
registry.py,base.pyundfuse.pysind seit Anlage des Issues inhaltlichunverändert.
Changelog: Nachzug nach #100 (
6.0.0-beta.8). Neu: § „Was ein neues Backend an Ausgabevertrag erbt" — Trefferzeile und Trunkierungsauskunft liegen jetzt backend-unabhängig inrun_search/render_table. Die eine echte neue Randbedingung: ein Backend, das intern ein Top-k zieht, machttotalstill falsch; wer Chunks statt Seiten rankt, muss vor der Rückgabe auf die Seite zurückabbilden. Daraus je ein Punkt in § Offene Fragen (Chunking), § qmd prüfen (gibt es eine vollständige Rangliste her?) und ein neues Akzeptanzkriterium.Korrigiert: „das gesamte Suchmodul sind 613 Zeilen" war schon vor #100 veraltet (772) und steht jetzt als datiertes Wertepaar 613 (Anlage) / 825 (2026-09-15), ebenso die Demo-Korpusgröße 188 → 182. Der
registry.py-Zitatblock ist gegen die Quelle geprüft und weiterhin wörtlich korrekt;registry.py,base.pyundfuse.pysind inhaltlich unverändert, die Grenze trägt also noch.Keine Entscheidung, kein Label geändert — #35 bleibt
kind/decision/prio/waiting, der Auslöser ist unverändert ein Korpus, an demrgnicht mehr trägt. #6 wurde in derselben Prüfung angesehen und brauchte nichts: es zitiertfuse.pys Sortierschlüssel, der unverändert ist.