Skip to content

feat: vouch diff <id-old> <id-new> — revision diff for claims and pages #327

Description

@plind-junior

already on the 0.1 roadmap (ROADMAP.md: "vouch diff <id-old> <id-new> for claim/page revisions") but not yet filed. when a claim gets superseded, lifecycle.supersede sets old.superseded_by -> new.id and mirrors a SUPERSEDES relation, but there's no way to see what actually changed between the two revisions without eyeballing two yaml files under .vouch/decided/. same for a page that was re-proposed and re-approved. a field-level diff between two artifact revisions makes review-gate history legible: a reviewer approving a superseding claim can see exactly which fields moved.

this is read-only. it reads existing durable artifacts and the audit trail; it never proposes, approves, or writes anything.

related: #197 (trace: provenance graph + impact analysis, closed) walks the graph of relationships between artifacts via kb.why/kb.trace/kb.impact. this issue is narrower and orthogonal: a two-revision, field-level content diff of a single artifact and its successor, not a graph traversal.

proposed surface

cli:

vouch diff <id-old> <id-new> [--format text|json] [--fields text,confidence,evidence,tags]
  • resolves both ids to durable artifacts of the same kind (both claims, or both pages). mismatched kinds is an error.
  • computes a field-level diff over the pydantic model fields: for a claim, text, type, confidence, evidence, entities, tags, status; for a page, title, body, type, claims, tags. scalar fields show old -> new; list fields show added / removed members; body/text show a unified line diff.
  • --format text (default) prints a human-readable summary; --format json returns a structured {kind, old_id, new_id, fields: [{name, change, old, new}]} for programmatic callers.
  • convenience: if <id-new> is omitted and <id-old> has superseded_by set, diff against the superseding revision automatically.

this adds a new kb.diff read method, so it must touch the four registration sites plus a test:

  • @mcp.tool() in src/vouch/server.py
  • _h_diff + HANDLERS["kb.diff"] in src/vouch/jsonl_server.py
  • METHODS in src/vouch/capabilities.py
  • vouch diff command in src/vouch/cli.py
  • test under tests/test_diff.py

the diff computation is business logic, so it lives in a new helper module (or lifecycle.py) — not in storage.py, which stays pure i/o and only fetches the two artifacts by id.

review gate & scope

nothing to gate: kb.diff creates and edits nothing. it reads two already-durable artifacts and, where relevant, walks the append-only audit.log.jsonl (via audit.read_events) to attribute the transition. because it emits no proposal and mutates no artifact, it sits entirely on the read side of proposals.approve() and cannot bypass the gate.

fully local-first: operates on .vouch/decided/ and the local audit log only, no network, no new storage format.

scope-aware where a ViewerContext (from scoping.py) is in play: resolution respects the same visibility rules as other read methods, so a diff can't surface an artifact the caller couldn't already read.

acceptance criteria

  • vouch diff <id-old> <id-new> prints a field-level diff for two claims of the same kind
  • page-to-page diff supported (title/body/type/claims/tags), with a unified line diff for body
  • --format json returns a structured, machine-readable diff object
  • omitting <id-new> diffs against superseded_by when set; clear error when it isn't
  • mismatched-kind or unknown-id inputs produce a clear error, not a stack trace
  • kb.diff registered at all four sites; test_capabilities stays green
  • diff logic lives outside storage.py; storage.py only fetches artifacts
  • read-only: no proposal emitted, no artifact or audit event written (asserted in tests/test_diff.py)
  • respects viewer scope when a ViewerContext is supplied

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