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
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.supersedesetsold.superseded_by -> new.idand mirrors aSUPERSEDESrelation, 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:
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/textshow a unified line diff.--format text(default) prints a human-readable summary;--format jsonreturns a structured{kind, old_id, new_id, fields: [{name, change, old, new}]}for programmatic callers.<id-new>is omitted and<id-old>hassuperseded_byset, diff against the superseding revision automatically.this adds a new
kb.diffread method, so it must touch the four registration sites plus a test:@mcp.tool()insrc/vouch/server.py_h_diff+HANDLERS["kb.diff"]insrc/vouch/jsonl_server.pyMETHODSinsrc/vouch/capabilities.pyvouch diffcommand insrc/vouch/cli.pytests/test_diff.pythe diff computation is business logic, so it lives in a new helper module (or
lifecycle.py) — not instorage.py, which stays pure i/o and only fetches the two artifacts by id.review gate & scope
nothing to gate:
kb.diffcreates and edits nothing. it reads two already-durable artifacts and, where relevant, walks the append-onlyaudit.log.jsonl(viaaudit.read_events) to attribute the transition. because it emits no proposal and mutates no artifact, it sits entirely on the read side ofproposals.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(fromscoping.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 kindtitle/body/type/claims/tags), with a unified line diff forbody--format jsonreturns a structured, machine-readable diff object<id-new>diffs againstsuperseded_bywhen set; clear error when it isn'tkb.diffregistered at all four sites;test_capabilitiesstays greenstorage.py;storage.pyonly fetches artifactstests/test_diff.py)ViewerContextis supplied