_retrieve returns each hit as (kind, id, summary, score, backend) — one opaque score and a backend label. a reviewer tuning retrieval (fusion weights, the reranker in #5, the recency/frequency signals in #317) has no way to see why an artifact surfaced or got dropped: how much came from lexical vs. semantic rank, what the rrf contribution was, whether the reranker moved it, whether a gate (budget, uncited, status filter) cut it. the ranker is a black box exactly where it most needs to be inspectable.
this proposes a read-only kb.explain_ranking that returns the per-candidate breakdown behind a query.
proposed surface
vouch explain-ranking "<query>" [--limit N] [--format text|json]
# kb.explain_ranking over mcp/jsonl
per candidate, return: lexical rank (FTS5), semantic rank (embedding), the rrf contribution, the rerank delta when #5 is enabled, the recency/frequency factors when #317 is enabled, the salience factor, and the gate outcome (kept / budget-dropped / uncited / status-filtered). it re-runs the existing _retrieve pipeline in an instrumented mode rather than duplicating the scoring math.
four registration sites (server.py, jsonl_server.py, capabilities.py, cli.py) plus tests/test_explain_ranking.py. the corresponding UI panel belongs in the vouch-ui console, not src/vouch/web — this issue is the backend that feeds it.
review gate & scope
pure read-time introspection over the retrieval pipeline — no writes, no proposals, no kb.approve, no lifecycle change. it exposes only what kb.context already computes internally, viewer-scoped the same way, so it can't reveal artifacts a caller couldn't already retrieve. storage.py stays pure i/o; the instrumentation lives in the retrieval layer.
acceptance criteria
related: #5 (rerank — this explains its effect), #317 (recency/frequency signals to surface), #92 (backend config).
_retrievereturns each hit as(kind, id, summary, score, backend)— one opaque score and a backend label. a reviewer tuning retrieval (fusion weights, the reranker in #5, the recency/frequency signals in #317) has no way to see why an artifact surfaced or got dropped: how much came from lexical vs. semantic rank, what the rrf contribution was, whether the reranker moved it, whether a gate (budget, uncited, status filter) cut it. the ranker is a black box exactly where it most needs to be inspectable.this proposes a read-only
kb.explain_rankingthat returns the per-candidate breakdown behind a query.proposed surface
per candidate, return: lexical rank (FTS5), semantic rank (embedding), the rrf contribution, the rerank delta when #5 is enabled, the recency/frequency factors when #317 is enabled, the salience factor, and the gate outcome (
kept/budget-dropped/uncited/status-filtered). it re-runs the existing_retrievepipeline in an instrumented mode rather than duplicating the scoring math.four registration sites (
server.py,jsonl_server.py,capabilities.py,cli.py) plustests/test_explain_ranking.py. the corresponding UI panel belongs in the vouch-ui console, notsrc/vouch/web— this issue is the backend that feeds it.review gate & scope
pure read-time introspection over the retrieval pipeline — no writes, no proposals, no
kb.approve, no lifecycle change. it exposes only whatkb.contextalready computes internally, viewer-scoped the same way, so it can't reveal artifacts a caller couldn't already retrieve.storage.pystays pure i/o; the instrumentation lives in the retrieval layer.acceptance criteria
kb.explain_ranking(query)returns per-candidate component scores and the gate decision, read-only.kb.context— no artifact is exposed that the caller couldn't retrieve.test_capabilitiespasses.tests/test_explain_ranking.pycovers a fused-only query and a gate-dropped candidate.make checkgreen.related: #5 (rerank — this explains its effect), #317 (recency/frequency signals to surface), #92 (backend config).