Skip to content

feat: vouch digest — read-only briefing of pending proposals, recent decisions, and stale items #324

Description

@plind-junior

an operator returning to a kb after a day (or a week) has to run several commands to reconstruct "what needs my attention": vouch list-pending for the review backlog, vouch metrics for approval rate and stale ratio, and a grep through .vouch/decided/ to see what got decided while they were away. a single read-only briefing folds those into one glance — "n proposals awaiting review (oldest first), what was approved/rejected since your last look, which claims have aged past the freshness threshold, and whether citation coverage moved" — so the human at the review gate knows where to spend attention before opening the queue.

this is a pure viewport. it reads the audit log, the decided records, and the artifact files; it writes nothing and touches no proposal. it composes the existing metrics.compute and store reads rather than duplicating any of them.

proposed surface

a new read-only cli command plus a matching kb.* read method:

vouch digest [--since <spec>] [--stale-days N] [--limit N]
             [--format text|json|markdown]
  • --since reuses metrics.parse_since verbatim — a duration (30d, 12h, 2w), an iso date (2026-01-01), or all. default window is a short recent span (e.g. 7d), since a digest is inherently "since last look" (unlike vouch metrics, which defaults to all-history).
  • --stale-days mirrors metrics.DEFAULT_STALE_DAYS (and vouch lint --stale-days) so the three surfaces agree on what "stale" means.
  • --limit caps the oldest-first pending list and the recent-decisions list (default e.g. 10 each) so the briefing stays a briefing.
  • --format selects human text (default), the stable json contract, or a markdown block suitable for pasting into a notification hook or a standup note.

the briefing composes four sections, all derived from sources that already exist on disk:

  • pending awaiting reviewstore.list_proposals(ProposalStatus.PENDING) (the same set behind kb.list_pending), sorted oldest proposed_at first, each row showing kind, proposer, age, and id. surfaces the review-gate backlog metrics.pending_now already counts, but as an actionable list.
  • recent decisions — approvals and rejections in the window, read from .vouch/audit.log.jsonl via audit.read_events (the authoritative, append-only stream), cross-referenced to the .vouch/decided/*.yaml summary for the payload title. reuses the _APPROVE_RE / _REJECT_RE verb matching in metrics.py.
  • stale items — active claims whose freshness anchor (last_confirmed_at or updated_at or created_at) is older than --stale-days, the exact predicate in metrics._fill_corpus_metrics, listed oldest first rather than merely counted.
  • citation-coverage deltametrics.compute gives current citation_coverage; the digest also computes it as of the --since cutoff and reports the movement, so a drop is visible.

a Digest dataclass with a to_dict() stable schema lives in a new src/vouch/digest.py, composing metrics.compute and store reads. no new business logic in storage.py, which stays pure i/o.

review gate & scope

the digest is read-only by construction: it calls list_* / read_events and formats. it never calls propose_*, approve, reject, or any write path, so the review gate is untouched — nothing here can create or edit knowledge, and there is nothing to approve. the four sections are all viewports over .vouch/ files that are already committed; no new on-disk state, no schema migration, fully local-first.

it stays distinct from the nearby aggregate surfaces. #192 / #101 ship vouch metrics — aggregate gauges (rates, ratios, percentiles) for graphing and alerting; digest is the human-facing counterpart of named, oldest-first lists of what to act on now, and consumes metrics.compute rather than recomputing it (it renders the citation-coverage delta over a window rather than a single point value). #164 (vouch stats / kb.stats) answers "is the kb alive?" with artifact counts; digest answers "what changed and what needs me?" over a window. #135 (vouch expire) acts on stale pending proposals through the gate; digest only reports the same backlog read-only — the "see it" that precedes running #135's "clear it".

if kb.digest is exposed over mcp, it must touch the four registration sites — @mcp.tool() in src/vouch/server.py, _h_digest + HANDLERS["kb.digest"] in src/vouch/jsonl_server.py, METHODS in src/vouch/capabilities.py, and the vouch digest command in src/vouch/cli.py — with a test at tests/test_digest.py, and should decide whether to attach the _meta.vouch_salience sidebar the way other reads do.

acceptance criteria

  • vouch digest runs read-only against a .vouch/ and writes nothing (no new files, no audit events, no proposals).
  • pending section lists proposals oldest proposed_at first, capped by --limit, from the same set as kb.list_pending.
  • recent-decisions section reads approvals/rejections from audit.log.jsonl within the --since window, titled via the decided/ record.
  • stale section uses the same predicate and default as metrics._fill_corpus_metrics / vouch lint --stale-days.
  • citation-coverage delta reports movement between the --since cutoff and now.
  • --format text|json|markdown all supported; the json shape is a documented, stable to_dict() schema.
  • --since accepts the same specs as vouch metrics (duration / iso / all) via metrics.parse_since.
  • if exposed over mcp, kb.digest is registered in all four sites and passes test_capabilities.
  • tests/test_digest.py covers oldest-first ordering, window filtering, empty-kb, and the read-only invariant.
  • make check green (pytest, mypy, ruff).

Metadata

Metadata

Assignees

No one assigned

    Labels

    clicommand line interfaceenhancementNew feature or requestsize: 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