diff --git a/docs/LEDGER-GATE.md b/docs/LEDGER-GATE.md index 9d402d7..57d71f1 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 66cbeab..e618b4b 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,36 @@ 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 + +# 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 @@ -217,12 +264,22 @@ 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 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: highest sub-boundary number $subFloorMax has reached 90% of #$PublicBacklogFloor." -ForegroundColor Yellow + } Write-Host "" Write-Host "Read-only: nothing was allocated." -ForegroundColor DarkGray return @@ -233,34 +290,70 @@ 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. + # WHY THE OLD "INTERNAL REACHED THE BOUNDARY" REFUSAL IS GONE. + # + # 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. + # + # 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. # - # 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 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. # - # 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) { + # 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. + # + # $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. 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. +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. "@ } - elseif ($observed -ge $warnAt) { + 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 "" } + # 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 dcb9fe9..33c7c66 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 a40bf19..9c0737f 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 @@ -608,3 +609,201 @@ 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." + ) + + +# --- 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