Skip to content

feat(delete): cascade delete — the referrers ride along in the proposal - #719

Merged
plind-junior merged 5 commits into
vouchdev:testfrom
minion1227:feat/cascade-delete
Jul 31, 2026
Merged

feat(delete): cascade delete — the referrers ride along in the proposal#719
plind-junior merged 5 commits into
vouchdev:testfrom
minion1227:feat/cascade-delete

Conversation

@minion1227

Copy link
Copy Markdown
Contributor

Closes #600

removing an approved claim usually fails with cannot delete claim <id>: referenced by page '<page>'. the block is right — referenced_by() refuses while anything still points at the target — but in a compiled kb it means most claims are permanently undeletable, because pages cite claims in bulk. and a supersede pair is mutually locked: b lists a in supersedes, a's superseded_by points back at b, so no delete ordering removes either half of a chain. the only workaround was hand-editing the page's claims: list first, which is exactly the parallel data path the north star forbids.

$ vouch propose-delete claim auth-uses-saml-everywhere
Error: cannot delete claim auth-uses-saml-everywhere: referenced by page 'auth-design'
(supersede it instead?) — or re-file with cascade to include the referrer edits in
this proposal (CLI: --cascade)

$ vouch propose-delete claim auth-uses-saml-everywhere --cascade
prop-01K9…

$ vouch approve prop-01K9…      # one decision, target + referrers

the invariant i most want reviewed

the gate is satisfied, not bypassed. cascade does not lower the bar for approving a delete; it changes what the reviewer is asked to approve. _apply_cascade runs before the target is touched, and then the existing approve-time referenced_by re-check still has to come back empty before deleter() is called. if the cascade missed a referrer, the delete refuses exactly as it does today. i deliberately did not add an "ignore refs when cascading" branch — the gate re-check is the thing that makes this safe to review.

the one place that genuinely had to change is _payload_block_reason, which refuses a DELETE proposal whose target is referenced. that runs inside approve() too, so without it a cascade proposal was unapprovable. it now skips the refusal only when the payload carries a cascade — i.e. only when a reviewer has already seen and approved the referrer edits.

the plan is re-derived at approve time, not replayed. same posture as the ref re-check: the kb may have moved since filing. a referrer added after propose is still unlinked (test_a_referrer_added_after_propose_is_still_unlinked); one removed since is simply absent from the new plan and is not fatal. the payload keeps the plan the reviewer actually saw, as the record of what they approved — test_payload_keeps_the_plan_the_reviewer_saw pins that the two can diverge and that the stored copy does not get rewritten.

why relations are deleted and nodes are not

pages and claims lose their pointer; relations are deleted outright. an edge whose endpoint is gone has no meaning, and referenced_by already returns [] for a relation — edges carry no inbound refs of their own. that is what bounds this: the walk is one level deep by construction, so there is no transitive cascade and no depth limit to argue about. the far endpoint of a deleted edge is never touched (test_cascade_deletes_the_relations_that_pointed_at_the_target asserts c2 survives).

pages lose the frontmatter entry and the inline [claim: …] body markers, through the same strip_claim_markers helper wipe_dead_refs uses. dropping only the frontmatter would leave the body rendering a citation whose claim no longer exists, which is the "silently corrupting pages" outcome #600 explicitly rejects.

what i did not do

the kind-blind relation match. referenced_by compares the claim id against bare relation endpoints, which carry no kind tag, so a same-slug artifact of a different kind can block an unrelated claim. #600 calls this out and says it "could land first" as a smaller separate change. cascade inherits the imprecision — it would delete that same mismatched edge. i left it alone rather than widen this pr into a second behaviour change; it deserves its own diff and its own tests, and it is not made worse here.

no console change. #600 also asks for DeleteArtifactButton to offer "delete with cascade". that is a src/vouch/web/ change on a surface with its own screenshot gate, and this pr is already the full kb.* + cli surface. happy to follow up if you want them in one piece.

no new kb.* method, so capabilities.METHODS is untouched — this is a new optional parameter on an existing method. the four registration sites still applied, and test_capabilities would not have caught a missing cli flag, so the three surfaces are pinned individually.

review gate

filing a cascade proposal writes nothing: test_proposing_a_cascade_writes_nothing pins the page's claim list, its body markers, the target claim, and the audit-event count across a propose_delete(cascade=True). self-approval is still refused on a human-reviewed kb, and the refusal leaves every referrer intact.

every cascade edit lands its own irreversible audit event — page.cascade_unlink, claim.cascade_unlink, relation.delete — and the {kind}.delete event names what it touched, so the log answers "what else changed when this was deleted" without a join.

tests

32 cases in tests/test_cascade_delete.py: today's refusal unchanged and the message naming the flag; plan shape mirroring referenced_by for every target kind; the supersede pair unlocked from both ends; superseded_by cleared, contradicts unlinked, entity refs dropped from claims and pages; relations deleted for claim/page/entity targets; inline marker stripping; re-derivation both directions; the check_approvable split (cascade allowed, plain referenced delete still blocked); self-approval; propose-writes-nothing; audit events and reversible=False; and all three surfaces — mcp, jsonl envelope shapes both ways, and the cli in success and clean-error form.

verification

pytest tests/ -q --ignore=tests/embeddings   green, 0 failures
mypy src                                     Success: no issues found in 122 source files
ruff check src tests                         All checks passed!

one note: this branch also carries fix(capture): apply coerce_numeric to the numeric config fields, the same one-hunk commit as #693. test is currently red without it — #686 added coerce_numeric, routed capture.py's two booleans through coerce_bool, and left min_observations / dedup_window_seconds on bare int()/float(), which fails test_load_config_malformed_numeric_falls_back and trips ruff F401 on the now-unused import. it reached test because the branch-push workflows there run only the bot jobs — pytest/mypy/ruff are pull_request-only, so the gate never ran on the merge that landed it. whichever of #693 or this lands first makes the other a no-op; the commits are identical so they merge cleanly either way.

minion1227 and others added 2 commits July 31, 2026 03:02
vouchdev#686 added coerce_numeric and routed capture.py's two boolean fields
through coerce_bool, but left min_observations and dedup_window_seconds
on bare int()/float(). a typo'd value raised out of load_config instead
of falling back to the default, which is the exact case the helper's own
docstring cites (`min_observations: "three"`), and the resulting unused
import tripped ruff F401.

surfaced by merging test into this branch: the branch-push workflows on
test don't run pytest/mypy/ruff, so the gate never ran on the merge that
landed it.

Co-authored-by: Cursor <cursoragent@cursor.com>
closes vouchdev#600. `referenced_by()` refuses a delete while anything still
points at the target. that block is correct, but in a compiled kb it
leaves most claims permanently undeletable — pages cite claims in bulk —
and a supersede pair is mutually locked at both ends, so no delete
ordering can ever remove either half of a chain.

the gate is unchanged. what changes is what the reviewer is asked to
approve: with cascade=true the required referrer edits are recorded in
the proposal payload as a plan, and _approve_delete re-derives that plan
at approve time — the same posture as the existing ref re-check — applies
it, and only then deletes. the approve-time referenced_by gate still has
to come back empty, so the gate is satisfied rather than bypassed.

pages and claims lose their pointer, frontmatter and the inline
[claim: …] body markers both. relations are deleted outright: an edge
whose endpoint is gone has no meaning, and relations carry no inbound
refs of their own, so the walk is one level deep by construction and
there is no transitive cascade to bound.

additive and default-off — omitting cascade reproduces today's behaviour
exactly, and the refusal message now names the flag so the dead end is
discoverable.

Co-authored-by: Cursor <cursoragent@cursor.com>
@minion1227
minion1227 requested a review from plind-junior as a code owner July 31, 2026 10:12
@github-actions github-actions Bot added docs documentation, specs, examples, and repo guidance cli command line interface mcp mcp, jsonl, and http surfaces storage kb storage, migrations, schemas, and proposals tests tests and fixtures size: L 500-999 changed non-doc lines labels Jul 31, 2026
minion1227 and others added 3 commits July 31, 2026 03:49
approve() re-derives the plan, so the applier's artifact-missing and
already-unlinked paths cannot be reached through the public flow. they
exist for the narrow race where a concurrent writer changes a referrer
between derivation and application, and for a crash-retry of approve().

exercised directly against _apply_cascade, which is the only honest way
to reach them, and what the 100% diff-coverage gate asks for.

Co-authored-by: Cursor <cursoragent@cursor.com>
@plind-junior
plind-junior merged commit 81af4e9 into vouchdev:test Jul 31, 2026
11 checks passed
@github-actions github-actions Bot added the ci: passing ci is green label Jul 31, 2026
@github-actions

Copy link
Copy Markdown
Contributor

diff coverage: n/a — this PR changes no python under src/vouch/, so there is nothing for the gate to measure.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci: passing ci is green cli command line interface docs documentation, specs, examples, and repo guidance mcp mcp, jsonl, and http surfaces size: L 500-999 changed non-doc lines storage kb storage, migrations, schemas, and proposals tests tests and fixtures

Projects

None yet

Development

Successfully merging this pull request may close these issues.

cascade option for propose_delete: page-cited claims are undeletable

2 participants