Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .claude/skills/ref-check/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ Build a map from `(LLP number) → (file path, title, {anchor: heading text})`.

Also record where the same number is claimed by **more than one** file. A duplicate number makes `@ref LLP NNNN#anchor` ambiguous, and a checker that keeps only the last file it walked will report every reference aimed at the other one as broken. Resolve an anchor against **any** claimant, so that the references keep resolving, and report the duplicate itself as `BROKEN` on the corpus: one number resolving to two documents is a defect in its own right, not a hint, and it is repaired by renumbering the later claimant (LLP 0156).

**The tree you are scanning is one branch of the corpus.** Two branches can each mint the same number cleanly, and nothing in a single tree can see it: the collision appears only when the second one merges (issue #907). `node scripts/llp-numbers.js survey` reports duplicates across every ref, `check` fails when the branch you are on mints a number claimed elsewhere, and `next` gives the number a new document should take. Run `git fetch --prune` first, or the scan only sees the refs you already had.
**The tree you are scanning is one branch of the corpus.** Two branches can each mint the same number cleanly, and nothing in a single tree can see it: the collision appears only when the second one merges (issue #907). `node scripts/llp-numbers.js survey` reports duplicates across every ref, `check` fails with exit 1 when the branch you are on mints a number claimed elsewhere, and refuses with exit 2 when the checkout cannot work out what the branch minted at all (no default-branch ref, no common ancestor with it, a shallow clone), which is a scan that could not run rather than a collision to renumber. `next` gives the number a new document should take. Run `git fetch --prune` first, or the scan only sees the refs you already had.

### 4. Validate each reference

Expand Down
28 changes: 19 additions & 9 deletions scripts/llp-numbers.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@
// conflict because their slugs differed (issue #907).
//
// node scripts/llp-numbers.js next the next free number, four digits
// node scripts/llp-numbers.js check exit 1 if this branch mints a taken number
// node scripts/llp-numbers.js check exit 1 if this branch mints a taken number,
// exit 2 if the scan cannot answer at all
// node scripts/llp-numbers.js survey every collision across every ref
//
// @ref LLP 0156#renumber [implements]: a fresh number sits above the highest claimed anywhere, including branches without an open PR
Expand Down Expand Up @@ -308,7 +309,9 @@ export function scanRefFiles(repoRoot) {
* A merge base that cannot be found (a truncated clone, an unrelated history)
* means the answer is unknown, reported as a null `mergeBase`. Treating it as an
* empty base instead would count the whole corpus at HEAD as newly minted and
* blame this branch for every collision already settled in it.
* blame this branch for every collision already settled in it. An empty number
* set carries no such distinction, so callers that gate on the result have to
* read `mergeBase` too: `run` refuses rather than passing on a null one.
*
* @param {string} repoRoot
* @returns {{ base: string | null, mergeBase: string | null, numbers: Set<number> }}
Expand Down Expand Up @@ -444,13 +447,20 @@ export function run(argv, repoRoot, write, writeError) {
return 0
}
const minted = mintedNumbers(repoRoot)
if (minted.base === null) {
writeError('no default branch to compare against, so nothing is minted here\n')
return 0
}
if (minted.mergeBase === null) {
writeError(`no common ancestor with ${minted.base}, so what this branch mints cannot be told from what it inherited\n`)
return 0
// No scope is not an empty scope. When the branch cannot be told apart from
// what it inherited, the gate checked nothing, and reporting that as a pass is
// the silent-pass failure the partial-scan refusal above already rules out. So
// it refuses on the same terms: exit 2 says "cannot answer", distinct from the
// exit 1 that says "answered, and a number collides". The base-less case is
// unreachable from here (`partialScan` refuses `NO_BASE_REF` first) and is kept
// refusing anyway, so that the two predicates drifting apart cannot reopen the
// hole.
if (minted.base === null || minted.mergeBase === null) {
const unknown = minted.base === null
? 'there is no default branch to compare against'
: `there is no common ancestor with ${minted.base}`
writeError(`${unknown}, so what this branch mints cannot be told from what it inherited. The check would pass without looking, so it refuses instead. Fetch the default branch's history, or run it where the shared history is.\n`)
return 2
}
const superseded = supersededRefs(repoRoot)
const rivals = new Map([...refFiles].filter(([ref]) => !superseded.has(ref)))
Expand Down
25 changes: 23 additions & 2 deletions test/core/llp-number-minting.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -301,10 +301,31 @@ test('a merge base that cannot be found is unknown, not an empty base', t => {
assert.equal(minted.base, 'refs/remotes/origin/master')
assert.equal(minted.mergeBase, null)
assert.deepEqual([...minted.numbers], [])
})

// The other half of the same repo: `mintedNumbers` reports the unknown honestly,
// and `check` has to act on it. An unrelated history leaves the gate with no
// scope at all, and a scope of nothing checked nothing, so exiting 0 there is a
// pass the gate never earned. That is the failure mode the partial-scan refusal
// above exists to prevent, so the answer here is the same one: refuse.
test('the check refuses a branch with no common ancestor instead of passing it', t => {
const repo = emptyRepo(t)
writeDoc(repo, 'llp/0100-a.spec.md')
commit(repo, 'the corpus')
git(repo, ['update-ref', 'refs/remotes/origin/master', 'master'])
git(repo, ['checkout', '-q', '--orphan', 'unrelated'])
writeDoc(repo, 'llp/0100-b.spec.md')
commit(repo, 'a history with no common ancestor, minting a number it cannot verify')

// The scan itself is fine: 0100 really is claimed twice across the refs.
assert.deepEqual(collisions(refFilesFromGit(repo, mergeableRefs(repo))).map(c => c.number), [100])

/** @type {string[]} */
const err = []
assert.equal(run(['check'], repo, () => {}, text => err.push(text)), 0)
assert.match(err.join(''), /no common ancestor/)
assert.equal(run(['check'], repo, () => {}, text => err.push(text)), 2)
const report = err.join('')
assert.match(report, /no common ancestor with refs\/remotes\/origin\/master/)
assert.match(report, /refuses/)
})

// One document is carried by every branch cut since it landed, which on this
Expand Down
Loading