From a52e7fadae5d3bc332a4c4977b8f8cc818a268cf Mon Sep 17 00:00:00 2001 From: wshallwshall Date: Mon, 3 Aug 2026 21:17:00 -0500 Subject: [PATCH 1/2] =?UTF-8?q?fix(coord):=20unbrick=20the=20allocator=20?= =?UTF-8?q?=E2=80=94=20the=20partition=20guard=20measured=20the=20wrong=20?= =?UTF-8?q?maximum?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Filing BACKLOG #1000, the first legitimate item in the post-partition public sequence, made EVERY backlog allocation in the repository throw: REFUSING TO ALLOCATE. The all-refs backlog maximum (1000) has reached the public floor (1000). I filed #1000 and I caused this. It is not a latent bug that #1000 revealed -- the guard's premise held right up until a public number legitimately reached the floor, and mine was the first. ONE MEASUREMENT WAS SERVING TWO INCOMPATIBLE PURPOSES. Get-Floor returned a single maximum, used for: - the emit start, which wants the max over EVERYTHING so a number is never re-issued. Correct as written. - the residual detector, which wants the maintainer-internal sequence's high-water to see it running out of room below the partition. It read the union instead. Public items are SUPPOSED to be >= 1000 -- that is the whole design -- so the detector fired on the partition being used correctly. Get-Floor now returns both `Floor` and `SubFloorMax`; the emit start keeps the union, the detector reads only the sub-floor band (353 on this tree, so it is quiet). THE REFUSAL IS REMOVED, NOT REWRITTEN TO BE UNREACHABLE. Once an internal item is allocated at or above the boundary it is indistinguishable, in the published files, from a legitimate public item at the same number -- both are `## N.` with N >= floor. A refusal arm would have to fire on correct input or never fire at all, and a branch that cannot fire reads as protection while providing none. Detecting a real breach needs an internal-side input this repository does not have. What remains is an honest warning at 90% of the boundary. THE COMMENT'S LOAD-BEARING CLAIM WAS CHALLENGED AND SURVIVED. It justified the guard with "Get-Floor already swept every ref, internal ones included", and that was suspected of being false. Measured instead of repeated: 490 vault-ish remote-tracking refs exist, 489 carry docs/BACKLOG.md, and 67 item numbers live ONLY there -- including #240-#247, the numbers the Ledger erratum records as re-issued over cited work. The claim is TRUE and the sweep is why the floor is trustworthy. What is false is the inference that seeing internal numbers means being able to detect the breach. The comment now says which is which. SECOND DEFECT, ALSO MINE: -ShowFloor lied. I added it earlier today and it returned 19 lines BEFORE the guard, so it printed `next: 1001` while every real allocation threw. A peer session listed it among six verifications, got a green answer, and recorded it as fact. The residual warning is now computed ONCE, above the -ShowFloor block, and both paths read that one expression -- they cannot drift apart. -ShowFloor also prints the sub-floor maximum, because a bare floor cannot answer the question the partition raises. Negative controls, in the existing source-text seam (tests/test_ledger_check.py) -- executing the allocator to test it would either spend a real number or write to .git/mefor-coord/alloc/**, and a test that mutates the ledger registry to check the ledger registry is its own hazard: - the sub-floor measurement must exist, and Floor must survive for the emit start - `$observed -ge $PublicBacklogFloor` must never reappear (the regression verbatim) - $residualWarning must be computed before the -ShowFloor block and read by both PROVEN TO FAIL: re-planting the exact regression makes the second test red; removing the plant makes it green. 29 tests pass. DISCLOSURE -- I spent a real number proving this. The regression probe allocated BACKLOG #1001, which is now a permanent hole; claims are never released, and the precedent for exactly this is #315/#317, the probe allocations made verifying the previous floor fix. Next backlog number is #1002. Verified after the fix: a real allocation succeeds, the ADR path still works (next 0160), -List works, and -ShowFloor leaves the high-water ratchet untouched. No BACKLOG number is filed for this work: the allocator is the thing that was broken. "A control nobody has watched fail is an assumption wearing a green tick" -- this one had never been watched fail on LEGITIMATE input, which is the harder half. That is BACKLOG #1000's own thesis, and #1000 is the item that broke it. ruff clean; docs/LEDGER-GATE.md documents both maximums and why the refusal is gone. --- docs/LEDGER-GATE.md | 34 ++++++++++++++ scripts/coord/alloc.ps1 | 92 +++++++++++++++++++++++++++----------- tests/test_ledger_check.py | 66 +++++++++++++++++++++++++++ 3 files changed, 166 insertions(+), 26 deletions(-) diff --git a/docs/LEDGER-GATE.md b/docs/LEDGER-GATE.md index 9d402d78..57d71f11 100644 --- a/docs/LEDGER-GATE.md +++ b/docs/LEDGER-GATE.md @@ -42,8 +42,14 @@ reaches `docs/adr/README.md`, so the ADR becomes invisible. Three had already be pwsh -NoProfile -File scripts\coord\alloc.ps1 -Kind adr -Title "Worktree gate" pwsh -NoProfile -File scripts\coord\alloc.ps1 -Kind backlog -Title "Ledger allocator" pwsh -NoProfile -File scripts\coord\alloc.ps1 -List +pwsh -NoProfile -File scripts\coord\alloc.ps1 -Kind backlog -ShowFloor # read-only: allocates nothing ``` +`-ShowFloor` prints the computed floor, **the paths it swept**, the sub-partition maximum and the number +it would issue next — without claiming anything. Use it to answer "what can the floor see" instead of +spending a number on the question: allocation is a one-way door, so before this existed the floor's own +correctness was the one property nobody re-tested. + It claims a number by **exclusively creating** `/mefor-coord/alloc//.json`. That create is atomic on NTFS: if a sibling session got there first it throws, and we move to the next number. It is a **test-and-set**, never a read-modify-write on a shared list — PowerShell was measured @@ -67,6 +73,34 @@ ever computed is stored at `/mefor-coord/alloc//.floor-hig never goes below it; a computed floor beneath the mark prints a loud NOTE rather than quietly handing out a used number. The mark can only rise. +**Two maximums, not one — and conflating them bricked the allocator on 2026-08-03.** The public backlog +sequence is partitioned from the maintainer-internal one at `PUBLIC_BACKLOG_FLOOR` (`#1000`), so the +allocator needs two different numbers: + +| Measurement | Question it answers | Must include public numbers? | +|---|---|---| +| **Floor** — max over everything swept | *What must I not re-issue?* | **Yes** | +| **Sub-floor max** — max below the partition | *How much runway does the internal sequence have?* | **No** | + +The residual detector read `Floor`. So the first legitimate item filed in the public sequence — `BACKLOG +#1000` — made every backlog allocation in the repository throw `REFUSING TO ALLOCATE … has reached the +public floor`. The guard was not detecting a breach; it was detecting the partition being used exactly as +designed, and it fired on correct input. + +**That detector can now only WARN, and the limit is the data, not the implementation.** Once an internal +item is allocated at or above the boundary it is indistinguishable, in the published files, from a +legitimate public item at the same number — both are just `## N.` with N ≥ the floor. A refusal arm would +have to fire on correct input or never fire at all, so it was **removed** rather than made unreachable: a +branch that cannot fire reads as protection and is worse than none. Detecting a real breach needs an +internal-side input this repository does not have. What remains is a warning at 90 % of the boundary, +measured on the sub-floor band, where public numbers cannot distort it. + +*(The sweep does reach internal numbers, which is worth stating because the opposite was suspected: +measured 2026-08-03, 489 of 490 vault-ish remote-tracking refs carry `docs/BACKLOG.md`, and 67 item +numbers live only there — including `#240`–`#247`, the numbers the Ledger erratum records as re-issued +over cited work. Seeing them is what makes the floor trustworthy; it is telling an internal `#1001` from +a public `#1001` that is impossible.)* + Two consequences worth knowing before you tidy refs: - **`git fetch origin --prune` is safe** — it prunes only `refs/remotes/origin/*`, which is not where the diff --git a/scripts/coord/alloc.ps1 b/scripts/coord/alloc.ps1 index 66cbeab2..05b65dbe 100644 --- a/scripts/coord/alloc.ps1 +++ b/scripts/coord/alloc.ps1 @@ -185,8 +185,26 @@ function Get-Floor { } $floor = [Math]::Max($computed, $previous) if ($floor -gt $previous -and -not $Peek) { Set-Content -Path $watermark -Value $floor -Encoding ASCII } - # Measure-Object hands back a [double]; the 'D4' format specifier is integer-only and throws on one. - [int]$floor + + # TWO NUMBERS, NOT ONE -- and conflating them is what bricked this script on 2026-08-03. + # + # `Floor` is the whole observed set's maximum. It answers "what must I not re-issue", so it MUST + # include public numbers. + # + # `SubFloorMax` is the maximum BELOW the partition. It answers a different question -- "how much + # runway does the maintainer-internal sequence have left" -- and it must EXCLUDE public numbers, + # because a public item at or above the boundary is the design working, not a breach. + # + # Returning one number for both is not a style problem. The residual detector below read `Floor`, + # so the first legitimate public item filed at #1000 made the guard throw on every subsequent + # backlog allocation, repo-wide, until it was patched. The guard fired on correct input. + # + # `[int]` on both: Measure-Object hands back a [double], and the 'D4' format specifier is + # integer-only and throws on one. + [pscustomobject]@{ + Floor = [int]$floor + SubFloorMax = [int](($seen | Where-Object { $_ -lt $PublicBacklogFloor } | Measure-Object -Maximum).Maximum) + } } # THE FLOOR IS DEFINED ONCE, IN THE GATE, AND READ HERE. @@ -207,7 +225,16 @@ if (Test-Path $gateFile) { if ($m.Success) { $PublicBacklogFloor = [int]$m.Groups[1].Value } } -$observed = Get-Floor -Peek:$ShowFloor +$measured = Get-Floor -Peek:$ShowFloor +$observed = $measured.Floor +$subFloorMax = $measured.SubFloorMax + +# The residual warning is evaluated ONCE, here, so -ShowFloor and a real allocation cannot disagree. +# They did: -ShowFloor returned 19 lines before the guard, so it printed `next: 1001` while every real +# allocation threw. An inspector that does not run the checks it previews reports a number the tool +# will refuse to issue -- it answers the adjacent question, which is the failure CLAUDE.md §11 names. +$warnAt = if ($null -ne $PublicBacklogFloor) { [int]($PublicBacklogFloor * 0.9) } else { 0 } +$residualWarning = ($Kind -eq "backlog") -and ($null -ne $PublicBacklogFloor) -and ($subFloorMax -ge $warnAt) if ($ShowFloor) { # Name the SOURCES, not just the number. "Which files did this sweep actually read" is the @@ -217,12 +244,17 @@ if ($ShowFloor) { Write-Host "floor : $observed" if ($Kind -eq "backlog") { Write-Host "paths : docs/BACKLOG.md, docs/archive/backlog/BACKLOG-CLOSED.md" + Write-Host "sub-floor: $subFloorMax (highest number below the #$PublicBacklogFloor partition -- the internal sequence's runway)" Write-Host "next : $([Math]::Max($observed, $PublicBacklogFloor - 1) + 1) (clamped to >= $PublicBacklogFloor)" } else { Write-Host "paths : docs/adr/NNNN-*.md (filenames, all refs)" Write-Host "next : $($observed + 1)" } Write-Host "watermark: $(Join-Path $alloc '.floor-highwater')" + if ($residualWarning) { + Write-Host "" + Write-Host "WOULD WARN: sub-floor max $subFloorMax has reached 90% of the #$PublicBacklogFloor partition." -ForegroundColor Yellow + } Write-Host "" Write-Host "Read-only: nothing was allocated." -ForegroundColor DarkGray return @@ -233,32 +265,40 @@ if ($Kind -eq "backlog") { throw "Could not read PUBLIC_BACKLOG_FLOOR from $gateFile. Refusing to allocate a backlog number rather than guess a floor the gate will not honour." } - # THE RESIDUAL DETECTOR, ON APPROACH RATHER THAN ARRIVAL. + # THE RESIDUAL DETECTOR -- IT CAN ONLY WARN, AND THAT IS A LIMIT OF THE INPUT, NOT AN OVERSIGHT. # - # The partition binds only the PUBLIC side; nothing can stop the maintainer-internal sequence - # allocating past the boundary, and CI cannot see it -- a public runner checks out origin only. But - # THIS machine can: Get-Floor already swept every ref, internal ones included. So the one place the - # breach is observable is here, at allocation time. + # The partition binds only the PUBLIC side; nothing stops the maintainer-internal sequence + # allocating past the boundary, and CI cannot see it -- a public runner checks out origin only. # - # Warning only on ARRIVAL would fire exactly when it is too late -- at that point the next internal - # allocation already collides and there is no room to move. A check that fires only on collision has - # the same practical value as no check for every moment until the collision. So: warn at 90% of the - # boundary, with hundreds of numbers of runway left, and REFUSE at the boundary itself. - $warnAt = [int]($PublicBacklogFloor * 0.9) - if ($observed -ge $PublicBacklogFloor) { - throw @" -REFUSING TO ALLOCATE. The all-refs backlog maximum ($observed) has reached the public floor ($PublicBacklogFloor). -The partition assumes the maintainer-internal sequence stays BELOW that boundary, and it no longer does --- so the next number this would hand out is not safe to use. Raise PUBLIC_BACKLOG_FLOOR in -scripts/hooks/ledger_check.py (the allocator reads it from there), and say so in the PR. -"@ - } - elseif ($observed -ge $warnAt) { + # What THIS machine can see is real and was measured on 2026-08-03, because the claim in the + # previous version of this comment was worth checking rather than repeating: sweeping every ref + # does reach internal numbers. 490 vault-ish remote-tracking refs are present, 489 carry + # docs/BACKLOG.md, and 67 item numbers live ONLY there -- including #240-#247, the very numbers + # the Ledger erratum records as re-issued over cited work. So "Get-Floor sweeps internal refs too" + # is TRUE, and the sweep is the reason the floor is trustworthy. + # + # But seeing internal numbers is not the same as being able to detect the breach, and that is the + # part that cannot be fixed here. Once an internal item is allocated at or above the boundary it is + # indistinguishable, in this data, from a legitimate public item at the same number -- both are just + # `## N.` with N >= the floor. There is no attribute in the published files that separates them. A + # refusal arm would therefore have to either fire on correct input or never fire at all. + # + # It used to fire on correct input. It compared the WHOLE-SET maximum against the floor, so the + # first legitimate public item filed at #1000 (BACKLOG #1000, 2026-08-03) made every subsequent + # backlog allocation throw, repo-wide. The guard was not detecting a breach; it was detecting the + # partition being used as designed. + # + # So the refusal is REMOVED rather than rewritten to be unreachable -- a branch that cannot fire + # reads as protection and is worse than none. What remains is honest: warn while the INTERNAL + # sequence still has runway, measured on the sub-floor band only, where public numbers cannot + # distort it. Detecting an actual breach needs an internal-side input this repository does not have. + if ($residualWarning) { Write-Host "" - Write-Host "WARNING: the all-refs backlog maximum ($observed) is approaching the public floor ($PublicBacklogFloor)." -ForegroundColor Yellow - Write-Host " Still safe -- but the partition's headroom is running out, and at the boundary" -ForegroundColor Yellow - Write-Host " this script will refuse to allocate. Plan to raise PUBLIC_BACKLOG_FLOOR in" -ForegroundColor Yellow - Write-Host " scripts/hooks/ledger_check.py before that happens, not after." -ForegroundColor Yellow + Write-Host "WARNING: the highest sub-partition number ($subFloorMax) has reached 90% of the #$PublicBacklogFloor boundary." -ForegroundColor Yellow + Write-Host " The maintainer-internal sequence is running out of room below the partition." -ForegroundColor Yellow + Write-Host " Raise PUBLIC_BACKLOG_FLOOR in scripts/hooks/ledger_check.py (this script reads" -ForegroundColor Yellow + Write-Host " it from there) BEFORE the two sequences meet, and say so in the PR. Once they" -ForegroundColor Yellow + Write-Host " meet, nothing in this repository can tell the two apart." -ForegroundColor Yellow Write-Host "" } $start = [Math]::Max($observed, $PublicBacklogFloor - 1) + 1 diff --git a/tests/test_ledger_check.py b/tests/test_ledger_check.py index a40bf19a..e91e182a 100644 --- a/tests/test_ledger_check.py +++ b/tests/test_ledger_check.py @@ -608,3 +608,69 @@ def test_the_allocator_still_parses_the_floor_the_same_way() -> None: "alloc.ps1's floor regex must tolerate a type annotation " "(PUBLIC_BACKLOG_FLOOR: Final[int] = 1000), or an ordinary tidy-up silently disarms allocation" ) + + +# --- the partition guard must never again read the whole-set maximum ------------------------------- +# +# On 2026-08-03 filing BACKLOG #1000 -- the FIRST legitimate item in the post-partition public sequence +# -- made every backlog allocation in the repository throw: +# +# REFUSING TO ALLOCATE. The all-refs backlog maximum (1000) has reached the public floor (1000). +# +# One number was serving two incompatible purposes. The emit start wants the maximum over EVERYTHING so +# a number is never re-issued; the residual detector wants the maximum of the maintainer-internal +# sequence, to see it running out of room below the partition. The detector read the union, so a public +# item sitting where public items are SUPPOSED to sit read as a breach. The guard fired on correct input. +# +# These are source-text assertions, matching the seam above, and deliberately so: executing the +# allocator to test it would either spend a real number (claims are never released -- "holes are free, +# collisions are not") or write to .git/mefor-coord/alloc/**, and a test that mutates the ledger +# registry to check the ledger registry is its own hazard. + + +def test_the_allocator_measures_the_partition_band_separately() -> None: + """`Get-Floor` must return BOTH numbers, or the conflation is available to be made again.""" + src = _ALLOC.read_text(encoding="utf-8") + assert "SubFloorMax" in src, ( + "alloc.ps1 no longer computes a sub-partition maximum. The residual detector needs the highest " + "number BELOW the floor; if it reads the whole-set maximum instead, the first public item at " + "the boundary bricks every backlog allocation (this happened, with BACKLOG #1000)." + ) + assert "Floor =" in src or "Floor =" in src, ( + "alloc.ps1's Get-Floor must still return the whole-set Floor for the emit start — without it " + "the allocator can re-issue a number that already exists." + ) + + +def test_the_residual_detector_does_not_read_the_whole_set_maximum() -> None: + """The exact regression: the guard compared `$observed` (union max) against the public floor.""" + src = _ALLOC.read_text(encoding="utf-8") + assert re.search(r"\$observed\s+-ge\s+\$PublicBacklogFloor", src) is None, ( + "alloc.ps1 compares the WHOLE-SET maximum against PUBLIC_BACKLOG_FLOOR again. That is the " + "2026-08-03 defect verbatim: every public item at or above the floor is indistinguishable from " + "an internal breach in this data, so the comparison fires on the partition working as designed. " + "Measure the sub-floor band instead." + ) + assert re.search(r"\$subFloorMax\s+-ge\s+\$warnAt", src), ( + "the residual warning must be derived from the sub-partition maximum, not the union maximum" + ) + + +def test_the_floor_preview_evaluates_the_same_guard_as_a_real_allocation() -> None: + """`-ShowFloor` must not be able to disagree with the run it previews. + + It could, and did: the `-ShowFloor` block `return`ed 19 lines before the guard, so it printed a + `next:` number while every real allocation threw. An inspector that skips the checks it previews + answers a question adjacent to the one asked — and it is worse than no inspector, because a peer + session verified the allocator with it, got a green answer, and recorded it as a fact. + """ + src = _ALLOC.read_text(encoding="utf-8") + show_at = src.index("if ($ShowFloor)") + assert "$residualWarning" in src[:show_at], ( + "$residualWarning must be computed BEFORE the -ShowFloor block, so the preview and the real " + "allocation evaluate one shared expression rather than two that can drift apart." + ) + assert src.count("$residualWarning") >= 3, ( + "-ShowFloor must consult $residualWarning too; if only the allocation path reads it, the " + "preview is once again reporting a number the allocator would refuse to issue." + ) From 20fd0d42cd0d063168393f6d9e1534b18c4ca124 Mon Sep 17 00:00:00 2001 From: wshallwshall Date: Mon, 3 Aug 2026 21:23:17 -0500 Subject: [PATCH 2/2] fix(coord): move the refusal to a reachable trigger, and execute the allocator in tests Revision after an independent design pass. The mechanism from the previous commit holds; four things about the patch did not. 1. THE REFUSAL NOW HAS A REAL TRIGGER instead of being removed. Refusing nothing was the safe correction, not the right one. PUBLIC_BACKLOG_FLOOR is a constant in a source file, so it can be LOWERED -- a bad revert, a merge resolved wrongly, a tidy-up -- and NEITHER the pre-commit gate NOR CI can catch that: both read only the current value and have no memory of the previous one. Drop it to 900 and a new public #900 lands on top of an internal #900, green all the way. A ratchet outside the constant is the only instrument that can see it, so alloc.ps1 now keeps .boundary-highwater beside its registry and refuses when the constant falls beneath a value this clone already allocated against. Locally observable, genuinely reachable. Three quantities, three purposes, never crossed: $observed (union max) -> the next number, ONLY $subFloorMax (below boundary) -> the warning, ONLY $boundarySeen (highest floor) -> the refusal, ONLY 2. "THIS CLONE CANNOT SEE THE INTERNAL LEDGER" IS FALSE AND IS NOT THE ARGUMENT. Measured: 490 vault-ish remote-tracking refs, 489 carrying docs/BACKLOG.md, 67 numbers living only there including the #242-#246 band ADR 0115 cites. The premise fails for four other reasons, any one fatal, and they are now written down: an integer has NO PROVENANCE (a legitimate public item at the boundary and an internal breach are the same observation -- which is exactly how #1000 read as a breach); the refs are a FOSSIL (newest 2026-07-26, no refspec advances them, and they say 314 while the real vault is at 315); they are CLONE-LOCAL (a fresh clone has none); and they are MASKED (internal 314 < public 353). 3. $observed STAYS THE UNION MAXIMUM, with the trap recorded. Repointing it at the sub-boundary max -- the tempting one-line fix -- makes $start = max(353, 999) + 1 = 1000, a number already merged, and in a fresh clone with an empty registry the atomic CreateNew has no claim to collide with and would NOT catch the re-issue. 4. NAMING: $subFloorMax is "the highest number below the boundary", never "the internal maximum". It includes public pre-partition numbers, so it deliberately OVER-states the internal high-water and warns early rather than late. Relatedly, ledger_check.py's high-water 314 is NOT refreshed to 353: 314 is the vault-refs maximum, 353 is a public number on origin/main, and relabelling one as the other would plant a fresh false premise inside the comment being fixed. ledger_check.py's own claim -- that alloc.ps1 "warns if the all-refs maximum reaches this boundary" -- was the defect written down: wrong instrument, and it asserted a liveness the fossil ref store does not have. Replaced, not softened. THE TESTS NOW RUN THE ALLOCATOR. Nothing in tests/ ever had: the two existing references are read_text() assertions and stayed green through the entire brick. The seam is the process working directory -- alloc.ps1 has no -Repo switch and reads no env var, so a throwaway repo gets its own registry AND supplies its own ledger_check.py, making the boundary injectable. FLOOR=100, not 10: at 10 the warn tier and the highest sub-boundary number collide and every tier test passes for the wrong reason. The isolation check runs FIRST, because every later test spends real numbers if it is false. PROVEN TO FAIL: the same rig against the PRE-FIX alloc.ps1, with an item at the boundary, returns rc=1 and "REFUSING TO ALLOCATE ... has reached the public floor (100)" -- the production brick reproduced at 1/10 scale. Against the fix it allocates #101. Real registry confirmed untouched by the suite: 62 claims before and after, .floor-highwater still 1000. 34 tests pass; ruff clean. --- scripts/coord/alloc.ps1 | 105 ++++++++++++++++++++------- scripts/hooks/ledger_check.py | 24 ++++-- tests/test_ledger_check.py | 133 ++++++++++++++++++++++++++++++++++ 3 files changed, 231 insertions(+), 31 deletions(-) diff --git a/scripts/coord/alloc.ps1 b/scripts/coord/alloc.ps1 index 05b65dbe..e618b4bd 100644 --- a/scripts/coord/alloc.ps1 +++ b/scripts/coord/alloc.ps1 @@ -229,13 +229,33 @@ $measured = Get-Floor -Peek:$ShowFloor $observed = $measured.Floor $subFloorMax = $measured.SubFloorMax -# The residual warning is evaluated ONCE, here, so -ShowFloor and a real allocation cannot disagree. -# They did: -ShowFloor returned 19 lines before the guard, so it printed `next: 1001` while every real +# Both checks are evaluated ONCE, here, so -ShowFloor and a real allocation cannot disagree. They did: +# -ShowFloor returned 19 lines before the guard, so it printed a next number while every real # allocation threw. An inspector that does not run the checks it previews reports a number the tool # will refuse to issue -- it answers the adjacent question, which is the failure CLAUDE.md §11 names. $warnAt = if ($null -ne $PublicBacklogFloor) { [int]($PublicBacklogFloor * 0.9) } else { 0 } $residualWarning = ($Kind -eq "backlog") -and ($null -ne $PublicBacklogFloor) -and ($subFloorMax -ge $warnAt) +# THE BOUNDARY RATCHET -- the one refusal this data can actually justify. +# +# PUBLIC_BACKLOG_FLOOR is a constant in a source file, so it can be LOWERED: a bad revert, a merge +# resolved the wrong way, a tidy-up. Lower it to 900 and ledger_check.py cheerfully accepts a new +# public #900 sitting on top of an internal #900 -- with a GREEN pre-commit and a GREEN CI, because a +# runner has no memory of yesterday's value and the constant is the only thing either consults. +# +# A ratchet OUTSIDE the constant is the only instrument that can see this, and unlike the boundary +# check it replaces, it is genuinely reachable: it triggers on an observable local fact (the value +# moved down) rather than on an integer whose provenance cannot be recovered. +# +# THREE QUANTITIES, THREE PURPOSES -- keep them strictly separate: +# $observed (union max) -> $start / the next number, ONLY +# $subFloorMax (below boundary) -> the WARNING, ONLY +# $boundarySeen (highest floor) -> the REFUSAL, ONLY +$boundaryMark = Join-Path $alloc ".boundary-highwater" +$boundarySeen = 0 +if (Test-Path $boundaryMark) { [void][int]::TryParse((Get-Content $boundaryMark -Raw).Trim(), [ref]$boundarySeen) } +$boundaryLowered = ($Kind -eq "backlog") -and ($null -ne $PublicBacklogFloor) -and ($PublicBacklogFloor -lt $boundarySeen) + if ($ShowFloor) { # Name the SOURCES, not just the number. "Which files did this sweep actually read" is the # question every silent-narrowing bug turns on, and a bare integer cannot answer it -- a floor of @@ -244,16 +264,21 @@ if ($ShowFloor) { Write-Host "floor : $observed" if ($Kind -eq "backlog") { Write-Host "paths : docs/BACKLOG.md, docs/archive/backlog/BACKLOG-CLOSED.md" - Write-Host "sub-floor: $subFloorMax (highest number below the #$PublicBacklogFloor partition -- the internal sequence's runway)" + Write-Host "sub-floor: $subFloorMax (highest number BELOW the #$PublicBacklogFloor boundary; over-states the internal high-water)" + Write-Host "boundary : $PublicBacklogFloor (highest ever seen on this clone: $boundarySeen)" Write-Host "next : $([Math]::Max($observed, $PublicBacklogFloor - 1) + 1) (clamped to >= $PublicBacklogFloor)" } else { Write-Host "paths : docs/adr/NNNN-*.md (filenames, all refs)" Write-Host "next : $($observed + 1)" } Write-Host "watermark: $(Join-Path $alloc '.floor-highwater')" + if ($boundaryLowered) { + Write-Host "" + Write-Host "WOULD REFUSE: PUBLIC_BACKLOG_FLOOR is $PublicBacklogFloor but this clone has allocated against $boundarySeen." -ForegroundColor Red + } if ($residualWarning) { Write-Host "" - Write-Host "WOULD WARN: sub-floor max $subFloorMax has reached 90% of the #$PublicBacklogFloor partition." -ForegroundColor Yellow + Write-Host "WOULD WARN: highest sub-boundary number $subFloorMax has reached 90% of #$PublicBacklogFloor." -ForegroundColor Yellow } Write-Host "" Write-Host "Read-only: nothing was allocated." -ForegroundColor DarkGray @@ -265,33 +290,48 @@ if ($Kind -eq "backlog") { throw "Could not read PUBLIC_BACKLOG_FLOOR from $gateFile. Refusing to allocate a backlog number rather than guess a floor the gate will not honour." } - # THE RESIDUAL DETECTOR -- IT CAN ONLY WARN, AND THAT IS A LIMIT OF THE INPUT, NOT AN OVERSIGHT. + # WHY THE OLD "INTERNAL REACHED THE BOUNDARY" REFUSAL IS GONE. # - # The partition binds only the PUBLIC side; nothing stops the maintainer-internal sequence - # allocating past the boundary, and CI cannot see it -- a public runner checks out origin only. + # It compared the WHOLE-SET maximum against the floor, so the first legitimate public item filed at + # #1000 (BACKLOG #1000, 2026-08-03) made every subsequent backlog allocation throw, repo-wide. It + # was not detecting a breach; it was detecting the partition being used exactly as designed. # - # What THIS machine can see is real and was measured on 2026-08-03, because the claim in the - # previous version of this comment was worth checking rather than repeating: sweeping every ref - # does reach internal numbers. 490 vault-ish remote-tracking refs are present, 489 carry - # docs/BACKLOG.md, and 67 item numbers live ONLY there -- including #240-#247, the very numbers - # the Ledger erratum records as re-issued over cited work. So "Get-Floor sweeps internal refs too" - # is TRUE, and the sweep is the reason the floor is trustworthy. + # It is NOT that this clone cannot see internal numbers -- that was suspected and is false. + # Measured 2026-08-03: 490 vault-ish remote-tracking refs are present, 489 carry docs/BACKLOG.md, + # and 67 item numbers live ONLY there, including the #242-#246 band ADR 0115 cites. The sweep does + # reach them, and that is exactly why the floor is trustworthy. # - # But seeing internal numbers is not the same as being able to detect the breach, and that is the - # part that cannot be fixed here. Once an internal item is allocated at or above the boundary it is - # indistinguishable, in this data, from a legitimate public item at the same number -- both are just - # `## N.` with N >= the floor. There is no attribute in the published files that separates them. A - # refusal arm would therefore have to either fire on correct input or never fire at all. + # The premise fails for four other reasons, any ONE of them fatal: + # (a) NO PROVENANCE. An integer does not say which sequence issued it. "Internal reached the + # boundary" and "public was legitimately allocated at the boundary" are the SAME observation + # -- which is why #1000, on origin/main and holding a registry claim, read as a breach. + # (b) FOSSIL. The newest vault-ish ref here is 2026-07-26 and the only configured refspec is + # +refs/heads/*:refs/remotes/origin/*, so nothing can advance them. The partition landed + # eight days later. (Measured: these refs say 314 while the real vault is at 315 -- the + # fossil is already stale by one item.) + # (c) CLONE-LOCAL. A fresh public clone has zero vault refs, so the term is absent entirely. + # (d) MASKED. Internal 314 < public 353, so the internal term does not even determine the + # sub-boundary maximum today. # - # It used to fire on correct input. It compared the WHOLE-SET maximum against the floor, so the - # first legitimate public item filed at #1000 (BACKLOG #1000, 2026-08-03) made every subsequent - # backlog allocation throw, repo-wide. The guard was not detecting a breach; it was detecting the - # partition being used as designed. + # So the refusal moved to a trigger that IS observable and IS reachable -- the boundary ratchet + # above, which fires when PUBLIC_BACKLOG_FLOOR is lowered beneath a value this clone has already + # allocated against. What remains here is a warning only. # - # So the refusal is REMOVED rather than rewritten to be unreachable -- a branch that cannot fire - # reads as protection and is worse than none. What remains is honest: warn while the INTERNAL - # sequence still has runway, measured on the sub-floor band only, where public numbers cannot - # distort it. Detecting an actual breach needs an internal-side input this repository does not have. + # $subFloorMax is "the highest number below the boundary", NOT "the internal maximum". It includes + # public pre-partition numbers, so it deliberately OVER-states the internal high-water: it warns + # early rather than late, which is the safe direction for a runway indicator. + if ($boundaryLowered) { + throw @" +REFUSING TO ALLOCATE. PUBLIC_BACKLOG_FLOOR is $PublicBacklogFloor, but this clone has already +allocated against a boundary of $boundarySeen. The constant was LOWERED beneath numbers that were +issued under the higher value, so the next number handed out could collide with the maintainer-internal +sequence -- and neither the pre-commit gate nor CI can see it, because both read only the current value +of the constant and have no memory of the previous one. + +Restore PUBLIC_BACKLOG_FLOOR in scripts/hooks/ledger_check.py to at least $boundarySeen. If the +reduction is deliberate, delete $boundaryMark and say why in the PR. +"@ + } if ($residualWarning) { Write-Host "" Write-Host "WARNING: the highest sub-partition number ($subFloorMax) has reached 90% of the #$PublicBacklogFloor boundary." -ForegroundColor Yellow @@ -301,6 +341,19 @@ if ($Kind -eq "backlog") { Write-Host " meet, nothing in this repository can tell the two apart." -ForegroundColor Yellow Write-Host "" } + # Record the boundary we are about to allocate under. Only rises; only on a real allocation. + if ($PublicBacklogFloor -gt $boundarySeen) { + Set-Content -Path $boundaryMark -Value $PublicBacklogFloor -Encoding ASCII + } + + # $observed IS THE UNION MAXIMUM HERE, DELIBERATELY, AND MUST STAY THAT WAY. + # + # The tempting "fix" for the #1000 brick is to repoint $observed at the sub-boundary maximum, since + # that is what the guard should have read. Do not: $start would become max(353, 999) + 1 = 1000 -- + # a number already merged on origin/main -- and in a FRESH clone, whose registry is empty, the + # atomic CreateNew has no claim file to collide with and would NOT catch the re-issue. The union + # maximum is what makes "never hand out a number that exists anywhere" true; the sub-boundary + # maximum answers a different question and belongs only to the warning above. $start = [Math]::Max($observed, $PublicBacklogFloor - 1) + 1 } else { diff --git a/scripts/hooks/ledger_check.py b/scripts/hooks/ledger_check.py index dcb9fe96..33c7c667 100644 --- a/scripts/hooks/ledger_check.py +++ b/scripts/hooks/ledger_check.py @@ -75,11 +75,25 @@ # of check_backlog() computing a set and discarding it, i.e. unable to fail at all. A floor needs no # registry, no worktree, and no sight of the internal ledger (CI checks out origin only). # -# KNOWN RESIDUAL, and where it is detected: this binds only the public side. Nothing here can stop the -# internal ledger allocating past #1000. CI cannot see that -- but alloc.ps1 can, on any machine -# holding those refs, and it warns at allocation time if the all-refs maximum ever reaches this -# boundary. Raising this number is a one-line reviewable source change, deliberately not an allowlist -# file that would rot out of sight. +# KNOWN RESIDUAL, and it is NOT detected anywhere: this binds only the public side, and nothing in this +# repository can stop -- or observe -- the maintainer-internal ledger allocating past #1000. +# +# This comment used to claim alloc.ps1 "warns at allocation time if the all-refs maximum ever reaches +# this boundary". That was the wrong instrument twice over, and it was the defect written down: +# - The all-refs maximum has NO PROVENANCE. A public item legitimately allocated at the boundary and +# an internal breach are the same observation. That guard fired on BACKLOG #1000 -- correct input -- +# and bricked every backlog allocation in the repo until 2026-08-03. +# - It claimed a liveness the ref store does not have. The vault-ish remote-tracking refs it would +# read are a FOSSIL: no configured refspec advances them, the newest is older than the partition +# itself, and a fresh clone has none at all. +# alloc.ps1 now warns only on the highest number BELOW the boundary (which over-states the internal +# high-water, so it warns early), and refuses only on a lowered boundary, which is locally observable. +# +# Raising this number is a one-line reviewable source change, deliberately not an allowlist file that +# would rot out of sight. LOWERING it is the dangerous direction and is the one thing neither this gate +# nor CI can catch -- both read only the current value and have no memory of the previous one -- so +# alloc.ps1 keeps a `.boundary-highwater` ratchet beside its registry and refuses when the constant +# drops beneath a value that clone has already allocated against. # # THIS LINE IS PARSED, not imported: scripts/coord/alloc.ps1 regex-matches it so the floor is defined # exactly once and the allocator can never emit a number this gate refuses. Keep the name and the diff --git a/tests/test_ledger_check.py b/tests/test_ledger_check.py index e91e182a..9c0737fa 100644 --- a/tests/test_ledger_check.py +++ b/tests/test_ledger_check.py @@ -12,6 +12,7 @@ import json import re +import shutil import subprocess import sys from pathlib import Path @@ -674,3 +675,135 @@ def test_the_floor_preview_evaluates_the_same_guard_as_a_real_allocation() -> No "-ShowFloor must consult $residualWarning too; if only the allocation path reads it, the " "preview is once again reporting a number the allocator would refuse to issue." ) + + +# --- EXECUTION tests: the allocator is actually RUN, in a throwaway repo --------------------------- +# +# Nothing in tests/ had ever executed alloc.ps1. The two references above are `read_text()` assertions, +# and they stayed green through the entire period the allocator refused every backlog allocation. A +# gate that is only ever read is not a gate that has been tested. +# +# The seam is the PROCESS WORKING DIRECTORY, and it is the only one: alloc.ps1 takes no -Repo switch +# and reads no environment variable. `$repo` and `$common` come from `git rev-parse` against the cwd, +# so a throwaway git repo gets its OWN registry under its own .git AND supplies its own +# ledger_check.py, which is where the floor is parsed from. That makes the boundary injectable. +# +# FLOOR = 100, deliberately not 10: at 10 the warn tier (9) and the highest sub-boundary number (9) +# coincide, and every tier assertion would pass for the wrong reason. + +_PWSH = shutil.which("pwsh") + + +def _mkrepo(tmp: Path, floor: int, items: list[int]) -> Path: + """A throwaway repo carrying its own alloc.ps1, its own floor constant, and its own registry.""" + repo = tmp / "rig" + (repo / "scripts" / "coord").mkdir(parents=True) + (repo / "scripts" / "hooks").mkdir(parents=True) + (repo / "docs").mkdir() + shutil.copy(_ALLOC, repo / "scripts" / "coord" / "alloc.ps1") + (repo / "scripts" / "hooks" / "ledger_check.py").write_text( + f"PUBLIC_BACKLOG_FLOOR = {floor}\n", encoding="utf-8" + ) + body = "# rig\n\n" + "".join(f"## {n}. item {n}\n\n> OPEN\n\n" for n in items) + (repo / "docs" / "BACKLOG.md").write_text(body, encoding="utf-8") + subprocess.run(["git", "init", "-q"], cwd=repo, check=True) + subprocess.run(["git", "add", "-A"], cwd=repo, check=True) + subprocess.run( + ["git", "-c", "user.email=t@t", "-c", "user.name=t", "commit", "-qm", "rig"], + cwd=repo, + check=True, + ) + return repo + + +def _alloc(repo: Path, *args: str) -> subprocess.CompletedProcess[str]: + assert _PWSH + return subprocess.run( + [_PWSH, "-NoProfile", "-File", str(repo / "scripts" / "coord" / "alloc.ps1"), *args], + cwd=repo, + capture_output=True, + text=True, + ) + + +@pytest.mark.skipif(not _PWSH, reason="pwsh not on PATH") +def test_the_rig_cannot_reach_the_real_registry(tmp_path: Path) -> None: + """FIRST, because every later test here spends real numbers if this is false. + + The registry lives beside the git common dir, so a throwaway repo must resolve to its OWN .git. + If it resolved to the project's, these tests would burn production ledger numbers on every run -- + and claims are never released, so the damage would be permanent and silent. + """ + repo = _mkrepo(tmp_path, floor=100, items=[5, 7]) + out = _alloc(repo, "-Kind", "backlog", "-ShowFloor") + assert out.returncode == 0, out.stderr + real = str(Path(__file__).resolve().parents[1] / ".git").lower() + assert real not in out.stdout.lower().replace("/", "\\"), ( + f"the rig resolved to the REAL registry — refusing to run the rest.\n{out.stdout}" + ) + assert str(tmp_path).lower()[:12] in out.stdout.lower(), out.stdout + + +@pytest.mark.skipif(not _PWSH, reason="pwsh not on PATH") +def test_a_public_number_at_the_boundary_does_not_brick_allocation(tmp_path: Path) -> None: + """The 2026-08-03 regression, executed rather than pattern-matched. + + An item at exactly the floor is the FIRST legitimate public allocation. Before the fix this threw + `REFUSING TO ALLOCATE … has reached the public floor` for every subsequent caller, repo-wide. + """ + repo = _mkrepo(tmp_path, floor=100, items=[5, 100]) + out = _alloc(repo, "-Kind", "backlog", "-Title", "after the boundary") + assert out.returncode == 0, f"allocation refused on legitimate input:\n{out.stdout}{out.stderr}" + assert "ALLOCATED BACKLOG #101" in out.stdout, out.stdout + assert "REFUSING" not in out.stdout + out.stderr + + +@pytest.mark.skipif(not _PWSH, reason="pwsh not on PATH") +def test_the_warning_reads_the_sub_boundary_band_not_the_union(tmp_path: Path) -> None: + """A public number above the boundary must NOT trip the runway warning; a sub-boundary one must.""" + quiet = _alloc( + _mkrepo(tmp_path / "a", floor=100, items=[5, 100]), "-Kind", "backlog", "-ShowFloor" + ) + assert "WOULD WARN" not in quiet.stdout, ( + f"a public item at the boundary tripped the internal-runway warning:\n{quiet.stdout}" + ) + loud = _alloc(_mkrepo(tmp_path / "b", floor=100, items=[95]), "-Kind", "backlog", "-ShowFloor") + assert "WOULD WARN" in loud.stdout, ( + f"sub-boundary 95 is past the 90 warn tier and did not warn:\n{loud.stdout}" + ) + + +@pytest.mark.skipif(not _PWSH, reason="pwsh not on PATH") +def test_lowering_the_boundary_is_refused(tmp_path: Path) -> None: + """The replacement refusal, and it must actually fire. + + Neither the pre-commit gate nor CI can catch a LOWERED floor: both read only the current value and + have no memory of the previous one. The ratchet beside the registry is the only instrument that can. + """ + repo = _mkrepo(tmp_path, floor=100, items=[5]) + first = _alloc(repo, "-Kind", "backlog", "-Title", "sets the ratchet") + assert first.returncode == 0, first.stderr + gate = repo / "scripts" / "hooks" / "ledger_check.py" + gate.write_text("PUBLIC_BACKLOG_FLOOR = 50\n", encoding="utf-8") + after = _alloc(repo, "-Kind", "backlog", "-Title", "should be refused") + assert after.returncode != 0, f"a lowered boundary was accepted:\n{after.stdout}" + assert "REFUSING TO ALLOCATE" in after.stdout + after.stderr + + +@pytest.mark.skipif(not _PWSH, reason="pwsh not on PATH") +def test_showfloor_agrees_with_a_real_allocation(tmp_path: Path) -> None: + """The preview must not be able to contradict the run it previews — it could, and did.""" + repo = _mkrepo(tmp_path, floor=100, items=[5, 100]) + preview = _alloc(repo, "-Kind", "backlog", "-ShowFloor") + assert "next : 101" in preview.stdout, preview.stdout + real = _alloc(repo, "-Kind", "backlog", "-Title", "must match the preview") + assert "ALLOCATED BACKLOG #101" in real.stdout, ( + f"-ShowFloor promised 101 and the allocator issued something else:\n{real.stdout}" + ) + + # And the refusal case must agree too, in the same direction. + (repo / "scripts" / "hooks" / "ledger_check.py").write_text( + "PUBLIC_BACKLOG_FLOOR = 50\n", encoding="utf-8" + ) + assert "WOULD REFUSE" in _alloc(repo, "-Kind", "backlog", "-ShowFloor").stdout + assert _alloc(repo, "-Kind", "backlog", "-Title", "x").returncode != 0