feat(invalidation): propagate a page invalidation to every page that cites it - #165
Merged
Conversation
tangletools
approved these changes
Aug 21, 2026
tangletools
left a comment
Contributor
There was a problem hiding this comment.
✅ Auto-approved drewstone PR — c96a903f
This PR was opened by the trusted drewstone account.
This approval is provisional and was applied by the local stand-in because the pr-reviewer webhook host is unreachable (2026-08-21). CI on this head is fully green. The full PR reviewer audit re-runs via the resweep when the service returns and will publish findings if it detects issues.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
discovery-lab measured, on 2026-08-19: 10 invalidated claims still live with no mark, 22 citation edges pointing into them, and 3 citing pages created the day AFTER the overturn (#157, capability 3).
The verdict lives on the refuted page. A reader who arrives through a citation never meets it. Nothing in the store carried the overturn one hop outward, so an agent could cite a dead claim in good faith a day after it died.
What
planInvalidationPropagation(visiblePages)is pure and plans a diff, not a rewrite:citesover the whole chain with the existingresolveKnowledgeCitation, and collect the ids of the resolved pages carrying aninvalidation;citesInvalidatedthe page already holds. A store already stamped plans nothing, so the pass can run after every grading round.herepages are stamped: a run does not write the stores it inherits or shares.formatKnowledgeInvalidationProposal(plan)renders the plan as write blocks for the existingapplyKnowledgeWriteBlocks, through the existingformatFrontmatter. Only the stamped field changes; there is no body banner, because rendering belongs to the consumer.Two read-side surfaces close the loop: the
cites-invalidatedlint warning names every live citation into a refuted page, andsearchKnowledge(..., { excludeInvalidated: true })drops refuted pages from a result set.excludeInvalidateddefaults tofalse— a caller reading history needs those pages, and silently changing what search returns is worse than an explicit option.No graph walk.
citesInvalidatednames pages this page cites, which is one hop by definition, sowalk/neighborswould add a traversal where a direct resolution is the whole rule.Simplification
Simplification:
pages.map(page => ({ page, origin: 'here' }))had grown a second copy with the intake gate; it is noworiginatedPages(pages, origin?)inrun-scoped.ts, the module that ownsOriginatedPage, andproposals.tsplus the intake tests route through it. Citation resolution, the intake gate, and this pass now take one page shape whether or not the caller runs run-scoped stores.Net: +324 / -22 lines, 14 files, 2 hand-rolled origin lifts collapsed to 1 owner, 0 new resolvers or graph walks.
Not done here: the pass renders frontmatter through
formatFrontmatter, so a page whose on-disk frontmatter uses YAML that the simple parser does not model is rewritten in the parser's normalized form. Widening that writer is its own PR againstfrontmatter.ts, not a change smuggled into a propagation pass. No CLI subcommand:agent-knowledge lintalready reports the finding, and the pass has an API consumer, not an operator one.Tests: +3 (a citer of an invalidated page gets stamped and a revalidated target clears the stamp; an inherited page is never stamped, which is the "runs cannot write shared scope" invariant; two passes over one store through the real write path leave byte-identical files, which is the property the whole diff design exists for), -0 deleted. No shape assertions.
Proof
Every local failure is the same macOS
/var->/private/varsymlink undertmpdir()raisingknowledge transaction directory escaped its root, identical on cleanorigin/mainon this host. Linux CI has no such symlink. The new tests realpath their temp root, so they are green locally too.Refs #157