Where it comes from
referencePatterns and lineReferences are correct and thorough for what they are given. The gap is the corpus, not the matcher: the loop that collects ambiguousRefs never looks beyond the plan directory.
Measured on this repo
IDs matching (SPEC|WORK|BUG|ADR)-\d{3} outside plan/, excluding node_modules and dist:
| File | Refs | Reachable after a renumber? |
|---|
site/content/_data/rune-attributes.json | 718 | generated — regenerable |
site/content/releases.md | 224 | in-repo — fixable, but unscanned |
packages/runes/CHANGELOG.md | 130 | published to npm — permanent |
packages/transform/CHANGELOG.md | 120 | published |
packages/lumina/CHANGELOG.md | 80 | published |
packages/content/CHANGELOG.md | 51 | published |
packages/runes/src/config.ts | 54 | in-repo source comments |
The changelog rows are the ones that cannot be walked back. A released entry reads:
- f1908a3: An entity with no properties is not emitted (WORK-567, SPEC-130 D4)
Renumber SPEC-130 and that line, in a tarball already on the registry, now names a spec about something else. Nothing in the repository records that it went stale.
Steps to Reproduce
- Branch from
main. - Create a spec locally that takes the next free ID — say
SPEC-138. - Meanwhile
main lands its own SPEC-138 (this is the ordinary race; it is why --against exists). - Merge
main into the branch. plan validate reports the duplicate. - Run
refrakt plan migrate ids.
Expected
The entity that is new on this branch is the one renumbered; the ID already reachable on the base ref is left alone. Failing that — with no base ref to consult — the tool says it cannot tell which claimant is published and refuses, rather than picking by filename.
Actual
Scanned 827 plan files in plan/
Would renumber 1 entit(y/ies):
SPEC-138 → SPEC-140 specs/SPEC-138-pluggable-query-engines-for-the-field-value-grammar.md
collides with specs/SPEC-138-collapse-the-rune-transform-boilerplate.md
pluggable-query-engines is the one already merged to main. Applying this would have renamed a published spec and left the unpublished draft holding SPEC-138.
Notes
- Verified by hand on branch
claude/nifty-ptolemy-80epot; the draft was renumbered manually to SPEC-140 instead, and plan validate --against origin/main then reported no collisions across 791 ids. - In that instance nothing referenced either ID yet, so the outcome was recoverable. The severity is that it is not detectably unrecoverable: the tool reports the same confident plan whether or not the ID it is about to move appears in a published changelog.
- The existing test does not pin the current behaviour.
picks which file moves deterministically (plugins/plan/test/migrate-ids.test.ts:113) asserts only that two runs agree, not which file moves. A fix that changes the selection rule leaves it passing. - Related but distinct from the refusal path, which is working as designed: this is about the case where the tool proceeds.
- A narrower fix — refuse whenever more than one claimant exists and no base ref was given — is strictly safer than today and needs no new plumbing, at the cost of the convenience the command was written for.
References
- SPEC-135 — duplicate-ID detection, prevention and resolution; D8 is the "resolve only when provable" rule this narrows
- WORK-582 — the work item that built
migrate ids and validate --against