Fix/commit seam id uniqueness gate - #43
Open
jesseh wants to merge 2 commits into
Open
Conversation
added 2 commits
August 29, 2026 22:32
validate::structural_violations() returns the silent-misbind subset (duplicate node/link/group ids, duplicate property labels, per-host and cross-host responsibility-id collisions). write_model_at -- the committed-layer seam every commit/fold/set_model rides through -- now rejects any model carrying one, naming each violation. A responsibility left on two hosts by a botched move is invisible to the plan diff: its id-keyed index keeps only one copy per id, so from and to collapse to a single entry, compare equal, emit no change, and the stale wrong copy sits committed indefinitely with nothing able to surface it. Fail loud at the seam instead of letting it sit.
Surface structural invariant violations (which write_model_at now refuses) in their own BLOCKING section, apart from advisory warnings and de-duped against them, so an agent editing the plan sees a duplicate id before a commit is refused at the seam. The plan write path intentionally does not gate, so the draft can hold the duplicate; validate_model is where it becomes visible.
Contributor
Author
|
Thanks for this innovative tool. I think it has the potential to become central to creating a sustainable AI workflow on a complex codebase. |
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.
Problem
Responsibilities (and nodes, links, groups) are identified by a globally-unique id, and every consumer bakes that assumption into its data structures:
diff::index_responsibilities(and the TS twinindexResponsibilities) build an id-keyed map with last-write-wins — a second copy of an id under a different host is silently dropped before the diff runs.commit.rs:find_responsibilityreturns the first match and stops.So when a responsibility is moved from host A to host B but a stale copy is left on A (a botched move, a
set_modeloverwrite, a hand-edit), the committed model holds the same id twice. The plan diff collapses each side to a single copy,fromandtocompare equal, no change is emitted, nobody is ever prompted to reconcile it, and the self-healingretainin the commit fold never fires. The wrong copy sits committed and invisible indefinitely.validate()already detects this (the "globally unique" check), but it is advisory — nothing gates on it, so the state is reported into a warnings list no writer consults.The underlying category: consumers assume an invariant that only an advisory check enforces, and they encode the assumption as silent deduplication — so a violation is not merely unhandled, it is actively hidden.
Fix
Promote the invariant from an advisory read to a hard gate at the one seam that persists committed state.
scryer-core/src/validate.rs— newpub fn structural_violations(model) -> Vec<String>: the silent-misbind subset only (duplicate node/link/group ids, duplicate property labels, per-host responsibility repeats, cross-host responsibility-id collisions). It deliberately excludes advisory findings (length caps, disconnected nodes, link legality) that are legitimate transient states, so it can guard every write without false positives.scryer-core/src/storage.rs—write_model_at(the single committed-layer writer every commit / fold /set_modelrides through) now runsstructural_violationson the exact bytes about to be written and returnsErr, naming each violation, if any exist. A silently-misbindable duplicate can no longer reachmodel.scry.scryer-mcp/src/tools/read.rs—validate_modelnow splits its output into a BLOCKING section (structural violations — "a commit will be REFUSED until resolved") and advisory warnings, de-duped against each other. The plan write path intentionally does not gate, so a draft can transiently hold a duplicate; this is where the agent sees it before hitting the commit refusal.Tests
structural_violations: flags a cross-host collision + duplicate node id; empty on a clean model.write_model_at: rejects a cross-host duplicate (nothing persisted); accepts a clean model.validate_model: a draft duplicate surfaces in the BLOCKING section.All existing tests pass; full
scryer-core(171) andscryer-mcp(121) suites green on top ofmain.Deliberately out of scope
No new
Changevariant to surface duplicates on the live canvas diff (diff.rs/planDiff.ts) — it ripples across TS, Rust, and the UI renderer for a case the seam gate now prevents at commit, andvalidate_modelalready surfaces a draft duplicate. Reasonable follow-up if earlier canvas-time warning is wanted.