Skip to content

fix: block duplicate intent ids (itd-82 collision) + capture iss-80 - #48

Merged
REPPL merged 2 commits into
mainfrom
fix/duplicate-intent-id-lint
Jul 12, 2026
Merged

fix: block duplicate intent ids (itd-82 collision) + capture iss-80#48
REPPL merged 2 commits into
mainfrom
fix/duplicate-intent-id-lint

Conversation

@REPPL

@REPPL REPPL commented Jul 12, 2026

Copy link
Copy Markdown
Owner

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-82 on mainand every gate stayed
green
, because record-lint never checked id uniqueness at all.

The collision was predictable. The silence was not, and it is the more important
finding.

Detector first

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 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-82 files and record-lint exits 2:

.abcd/development/intents/drafts/itd-82-drain-ledger-triage.md:2: [BLOCKER intent_lifecycle]
  duplicate intent id itd-82; also claimed by .../itd-82-review-bar-fires-itself.md
  — an id is the intent's identity across the record and must be unique
.abcd/development/intents/drafts/itd-82-review-bar-fires-itself.md:2: [BLOCKER intent_lifecycle]
  duplicate intent id itd-82; also claimed by .../itd-82-drain-ledger-triage.md
  — an id is the intent's identity across the record and must be unique

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) 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.

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 + 1
allocator 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-N ledger once (recorded as iss-74). It applies equally to itd-N,
spc-N, and iss-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 main again
. That is a backstop, not a
solution.

Gates

make preflight exits 0 (build, gofmt, vet, tests, race). make record-lint exits 0
after the renumber, and exits 2 before it — the detector was verified in both
directions.

Assisted-by: Claude:claude-opus-4-8

REPPL added 2 commits July 12, 2026 22:01
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
@REPPL
REPPL merged commit 9b441cb into main Jul 12, 2026
12 checks passed
@REPPL
REPPL deleted the fix/duplicate-intent-id-lint branch July 12, 2026 21:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant