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
a page can land as a stub: a
title, a one-linebody, and few or no entries in itsclaims/sources/entitieslists. 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_pagemethod 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]— scankb.list_pages, select the pages under the thresholds, and file one proposal per qualifying page.--dry-runreports what would be proposed (page id, current body length / citation count, candidate claim ids) without filing anything..vouch/config.yamlunderenrichment.min_body_chars/enrichment.min_citations, overridable per-invocation. "thin" islen(page.body)below the char threshold orlen(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 insynthesize(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 resultingclaim_ids/source_idsbecome the proposal'sclaims/sourceslists, and the deterministicsynthesis_confidenceis carried into the proposal rationale.review gate & scope
the pass proposes, never writes. each enriched page goes through
propose_pageinsrc/vouch/proposals.py(withslug_hint=<page_id>so the revision targets the existing page) and lands as a pending proposal a human clears withkb.approveor rejects withkb.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.pystays 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_pagemust touch the four registration sites —@mcp.toolinsrc/vouch/server.py,_h_enrich_page+HANDLERS["kb.enrich_page"]insrc/vouch/jsonl_server.py,METHODSinsrc/vouch/capabilities.py, and the cli command insrc/vouch/cli.py— plustests/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_charsandenrichment.min_citationsread fromconfig.yamlwith sane defaults; a page qualifies as thin when under the body-char threshold or under the combinedclaims + sourcescitation threshold.kb.enrich_pagebuilds 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.propose_pagewithslug_hintset to the existing page id and lands as a pending proposal; nothing is written to the durable page store without a humankb.approve.vouch enrich-pages --dry-runlists qualifying pages and their candidate claim ids without filing any proposal.test_capabilitiespasses;tests/test_enrich_page.pycovers thin-page selection, the propose-not-write invariant, and the citation-per-sentence guarantee.