feat(promotion): promote run-scoped pages with their cited support - #167
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 — c47f1f58
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: 64 of 867 citation edges dangled, and 23 of the 46 missing targets existed in a run-local store and were never promoted (#157, capability 2b; #151, the whole Promotion section).
Half the referential rot was not an authoring failure at all. The authors cited pages that existed and resolved at the time. The promotion step orphaned them: it moved a claim into shared scope and left the run-local pages the claim cites behind. Until promotion carries a claim's support, every promotion is a chance to manufacture a dangling edge.
There was also no promotion API at all:
src/run-scoped.tstreated the shared root as read-only by convention, and nothing recorded what had been promoted, by whom, or why.What
promoteRunScopedPages(stores, runId, { pageIds, sharedRoot, actor, reason })is the one path from run scope into shared scope.citesis carried along, resolved with the existingresolveKnowledgeCitationover the run's chain. A citation already pointing into shared needs no carrying.assertKnowledgeCitationsResolved. A citation qualifiedhere::orinherited:is refused too: after the move,herenames a future reader's own store, so carrying that qualifier verbatim would point the citation at the wrong page rather than at none.knowledgePageDigestis the same in both scopes and the record describes what a reader will load. That is whyRunScopedStoresgainsstorePath(runId); a chain read hides the root.<shared>/.agent-knowledge/promotions/<digest>.json: source run, every page digest, which pages were requested and which were carried support, actor, reason, time. It is content-addressed viacanonicalCandidateDigest, so re-running one promotion writes the same bytes at the same path.Everything is composed: the existing resolver, the existing
commitKnowledgeFileMutationsunder the existing lock, the existingknowledgePageDigest, the existing durable JSON writer. No second resolver, no second transaction, no new digest scheme.Simplification
Simplification:
sharedRootwas a read-only convention with no enforcement and no record. It is now a convention plus one API that owns every write into it and leaves an auditable record, which is what makes "runs cannot write shared scope" checkable instead of merely stated.Net: +491 / -1 lines, 9 files, 0 new resolvers, transactions, or digest schemes; 1 read-only-by-comment rule replaced by 1 enforced path.
Not done here: no rollback API. The record is content-addressed and the promotion is re-runnable, so reversing one is a shared-scope write like any other, and an "unpromote" that must also decide what happens to pages promoted since is its own design. Not a CLI command either: promotion needs an actor and a reason from the system that decided, not from a shell.
Tests: +4 (a promoted claim carries the run-local measurement it cites, at that measurement's own rung, with the promoted file byte-identical to the source — the exact defect that produced 23 of the 46 missing targets; a citation that would dangle in shared refuses the promotion and leaves the shared store empty, which is the fail-closed gate over shared data; a page the run did not author is refused; the record round-trips from disk and a second identical promotion returns the same record, which is what makes a crash between the page write and the record write repairable), -0 deleted. No shape assertions.
Proof
128 local failures, the same number and the same files as the two merged PRs before this one and as clean
origin/mainon this macOS host. Every one raisesknowledge transaction directory escaped its rootfrom the/var->/private/varsymlink undertmpdir(). Linux CI has no such symlink; the new tests realpath their roots and are green locally.Refs #157, refs #151