Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,17 @@ All notable changes to vouch are documented here. Format follows

## [Unreleased]

### Added
- `kb.enrich_page` / `vouch enrich-page` / `vouch enrich-pages` — thin-page
enrichment: scores a page against `enrichment.min_body_chars` /
`enrichment.min_citations` in `.vouch/config.yaml`, and drafts an addition
synthesized strictly from approved, non-retracted claims already in the KB
(reusing `kb.synthesize`'s machinery) appended to the page's existing body.
Never writes durably — the enriched revision is filed as a pending page
proposal (`slug_hint` targets the existing page id) for a human to clear
with `vouch review`. `--dry-run`, `--min-body-chars`, `--min-citations`,
`--limit` (batch only), `--json`. see `docs/enrich.md`.

## [1.2.2] — 2026-07-07

### Packaging
Expand Down
61 changes: 61 additions & 0 deletions docs/enrich.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# vouch enrich-page / enrich-pages — thin-page enrichment

A page can land as a stub: a title, a one-line body, and few or no
`claims`/`sources` citations. Those 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 — even though the KB often already holds
approved claims that speak directly to the stub's topic.

`vouch enrich-page` / `vouch enrich-pages` find those thin pages and draft
an enriched revision synthesized *strictly* from related, approved, non-
retracted claims already in the KB (reusing the same machinery as
`kb.synthesize` — see `docs/`) — no external fetch, no invented sentences.
Like `vouch compile`, this only ever proposes: the enriched page is filed
as a pending page proposal a human clears with `vouch review`.

## What counts as thin

A page qualifies when either threshold is under its configured minimum:

```yaml
enrichment:
min_body_chars: 200 # page.body shorter than this...
min_citations: 2 # ...or len(page.claims) + len(page.sources) below this
```

Both are overridable per-invocation with `--min-body-chars` / `--min-citations`.

## Run it

```bash
vouch enrich-page <page_id> # score + draft one page
vouch enrich-page <page_id> --dry-run # show what would be proposed, file nothing

vouch enrich-pages # scan all pages, file one proposal per thin page
vouch enrich-pages --dry-run # list qualifying pages + candidate claim ids
vouch enrich-pages --limit 10 # cap how many thin pages are processed this run
vouch review # the human half: approve / reject each draft
```

## How the addition is built

For a qualifying page, the page's title + body become the query for
`kb.synthesize`, which answers strictly from approved, non-retracted claims
with one cited clause per claim. The result is appended to the page's
existing body (the stub's hand-written text is never discarded or
overwritten) and its cited claim ids are unioned with the page's existing
`claims`. Evidence ids of those claims that resolve to registered sources
are unioned into the page's `sources` list the same way.

A page is skipped (no proposal filed) when:

- it's already above both thresholds,
- no approved claim matches its topic (nothing to cite), or
- the proposal is rejected at the gate (e.g. a page kind's required fields
tightened since the page was last approved) — this drops only that one
page rather than sinking a batch `enrich-pages` run.

Proposals are filed by the `page-enricher` actor (or `VOUCH_AGENT` when
set), so under the default gate the reviewing human is always a different
actor and self-approval stays impossible. Each non-dry `enrich-pages` run
appends an `enrich_pages.run` audit event with the filed proposal ids.
1 change: 1 addition & 0 deletions src/vouch/capabilities.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@
"kb.detect_themes",
"kb.propose_theme",
"kb.compile",
"kb.enrich_page",
]


Expand Down
82 changes: 82 additions & 0 deletions src/vouch/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@

from . import __version__, bundle, health
from . import audit as audit_mod
from . import enrich as enrich_mod
from . import lifecycle as life
from . import sessions as sess_mod
from . import verify as verify_mod
Expand Down Expand Up @@ -446,6 +447,87 @@ def session_end_cmd(session_id: str, note: str | None) -> None:
_emit_json({"session": sess.id, "proposals": sess.proposal_ids})


@cli.command(name="enrich-page")
@click.argument("page_id")
@click.option("--min-body-chars", type=int, default=None,
help="Override enrichment.min_body_chars for this run.")
@click.option("--min-citations", type=int, default=None,
help="Override enrichment.min_citations for this run.")
@click.option("--dry-run", is_flag=True, help="Score and draft; file nothing.")
@click.option("--json", "as_json", is_flag=True, help="Machine-readable report.")
def enrich_page_cmd(
page_id: str, min_body_chars: int | None, min_citations: int | None,
dry_run: bool, as_json: bool,
) -> None:
"""Draft an enriched revision of one thin page (a page proposal).

Scores PAGE_ID against the thin-page thresholds and, if it qualifies,
synthesizes an addition strictly from approved claims already in the KB
and files it as a pending page proposal. Approval stays a separate
human step (`vouch review`).
"""
store = _load_store()
actor = os.environ.get("VOUCH_AGENT") or enrich_mod.ENRICH_ACTOR
with _cli_errors():
result = enrich_mod.enrich_page(
store, page_id, actor=actor,
min_body_chars=min_body_chars, min_citations=min_citations,
dry_run=dry_run,
)
if as_json:
_emit_json(result.to_dict())
return
if result.skipped_reason:
click.echo(f"skipped {page_id}: {result.skipped_reason}")
return
verb = "would propose" if dry_run else "proposed"
click.echo(f"{verb} enrichment of {page_id} citing {len(result.claim_ids)} claim(s)"
f"{'' if dry_run else f': {result.proposal_id}'}")
if not dry_run:
click.echo("run `vouch review` to decide.")


@cli.command(name="enrich-pages")
@click.option("--min-body-chars", type=int, default=None,
help="Override enrichment.min_body_chars for this run.")
@click.option("--min-citations", type=int, default=None,
help="Override enrichment.min_citations for this run.")
@click.option("--limit", type=int, default=None,
help="Cap how many thin pages are processed this run.")
@click.option("--dry-run", is_flag=True,
help="Report qualifying pages and candidate claims; file nothing.")
@click.option("--json", "as_json", is_flag=True, help="Machine-readable report.")
def enrich_pages_cmd(
min_body_chars: int | None, min_citations: int | None, limit: int | None,
dry_run: bool, as_json: bool,
) -> None:
"""Scan pages for thin ones and file an enrichment proposal for each.

`--dry-run` lists the qualifying pages and their candidate claim ids
without filing anything.
"""
store = _load_store()
actor = os.environ.get("VOUCH_AGENT") or enrich_mod.ENRICH_ACTOR
report = enrich_mod.enrich_pages(
store, actor=actor, triggered_by=_whoami(),
min_body_chars=min_body_chars, min_citations=min_citations,
limit=limit, dry_run=dry_run,
)
if as_json:
_emit_json(report.to_dict())
return
verb = "would propose" if dry_run else "proposed"
click.echo(f"{verb} enrichment for {len(report.proposed)} page(s):")
for row in report.proposed:
click.echo(f" • {row['page_id']} {row['proposal_id']}")
if report.skipped:
click.echo(f"skipped {len(report.skipped)}:")
for row in report.skipped:
click.echo(f" • {row['page_id']} — {row['reason']}")
if report.proposed and not dry_run:
click.echo("run `vouch review` to decide.")


@cli.command()
@click.argument("session_id")
@click.option("--no-page", is_flag=True, help="Skip the session-summary page.")
Expand Down
Loading
Loading