Skip to content

feat: thin-page enrichment — propose expansions for stub/sparse pages #309

Description

@plind-junior

a page can land as a stub: a title, a one-line body, and few or no entries in its claims/sources/entities lists. those stubs are the pages least useful to a retrieving agent, and the ones most likely to stay thin because nobody circles back to fill them in. meanwhile the kb often already holds approved claims and registered sources that speak directly to the stub's topic — they just were never woven into the page.

this asks for a maintenance pass that finds thin pages and, for each, drafts an enriched revision synthesized strictly from related approved material already in the kb. no external network fetch, no new facts invented — the enriched body is assembled from claims and sources the review gate already accepted, and every added sentence carries its [claim_id] citation. the enriched page is itself a proposal a human must approve; nothing is written durably by the pass.

proposed surface

a new kb.enrich_page method plus a batch cli entry point.

  • vouch enrich-page <page_id> [--dry-run] — score one page against the thin-page thresholds, and if it qualifies, build an enriched revision and file it as a page proposal.
  • vouch enrich-pages [--min-body-chars N] [--min-citations N] [--limit N] [--dry-run] — scan kb.list_pages, select the pages under the thresholds, and file one proposal per qualifying page. --dry-run reports what would be proposed (page id, current body length / citation count, candidate claim ids) without filing anything.
  • thresholds default from .vouch/config.yaml under enrichment.min_body_chars / enrichment.min_citations, overridable per-invocation. "thin" is len(page.body) below the char threshold or len(page.claims) + len(page.sources) below the citation threshold.

for each thin page, candidate approved material is gathered by running build_context_pack (src/vouch/context.py) with the page title/body as the query, then filtered to claim hits whose status is not in _RETRACTED_CLAIM_STATUSES. the enriched body is produced deterministically by reusing the rendering path in synthesize (src/vouch/synthesize.py), which already emits one cited clause per claim — reuse it rather than adding a second synthesis code path, so the "every sentence traces to an approved claim" invariant holds identically. the resulting claim_ids / source_ids become the proposal's claims / sources lists, and the deterministic synthesis_confidence is carried into the proposal rationale.

review gate & scope

the pass proposes, never writes. each enriched page goes through propose_page in src/vouch/proposals.py (with slug_hint=<page_id> so the revision targets the existing page) and lands as a pending proposal a human clears with kb.approve or rejects with kb.reject. background or scheduled invocation is fine precisely because it can only ever produce pending proposals — it holds no approve capability and touches no durable artifact directly. storage.py stays pure i/o; the thin-page selection and the "build a revision from approved claims only" logic live alongside the other proposal-building code, not in the store.

fully local-first: candidate material is drawn only from claims and sources already resolved in this kb via the existing retrieval path — no network fetch and no new dependency on any external component.

as a new kb.* method, kb.enrich_page must touch the four registration sites — @mcp.tool in src/vouch/server.py, _h_enrich_page + HANDLERS["kb.enrich_page"] in src/vouch/jsonl_server.py, METHODS in src/vouch/capabilities.py, and the cli command in src/vouch/cli.py — plus tests/test_enrich_page.py.

relationship to #222 (kb.synthesize): #222 answers an ad-hoc query in prose and returns the string to the caller, writing nothing. this issue reuses that same deterministic synthesis machinery but points it at an existing thin page and files the result as a review-gated page revision. the distinction is the target (a durable page vs a transient answer) and the outcome (a proposal through the gate vs a returned string).

acceptance criteria

  • enrichment.min_body_chars and enrichment.min_citations read from config.yaml with sane defaults; a page qualifies as thin when under the body-char threshold or under the combined claims + sources citation threshold.
  • kb.enrich_page builds candidate material only from approved, non-retracted claims/sources already in the kb — no network fetch, no invented sentences; every added sentence carries its [claim_id] citation.
  • each enriched page is filed via propose_page with slug_hint set to the existing page id and lands as a pending proposal; nothing is written to the durable page store without a human kb.approve.
  • vouch enrich-pages --dry-run lists qualifying pages and their candidate claim ids without filing any proposal.
  • pages already above the thresholds, and pages with no related approved claims, are skipped (no empty or no-op proposals).
  • all four registration sites updated and test_capabilities passes; tests/test_enrich_page.py covers thin-page selection, the propose-not-write invariant, and the citation-per-sentence guarantee.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or requestretrievalcontext, search, synthesis, and evaluationsize: M200-499 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