Skip to content

feat: kb.experts — rank entities by evidence density on a topic #315

Description

@plind-junior

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:

  • src/vouch/server.py@mcp.tool() kb_experts
  • src/vouch/jsonl_server.py_h_experts + HANDLERS["kb.experts"]
  • src/vouch/capabilities.py — add kb.experts to METHODS
  • src/vouch/cli.pyvouch experts mirror
  • tests/test_experts.py

acceptance criteria

  • kb.experts(topic) returns entities ranked by evidence density, most-associated first, across mcp / jsonl / cli with identical results.
  • weight accepts count | recency | citation; an unknown value falls back to count (no crash), matching the defensive-config style used elsewhere.
  • min_claims filters out entities below the threshold; limit caps the returned list.
  • claims with status superseded / archived / redacted are excluded from scoring.
  • ranking is deterministic — a stable tie-break on entity_id for equal scores.
  • test_capabilities passes with kb.experts present at all four sites.
  • tests/test_experts.py covers each weight mode, the status-exclusion filter, min_claims / limit, and the empty-kb / no-match cases.
  • no propose_* / approve calls in the path; storage stays pure i/o; make check green.

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or requestretrievalcontext, search, synthesis, and evaluationsize: S50-199 changed non-doc lines

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions