fix: block duplicate intent ids (itd-82 collision) + capture iss-80 - #48
Merged
Conversation
Two agents worked in parallel on separate branches. Each allocated "the next free" intent id by scanning existing files for max + 1. Both got itd-82. Both merged. Two intents now claim itd-82 on main, and every gate stayed green -- record-lint never checked id uniqueness at all. The id is the intent's identity across the whole record: cross-references are written as "itd-N", so a duplicate makes every reference to it ambiguous. That is a blocker, not a warning. checkIntentLifecycle already walked every intent file and collected ids -- into a map[string]bool, which silently deduped exactly the condition we needed to detect. It now tracks the files each id claims and flags EVERY file in a colliding set, not just one: the linter cannot know which claimant is authoritative, and flagging a single file would imply the others are fine. Detector-first: the test was watched fail on a real collision shape (same id in one bucket, and the same id across two buckets) before the rule existed, then watched pass. Run against the live record it fires on both itd-82 files. This is the DETECTION half only. The minting scheme is the actual defect -- a branch-local allocator cannot see ids minted on unmerged branches, and the same class already bit the iss-N ledger (iss-74). Tracked as iss-80. Assisted-by: Claude:claude-opus-4-8
The armed detector fires on both itd-82 files. This is the fix behind it. The later claimant yields: itd-82-review-bar-fires-itself (PR #47, merged second) becomes itd-83; itd-82-drain-ledger-triage (PR #46, merged first) keeps itd-82. This follows the precedent set when the ledger allocator collided -- iss-77 was renumbered to iss-79, the newer allocation giving way. No document referenced the id yet, so nothing else moves. Renumbering contradicts the record's own "ids are capture-stable, never renumbered" invariant. That contradiction is the point: the invariant is only holdable if ids cannot collide in the first place, and a branch-local max + 1 allocator guarantees they can. Fixing the collision protects the invariant going forward; it does not excuse the allocator. iss-80 captures the minting half -- a collision-free scheme (forge-minted, random-suffix, timestamp, or a reserve-registry) is under research. Until it lands, the detector is what stops a silent duplicate reaching main. Assisted-by: Claude:claude-opus-4-8
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.
Two agents worked in parallel on separate branches. Each allocated "the next free"
intent id by scanning existing files for
max + 1. Both got itd-82. Both merged(#46, #47). Two intents now claim
itd-82onmain— and every gate stayedgreen, because
record-lintnever checked id uniqueness at all.The collision was predictable. The silence was not, and it is the more important
finding.
Detector first
checkIntentLifecyclealready walked every intent file and collected ids — into amap[string]bool, which silently deduped exactly the condition we needed todetect. It now tracks which files claim each id.
The rule flags every file in a colliding set, not just one. The linter cannot
know which claimant is authoritative, and flagging a single file would imply the
others are fine.
Watched fail before the rule existed, on two real collision shapes (same id twice in
one bucket; same id across two buckets), then watched pass. Run against the live
record, it fires on both
itd-82files andrecord-lintexits 2:The fix behind it
The later claimant yields:
itd-82-review-bar-fires-itself(#47, merged second)becomes itd-83;
itd-82-drain-ledger-triage(#46, merged first) keepsitd-82.This follows the precedent set when the ledger allocator collided —
iss-77wasrenumbered to
iss-79, the newer allocation giving way. No document referenced theid yet, so nothing else moves.
This renumber contradicts the record's own "ids are capture-stable, never
renumbered" invariant, and that contradiction is the point. The invariant is only
holdable if ids cannot collide in the first place, and a branch-local
max + 1allocator guarantees they can. Fixing the collision protects the invariant going
forward; it does not excuse the allocator.
What this does NOT fix
The minting scheme is the actual defect — captured as iss-80. A branch-local
allocator cannot see ids minted on unmerged branches, and the same class already bit
the
iss-Nledger once (recorded asiss-74). It applies equally toitd-N,spc-N, andiss-N.A collision-free scheme is under research (forge-minted, random-suffix, timestamp, or
a reserve-registry — each trades differently against the human-readable, offline,
record-is-truth constraints). Until one lands, this detector is the only thing
stopping a silent duplicate from reaching
mainagain. That is a backstop, not asolution.
Gates
make preflightexits 0 (build, gofmt, vet, tests, race).make record-lintexits 0after the renumber, and exits 2 before it — the detector was verified in both
directions.
Assisted-by: Claude:claude-opus-4-8