Mint LLP numbers across every ref, and fail the branch that takes a claimed one - #916
Conversation
…laimed one (#907) Three branches cut from the same master each minted `llp/0266-*`. Each read the highest number in the tree it had checked out, each got 0265, and each picked 0266. The slugs differed, so git reported no conflict: whichever merged second would simply have added a second document numbered 0266, and every `@ref LLP 0266` would have become ambiguous. LLP 0156 already settles the rule ("above the highest number claimed anywhere, including branches without an open PR"), but nothing executed it: the minting rule lived only as prose in `/llp-create`, which said `max(existing) + 1` over the working tree. `scripts/llp-numbers.js` is that rule as code, in three modes: - `next` prints the next free number across every ref that could still merge. - `check` exits nonzero when a number this branch mints is claimed elsewhere. - `survey` lists every collision across every ref. `check` reports only the numbers the branch adds relative to its merge base. A survey of the real repository found 23 collided numbers, and most are the corpus's already-repaired ones surviving on branches cut before the repair; a gate that reported those would redden pull requests party to none of them. Wired into `.github/workflows/llp-check.yml` (the existing in-tree job only sees a collision after the second merge) and into `npm test`. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…916) A shallow or single-branch checkout carries one ref and no default branch, which is what `actions/checkout` hands the `test` and `typecheck` jobs in `.github/workflows/ci.yml`. In that clone every mode answered from a corpus of one: `next` printed 0267 on a tree where 0266 is claimed eight times and the real answer is 0281, and the in-suite gate in `test/core/llp-number-minting.test.js` passed green without comparing anything, because `mintedNumbers` found no base and returned an empty set. That is the defect of issue #907 wearing this script's own clothes. - `partialScan` names the three conditions that make an answer a guess: not a git checkout, a shallow clone, no default-branch ref. `run` refuses outright (exit 2) in the first case, which is the fallback the new `/llp-create` step 1 already told the agent to take, and warns on stderr in the others while still printing the number. - The in-suite test skips with the reason instead of passing vacuously, so a CI run that cannot carry the check says so. - `mergeableRefs` used the throwing `git` helper, so outside a checkout every mode died with a raw Node stack trace and a leaked `fatal:` line. It uses `tryGit` like the rest of the file; `git` had no other caller and is gone. - `addedDocs`'s JSDoc claimed basenames stop the LLP 0156 renumber repair from reading as a fresh mint. A renumber changes the basename and does read as a mint, correctly, since the number it moves to has to be free too. What basenames absorb is a move between directories, which is what its own test asserts. - `AGENTS.md` said `check` runs "in `npm test` and in CI"; in CI the `npm test` half cannot see the other branches, and the gate is the `cross-branch-numbers` job that fetches them. - `/llp-create` step 1 said to run the script unconditionally, then said to include non-standard LLP trees "in the scan" the script cannot extend. It now names all three fallbacks: no script, no checkout, LLP tree elsewhere. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…view of #916) Four defects the ref-only scan carried, each reproduced against this repo before and after. **A document you have not committed claims its number.** The scan read `git ls-tree` over refs only, so `next` printed 0281 before and after `llp/0281-*.decision.md` was created on disk. Two `/llp-create` calls in one session therefore minted the same number, which is issue #907 with both claimants on one machine, and the prose rule this replaced (`scan all files in llp/`) did catch it. `worktreeFiles` adds the tracked and untracked files under `llp/`, `scanRefFiles` is the union every mode now reads, and the tip `mintedNumbers` diffs is the working tree, so `npm test` reddens on the document you just wrote rather than only after you commit it. **A slug rewrite is not a mint.** `addedDocs` matched basenames, so renaming `0265-grep-search-implementation.plan.md` to `0265-renamed-slug.plan.md` read as minting 0265, a number the branch already owned, and `check` exited 1 telling it to renumber a collision it was not party to. `mintedAgainst` counts distinct documents per number instead: a number is minted when more documents claim it at the tip than at the merge base. A slug rewrite (1 to 1) is not, a second document at an existing number (1 to 2) is, and the LLP 0156 renumber repair mints the number it moves to and is checked there. **A merge base that cannot be found is unknown, not empty.** With a base ref that resolves but no common ancestor, `mintedNumbers` passed `[]` as the base tree and counted the whole corpus at HEAD as newly minted, so `check` blamed the branch for every collision already settled in it. It now reports a null `mergeBase` and `check` says so and exits 0. **A claimant line is capped.** One document is carried by every branch cut since it landed, which here is 105 refs on one line and 64KB from `survey`. `describeRefs` names six and counts the rest. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Neutral review of #916 at
|
…rseded (review of #916) Four findings from the review of ec85533, all reproduced against fixtures first. Medium: `check` counted a base-branch document sitting in the working tree as newly minted, because the floor was the merge-base tree while the tip was the working tree. An interrupted `git merge origin/master` (or a `git checkout origin/master -- llp/`) put master's own document on disk, and the branch was told to renumber a document it never wrote. `mintedNumbers` now adds the base branch's documents that are actually present here to the floor. A rival that landed on master first is still caught: it claims the number under a name this branch does not carry, so the branch really did mint it. Medium: renaming the slug of a number this branch minted and pushed failed the gate against its own pre-rename filename, still sitting on `refs/remotes/origin/<branch>`, and prescribed a renumber for a collision with itself. `check` now drops refs reachable from HEAD: they are prior versions of this branch, not rivals. `next` and `survey` still read every ref, so a number is never handed out because a stale ref was filtered. Low: `check` exited 0 on a checkout that could not see the corpus. A job that lost its `fetch-depth: 0` would go green having compared one ref with itself, which is issue #907 one layer up. It now refuses with exit 2. Low: the partial-scan warning named `git fetch --prune --unshallow` for both cases, and that aborts with `--unshallow on a complete repository does not make sense` on the single-branch clone. Each case now names the fetch that works, in the script and in `/llp-create` step 1. Regression tests: four added, three of which fail against the pre-fix script. Full suite 4503 pass, 0 fail, 1 skipped. `npm run typecheck` clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Neutral review of #916 at
|
|
Triage at |
What was wrong
Three open branches each minted
llp/0266-*with a different slug. Git reportsno conflict between differently named files, so whichever merged second would
have added a second document numbered 0266 and every
@ref LLP 0266would havebecome ambiguous. The in-tree gates (
.github/workflows/llp-check.ymland theno LLP number is claimed by two documentstest) only see the collision onceboth documents are in one tree, which is after the damage.
Root cause
The minting rule existed only as prose, and the prose named the wrong
denominator.
/llp-createstep 1 saidmax(existing) + 1overllp/in theworking tree, so every worker on a branch cut from the same master computed the
same answer. LLP 0156 had already settled the correct rule ("above the highest
number claimed anywhere, including branches without an open PR"), but
nothing executed it.
The fix
scripts/llp-numbers.jsmakes LLP 0156's rule executable, in three modes:nextprints the next free number across every ref that could still merge(local heads, remote tracking branches,
HEAD), tombstones included, becausea retired number is never reused.
checkexits nonzero when a number this branch mints is already claimed by adifferent document somewhere else.
surveylists every collision across every ref.checkdeliberately reports only the numbers the branch adds relative to itsmerge base with the default branch. That scoping is load-bearing, not caution: a
surveyof this repository right now finds 23 collided numbers, and most ofthem (0026, 0032, 0058, 0059, 0086, 0098, 0099, 0111, 0142, 0143, 0144, 0188,
0193, 0200, 0212, 0223, 0224) are the corpus's own already-repaired collisions,
alive on branches cut before the repair. A gate that reported those would redden
pull requests party to none of them and would stop being read.
Wired in two places:
.github/workflows/llp-check.ymlgains across-branch-numbersjob thatfetches every branch and runs
check. The existing in-tree job stays.test/core/llp-number-minting.test.jsruns the same check against thisrepository, so
npm testcarries the in-suite half.Also updated:
/llp-createstep 1 and/ref-checkstep 3 (a duplicate number isBROKEN, not aWARNING, and exits 1), and the LLP tooling bullet inAGENTS.md(CLAUDE.mdis a symlink to it).Regression test and its before/after
test/core/llp-number-minting.test.jsrebuilds the issue's history in a temp gitrepo: a master at LLP 0265 with the three branches hanging off it, each minting
llp/0266-*under its own slug, mirrored underrefs/remotes/origin/. Itasserts that master alone answers 0266 (the wrong answer all three workers got)
while every-ref answers 0267, that the collision is reported with all three
claimants, that
checkfails on a minting branch and points at 0267 or above,and that a settled collision surviving on a stale branch does not fail an
unrelated branch.
Before (the pre-fix denominator,
mergeableRefsreturning just thechecked-out tree):
5of11fail, including the reproductionthe next free number is the highest claimed on any ref, not on the checked-out one.After:
11pass,0fail. Full suitenpm test: 4494 pass, 0 fail, 1skipped.
npm run typecheckclean.What this PR does not do
It does not renumber the three colliding documents. They live on three other
branches, so a master-based PR cannot touch them, and the issue is explicit that
which document keeps 0266 depends on intended merge order. Ground truth as of
this branch, from
survey, is worse than the issue recorded: six documentsnow claim 0266, and 0248, 0249, 0265, 0267 and 0268 collide too. The next free
number is 0280.
Once this lands, those branches go red on
check(and onnpm test) until theyrenumber, which is the point: the failure now arrives before the merge instead of
after it. That is the residual risk worth naming explicitly.
Fixes #907