feat(decision-records): warn on, and mint past, IDs taken on origin/main - #11
Merged
Merged
Conversation
Uniqueness is checked against a single tree, so two branches can each mint 0044 — or the same draft mnemonic — and both stay green until they meet. The collision comes into being on the rebase, by which point the record is written and cross-referenced. `check` now also reads the origin/main already on disk and prints a WARN line when an ID there names a different file, with the next free counter. Same ID and same filename is that record, edited or moved — accepted/ to archived/ keeps the name and stays silent. It never fetches, never fails the run, and says nothing when the ref is absent: a fresh clone or an offline machine is not a finding. lanh-ai runs the same shape over its PRD and migration counters from a shared scripts/upstream.py; a skill has to stand alone, so this is a deliberate second copy.
`check` only warns about a counter taken upstream, because origin/main moves under a branch and a gate there would fail correct work. Minting is the other side of it: a write. By the time anything reads the number, the record has been renamed, its H1 rewritten and every inbound link repathed — so `next_counter` now asks the ref rather than leaving `check` to report the collision afterwards. Stepping over a counter origin/main holds leaves what reads as a hole, so the gap rule is told to read those as held rather than missing: the record exists, it is just not on this branch yet, and the rebase closes the sequence. A number neither tree has still fails. `promote` prints which counters it stepped over, or the jump is a mystery — the tree ends at 0048 and the record comes back 0050. With no ref on disk both fall back to this tree alone, which is what a fresh clone and an offline machine have always done.
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.
Every uniqueness check in this tool reads one tree, so a duplicate ID can only fail once both copies are in it — after a rebase or a merge. On a branch the ID you minted is unique and everything is green; the collision comes into being when the trees meet, by which point the record is written, renamed, and cross-referenced. lanh-ai hit exactly this with
PRD-005today, and its migration sequence has hit it before with0052.Two commits, one for each side of the problem.
checkwarns about an IDorigin/mainalready gives to a different file (6e6a812). Same ID and same filename is that record, edited — not a collision — and a record that movedaccepted/toarchived/keeps its filename and stays silent. Counters get the next free number across both trees appended as the fix; draft mnemonics get "re-mint it". Filenames only, so a record whose front-matter is still being written reads like any other.It warns and never gates:
origin/mainmoves under a branch on every fetch, so a failure here would reject correct work for a reason its author cannot act on. It never fetches — a hook that reaches the network is a hook people turn off — and when the ref is not on disk (a fresh clone, an offline machine, a CI checkout that took only the branch) it reports nothing, because a missing ref is not a finding. Read-onlygit ls-tree/rev-parseagainst the object store; nothing is checked out and no ref moves, so it is safe from any worktree.promotemints past whatorigin/mainholds (12cea62). Minting is the other side of it: a write. By the time anything reads the number, the record has been renamed, its H1 rewritten and every inbound link repathed, sonext_counterasks the ref rather than leavingcheckto report the collision afterwards. It prints which counters it stepped over, or the jump is a mystery — the tree ends at0048and the record comes back0050.That skip leaves what reads as a hole, so the gap rule is told to read a counter
origin/mainholds as held rather than missing: the record exists, it is just not on this branch yet, and the rebase closes the sequence. A number neither tree has still fails. This is the one place a gate consults the ref, and it only ever suppresses an error, never raises one — a fetch can makecheckgreener, never redder.With no ref on disk, both fall back to this tree alone, which is exactly what they did before.
Verification
Watched failing on a known collision before being kept, against lanh-ai's real
docs/decisions/: renaming0044-template-authority.mdfired the warning naming0049; restoring it went silent. The minting half was probed read-only against the same tree with0048removed to simulate a branch that forked before it landed — the local tree alone answers0048(the collision), withorigin/mainit answers0049.116 tests pass, 8 new. Every guard is mutation-checked: reverting the mint to local-only fails 4 tests (and its captured stderr shows it minting the very collision the warning then reports); dropping the gap exemption fails 3; making the gap rule always-permissive fails the "a counter neither tree has is still a gap" test. The pre-existing
test_gap_in_countersruns outside git and still passes, which pins the no-ref fallback. Tests build real git work trees — nothing about git is stubbed, because the behaviour under test is reading refs off disk.Note for consumers
lanh-ai runs the same shape over its PRD and migration counters from a shared
scripts/upstream.py(skymavis/lanh-ai#feat/upstream-collision-warning). This copy is deliberate — a skill has to run in a repo that has none of that — and both sides say so. Keep them in step.A consumer does not get this until its
skills-lock.jsonis bumped and the skill re-materialized; lanh-ai'sdecisions-checkhook runs the materialized copy.