retrieval today answers "what does the kb know about x" (kb.search over claim/page/entity text) but not "who or what does the kb know about x." given a topic string, an agent often wants the entities most strongly anchored to it — the person, project, or concept carrying the most approved evidence — so it can pull the right write-ups or ask the right follow-up. that ranking is already latent in the data: every approved Claim carries entities: list[str] and evidence: list[str], and index_db.search already surfaces claim/entity fts hits. kb.experts exposes it as a first-class read query.
this is read-only. it aggregates already-approved artifacts and returns a ranking; it never proposes, writes, or mutates anything. the entity-scoring shape follows what src/vouch/salience.py:compute_salience already computes in-memory (rank entities by matched-claim count), promoted to an on-demand query keyed by an explicit topic rather than a session's query buffer.
proposed surface
new read method kb.experts.
- cli:
vouch experts "<topic>" [--limit N] [--min-claims N] [--weight count|recency|citation] [--json]
- method args:
kb.experts(topic: str, limit: int = 10, min_claims: int = 1, weight: str = "count")
- matching: resolve the topic to candidate claims via
index_db.search(kb_dir, topic) (fts over claim/entity text) plus the existing substring pass on entity name/aliases (salience._substring_entity_ids); collect the entities referenced by the matched approved claims.
- ranking weights:
count — number of matched claims referencing the entity.
recency — decay-weighted by Claim.updated_at / Claim.last_confirmed_at.
citation — weighted by distinct Claim.evidence ids (source/evidence breadth) and Claim.confidence.
- result shape:
[{ "entity_id", "name", "type", "claim_count", "citation_count", "score", "top_claim_ids": [...] }], ranked descending, with a deterministic tie-break on entity_id.
review gate & scope
pure read. no propose_*, no approve, no new artifacts — so the review gate is untouched by construction. it only reads approved claims already past the gate. exclude non-live statuses (superseded, archived, redacted) from the aggregation, consistent with the fix tracked in #78, so a superseded claim never inflates an entity's score. ranking/status logic lives in a new dedicated helper src/vouch/experts.py (not recall.py, which is the session-digest module, and not storage.py, which stays pure i/o). runs entirely against the local .vouch/ kb and state.db; zero network, zero llm calls.
new kb.* method → touch the four registration sites plus a test:
acceptance criteria
adjacent issues: #223 (entity-salience retrieval reflex) auto-prefetches candidates from a session's buffered query context via compute_salience; kb.experts is the same scoring promoted to an explicit-topic on-demand query with no session buffer and no auto-prefetch, so it's a caller-driven read rather than a background reflex. #184 (kb.neighbors / --expand-graph) walks typed edges outward from a seed id — it needs an id and traverses relations; kb.experts takes free-text and ranks by aggregate claim/citation weight rather than edge traversal, so the two compose (rank with experts, expand a winner with neighbors) but don't overlap. #222 (kb.synthesize) produces synthesized prose over retrieved claims; kb.experts returns a structured entity ranking with no synthesis step and no llm call.
retrieval today answers "what does the kb know about x" (
kb.searchover claim/page/entity text) but not "who or what does the kb know about x." given a topic string, an agent often wants the entities most strongly anchored to it — the person, project, or concept carrying the most approved evidence — so it can pull the right write-ups or ask the right follow-up. that ranking is already latent in the data: every approvedClaimcarriesentities: list[str]andevidence: list[str], andindex_db.searchalready surfaces claim/entity fts hits.kb.expertsexposes it as a first-class read query.this is read-only. it aggregates already-approved artifacts and returns a ranking; it never proposes, writes, or mutates anything. the entity-scoring shape follows what
src/vouch/salience.py:compute_saliencealready computes in-memory (rank entities by matched-claim count), promoted to an on-demand query keyed by an explicit topic rather than a session's query buffer.proposed surface
new read method
kb.experts.vouch experts "<topic>" [--limit N] [--min-claims N] [--weight count|recency|citation] [--json]kb.experts(topic: str, limit: int = 10, min_claims: int = 1, weight: str = "count")index_db.search(kb_dir, topic)(fts over claim/entity text) plus the existing substring pass on entity name/aliases (salience._substring_entity_ids); collect theentitiesreferenced by the matched approved claims.count— number of matched claims referencing the entity.recency— decay-weighted byClaim.updated_at/Claim.last_confirmed_at.citation— weighted by distinctClaim.evidenceids (source/evidence breadth) andClaim.confidence.[{ "entity_id", "name", "type", "claim_count", "citation_count", "score", "top_claim_ids": [...] }], ranked descending, with a deterministic tie-break onentity_id.review gate & scope
pure read. no
propose_*, noapprove, no new artifacts — so the review gate is untouched by construction. it only reads approved claims already past the gate. exclude non-live statuses (superseded,archived,redacted) from the aggregation, consistent with the fix tracked in #78, so a superseded claim never inflates an entity's score. ranking/status logic lives in a new dedicated helpersrc/vouch/experts.py(notrecall.py, which is the session-digest module, and notstorage.py, which stays pure i/o). runs entirely against the local.vouch/kb andstate.db; zero network, zero llm calls.new
kb.*method → touch the four registration sites plus a test:src/vouch/server.py—@mcp.tool()kb_expertssrc/vouch/jsonl_server.py—_h_experts+HANDLERS["kb.experts"]src/vouch/capabilities.py— addkb.expertstoMETHODSsrc/vouch/cli.py—vouch expertsmirrortests/test_experts.pyacceptance criteria
kb.experts(topic)returns entities ranked by evidence density, most-associated first, across mcp / jsonl / cli with identical results.weightacceptscount|recency|citation; an unknown value falls back tocount(no crash), matching the defensive-config style used elsewhere.min_claimsfilters out entities below the threshold;limitcaps the returned list.superseded/archived/redactedare excluded from scoring.entity_idfor equal scores.test_capabilitiespasses withkb.expertspresent at all four sites.tests/test_experts.pycovers each weight mode, the status-exclusion filter,min_claims/limit, and the empty-kb / no-match cases.propose_*/approvecalls in the path; storage stays pure i/o;make checkgreen.adjacent issues: #223 (entity-salience retrieval reflex) auto-prefetches candidates from a session's buffered query context via
compute_salience;kb.expertsis the same scoring promoted to an explicit-topic on-demand query with no session buffer and no auto-prefetch, so it's a caller-driven read rather than a background reflex. #184 (kb.neighbors/--expand-graph) walks typed edges outward from a seed id — it needs an id and traverses relations;kb.expertstakes free-text and ranks by aggregate claim/citation weight rather than edge traversal, so the two compose (rank withexperts, expand a winner withneighbors) but don't overlap. #222 (kb.synthesize) produces synthesized prose over retrieved claims;kb.expertsreturns a structured entity ranking with no synthesis step and no llm call.