Description
kb_search in server.py determines the backend value once based
on which code branch executed, then stamps that single value uniformly
across every result in the returned list:
hits = index_db.search(store.kb_dir, query, limit=limit)
backend = "fts5"
if not hits:
hits = store.search_substring(query, limit=limit)
backend = "substring"
return [
{"kind": k, "id": i, "snippet": s, "score": sc, "backend": backend}
for k, i, s, sc in hits
]
The backend tag is supposed to tell the caller which retrieval engine
produced each result — this is load-bearing information for agents and
tooling that need to interpret relevance scores correctly (FTS5 scores
are BM25-ranked; substring scores are raw hit counts). Stamping a
single global label breaks that contract entirely.
The bug has two concrete failure modes:
- When
state.db is absent or FTS5 raises, the except branch runs
and backend is correctly set to "substring" — but if FTS5
returns an empty list instead of raising, backend stays "fts5"
and substring results are mislabelled.
- Any future code path that merges results from both backends will
mislabel every hit from whichever backend ran second.
Steps to Reproduce
- Ensure
state.db exists but is empty so FTS5 returns no hits
- Call
kb_search with a query that matches via substring
- Observe: all returned results carry
"backend": "fts5" instead of
"backend": "substring"
Actual Behavior
All results share one backend label determined by control flow, not
by which backend produced each individual hit.
Expected Behavior
Each hit should carry the backend tag of the backend that actually
produced it. The label must be attached at the point the hit is
generated, not assigned globally after retrieval.
Environment
- Python 3.11+
- vouch pre-1.0
Description
kb_searchinserver.pydetermines thebackendvalue once basedon which code branch executed, then stamps that single value uniformly
across every result in the returned list:
The
backendtag is supposed to tell the caller which retrieval engineproduced each result — this is load-bearing information for agents and
tooling that need to interpret relevance scores correctly (FTS5 scores
are BM25-ranked; substring scores are raw hit counts). Stamping a
single global label breaks that contract entirely.
The bug has two concrete failure modes:
state.dbis absent or FTS5 raises, the except branch runsand
backendis correctly set to"substring"— but if FTS5returns an empty list instead of raising,
backendstays"fts5"and substring results are mislabelled.
mislabel every hit from whichever backend ran second.
Steps to Reproduce
state.dbexists but is empty so FTS5 returns no hitskb_searchwith a query that matches via substring"backend": "fts5"instead of"backend": "substring"Actual Behavior
All results share one backend label determined by control flow, not
by which backend produced each individual hit.
Expected Behavior
Each hit should carry the backend tag of the backend that actually
produced it. The label must be attached at the point the hit is
generated, not assigned globally after retrieval.
Environment