What happened
vouch fsck crashes with an uncaught KeyError on any KB that has ever
had an approved delete proposal.
_check_decided_proposals in src/vouch/health.py builds a presence
dict keyed by ProposalKind, then indexes it by every approved
proposal's own kind:
# src/vouch/health.py:479-497 (before fix)
presence: dict[ProposalKind, set[str]] = {
ProposalKind.CLAIM: set(claims),
ProposalKind.PAGE: set(pages),
ProposalKind.ENTITY: set(entities),
ProposalKind.RELATION: relations,
}
for pr in store.list_proposals(ProposalStatus.APPROVED):
artifact_id = pr.payload.get("id") if isinstance(pr.payload, dict) else None
...
if artifact_id not in presence[pr.kind]:
presence has no entry for ProposalKind.DELETE. But propose_delete()
(src/vouch/proposals.py) files delete proposals with
kind=ProposalKind.DELETE, and approve() explicitly allows a DELETE
proposal to be approved without requiring the target artifact to
pre-exist (it's being removed, not created). Any approved delete
proposal reaching this loop hits presence[ProposalKind.DELETE] →
KeyError, aborting fsck() entirely.
What you expected
vouch fsck should handle DELETE proposals correctly: verify the
target artifact is actually absent (checked against target_kind, the
kind of what was deleted — not pr.kind, which is always DELETE), and
report a Finding if it's invalid or wasn't actually removed, rather than
crashing.
Reproduction
import tempfile, pathlib
from vouch.storage import KBStore
from vouch import proposals, health
d = pathlib.Path(tempfile.mkdtemp())
store = KBStore.init(d)
src = store.put_source(b"evidence", title="s")
pr = proposals.propose_claim(store, text="the sky is blue", evidence=[src.id], proposed_by="agent-a")
claim = proposals.approve(store, pr.id, approved_by="human-b")
del_pr = proposals.propose_delete(store, target_kind="claim", target_id=claim.id, proposed_by="agent-a")
proposals.approve(store, del_pr.id, approved_by="human-c")
health.fsck(store) # raises
Output:
Traceback (most recent call last):
File ".../health.py", line 369, in fsck
_check_decided_proposals(store, claims, pages, entities, findings)
File ".../health.py", line 497, in _check_decided_proposals
if artifact_id not in presence[pr.kind]:
KeyError: <ProposalKind.DELETE: 'delete'>
vouch fsck's CLI entry (src/vouch/cli.py) calls health.fsck() with
no exception handling around it, so the CLI command itself crashes.
tests/test_health.py has zero coverage of delete proposals in
_check_decided_proposals, which is why this went unnoticed.
Environment
- vouch version:
test branch @ current HEAD
- Python version: 3.11+
- OS: any
- Host: any —
vouch fsck / kb.fsck (if exposed) is host-independent
.vouch/ state
Not required to reproduce — the repro above uses a fresh temp KB.
Anything else
This exact defect and a fix were previously submitted as #538
(fix(health): fsck survives approved delete proposals,
CodeRabbit-reviewed, author addressed review feedback) but the PR was
closed unmerged on 2026-07-29 purely for going stale against a
fast-moving test branch (a CHANGELOG.md conflict) — the maintainer's
closing comment explicitly said "this isn't a judgement on the change
itself... happy to look again." Re-verified independently against
current test HEAD: the crash is still live.
Suggested fix: handle ProposalKind.DELETE proposals in a first pass —
resolve target_kind from the payload, report
decided_delete_invalid_target_kind if it's missing/unrecognized, report
decided_delete_artifact_present if the target still exists on disk, and
otherwise record the id as legitimately deleted. A second pass then
checks ordinary create/edit proposals against presence, skipping any
artifact id that a delete proposal legitimately removed — otherwise the
original creating proposal (still recorded as APPROVED in decided/)
false-positives as decided_missing_artifact once its artifact is
correctly deleted.
What happened
vouch fsckcrashes with an uncaughtKeyErroron any KB that has everhad an approved delete proposal.
_check_decided_proposalsinsrc/vouch/health.pybuilds apresencedict keyed by
ProposalKind, then indexes it by every approvedproposal's own kind:
presencehas no entry forProposalKind.DELETE. Butpropose_delete()(
src/vouch/proposals.py) files delete proposals withkind=ProposalKind.DELETE, andapprove()explicitly allows aDELETEproposal to be approved without requiring the target artifact to
pre-exist (it's being removed, not created). Any approved delete
proposal reaching this loop hits
presence[ProposalKind.DELETE]→KeyError, abortingfsck()entirely.What you expected
vouch fsckshould handleDELETEproposals correctly: verify thetarget artifact is actually absent (checked against
target_kind, thekind of what was deleted — not
pr.kind, which is alwaysDELETE), andreport a Finding if it's invalid or wasn't actually removed, rather than
crashing.
Reproduction
Output:
vouch fsck's CLI entry (src/vouch/cli.py) callshealth.fsck()withno exception handling around it, so the CLI command itself crashes.
tests/test_health.pyhas zero coverage of delete proposals in_check_decided_proposals, which is why this went unnoticed.Environment
testbranch @ current HEADvouch fsck/kb.fsck(if exposed) is host-independent.vouch/stateNot required to reproduce — the repro above uses a fresh temp KB.
Anything else
This exact defect and a fix were previously submitted as #538
(
fix(health): fsck survives approved delete proposals,CodeRabbit-reviewed, author addressed review feedback) but the PR was
closed unmerged on 2026-07-29 purely for going stale against a
fast-moving
testbranch (aCHANGELOG.mdconflict) — the maintainer'sclosing comment explicitly said "this isn't a judgement on the change
itself... happy to look again." Re-verified independently against
current
testHEAD: the crash is still live.Suggested fix: handle
ProposalKind.DELETEproposals in a first pass —resolve
target_kindfrom the payload, reportdecided_delete_invalid_target_kindif it's missing/unrecognized, reportdecided_delete_artifact_presentif the target still exists on disk, andotherwise record the id as legitimately deleted. A second pass then
checks ordinary create/edit proposals against
presence, skipping anyartifact id that a delete proposal legitimately removed — otherwise the
original creating proposal (still recorded as
APPROVEDindecided/)false-positives as
decided_missing_artifactonce its artifact iscorrectly deleted.