Skip to content

fix(knowledge): report lexical/ANN score provenance instead of fixed score bands - #2381

Open
ohdearquant wants to merge 5 commits into
mainfrom
knowledge/search-score-provenance
Open

fix(knowledge): report lexical/ANN score provenance instead of fixed score bands#2381
ohdearquant wants to merge 5 commits into
mainfrom
knowledge/search-score-provenance

Conversation

@ohdearquant

Copy link
Copy Markdown
Owner

Replaces the fixed score bands (0.46 / 0.42) with per-hit score provenance and a request-level candidate provenance on knowledge.search.

  • ScoreProvenance on every scored hit (lexical, ann, embedding_rerank), merged on RRF fusion overlap.
  • LexicalCandidateState (matched, no_match, filtered, partial_timeout, timed_out) replaces the bounded full-scan fallback: a genuine FTS miss returns nothing instead of the newest corpus rows.
  • candidate_provenance on the response reports the lexical state and whether a fallback ran.
  • Docs (pack README, ADR-047, api-reference) drop the score bands in favor of provenance-based interpretation.
  • Test fixture fix: the rerank embed stub now yields distinct unit vectors per text; the rerank-runs case is pinned.

First of three stacked changes; the next two build on the LexicalCandidateState vocabulary introduced here.

…score bands

knowledge.search scores are hybrid, request-relative ranking values, not
calibrated probabilities, so the documented fixed score bands were routinely
wrong. A genuine full-text miss also used to fall back to a scan of the
newest corpus rows, which rank fusion could dress up as topically relevant.

Each result now carries score_provenance (lexical/ANN sources, whether
embedding rerank ran, and the normalization used), and the response carries
a top-level candidate_provenance describing the lexical candidate stage
(matched, no match, filtered by status/kind, or timed out) and whether the
returned set is ANN-only. A true lexical miss now returns no candidates
instead of the newest rows.
@ohdearquant
ohdearquant marked this pull request as ready for review September 3, 2026 20:40

@ohdearquant ohdearquant left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated review. Posted by this repository's automated pull-request review pipeline; this is not a human read and does not gate the merge by itself.

Verdict on head 80da848: REQUEST-CHANGES, 1 blocking finding. Finding details are delivered to the review's recipients rather than posted here. Do not merge this head while blocking findings are outstanding; a pipeline comment on a newer head supersedes this one.

… recency fallback

Removing the recency fallback from fetch_fts_candidates made two candidate
classes the scorer still promises unreachable, because fts_knowledge cannot
surface them regardless of corpus content: a query shorter than the trigram
tokenizer's minimum span (e.g. "RAG", "ML") never forms a trigram, so
exact_name_bonus scoring never got a candidate; and tags is not one of the
FTS-indexed columns, so a tag-only query never got one either.

A genuine FTS miss now runs one further bounded, direct-predicate SQL lookup
before reporting empty: atoms whose name contains the raw query
(case-insensitive) or whose tags JSON array contains a query token, capped at
the existing candidate-pool limit and still scoped by namespace/status/kind
eligibility inside the SQL. This is not a reintroduction of the removed
recency scan — it never orders by recency and only returns rows that overlap
the query by name substring or literal tag.

candidate_provenance.lexical reports this outcome as a new exact_match state,
kept distinct from matched (a real FTS/bm25 hit) so a caller can still tell
which lexical mechanism produced the candidates. ADR-047 and the API
reference are updated to document the new state.

Also fixes the rerank-provenance test fixture: the embedding stub's
uniform-scaling vec![v / norm; DIM] shape collapsed to the same vector for
every positive v (norm is v * sqrt(DIM), so v / norm cancels v out entirely),
so it never exercised a genuine cosine difference between texts. Replaced
with one unit basis vector per text position.

@ohdearquant ohdearquant left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated review. Posted by this repository's automated pull-request review pipeline; this is not a human read and does not gate the merge by itself.

Verdict on head ecd4c2a: REQUEST-CHANGES, 1 blocking finding. Finding details are delivered to the review's recipients rather than posted here. Do not merge this head while blocking findings are outstanding; a pipeline comment on a newer head supersedes this one.

The bounded name/tag recovery lookup in fetch_fts_candidates only ran
after a genuine FTS miss (every extracted term matched nothing eligible).
Two earlier return points skipped it entirely: a non-empty FTS match
returned Matched immediately, and an FTS match whose rows were all
ineligible returned Filtered immediately. Either return meant a name- or
tag-only atom was silently dropped whenever the query also happened to
match something else in the trigram index, or matched only an ineligible
row.

The recovery lookup now runs whenever the query yields a name needle or
tag terms, independent of what FTS found, and its rows are unioned
(deduplicated by id, FTS rows ordered first) with whatever FTS produced.
Provenance stays Matched whenever FTS contributed at least one eligible
row; a Filtered or genuine-miss outcome that recovery turns non-empty now
reports ExactMatch, the state already used for recovery-sourced rows, so
no new state variant was needed. Filtered and NoMatch are unchanged when
recovery also finds nothing.

Adds three tests: a non-empty FTS match with a tag-only sibling, an
ineligible-only FTS match with an eligible tag-only sibling, and a dedup
check for an atom reachable both ways. The first two fail against the
prior control flow. Updates the knowledge-pack API reference and ADR-047
to describe the union.

@ohdearquant ohdearquant left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated review. Posted by this repository's automated pull-request review pipeline; this is not a human read and does not gate the merge by itself.

Verdict on head 8bde674: REQUEST-CHANGES, 2 blocking findings. Finding details are delivered to the review's recipients rather than posted here. Do not merge this head while blocking findings are outstanding; a pipeline comment on a newer head supersedes this one.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant