fix(scripts): check:single-claim-paths keeps its own two contracts — a failed board read exits PREREQUISITE NOT MET, and the self-test handshake is set by the verdict it certifies - #18986
Conversation
…o PREREQUISITE NOT MET instead of the accusation code
The live path read a PR's file list with nothing catching a throw, so any
transport failure — a 404 from a number that resolves to no pull request, a
dead credential, a dropped socket — exited on node's unhandled rejection
status, 1. This file defines 1 as "judged, an earlier open PR already claims
a listed path": an accusation about a board the run never read, against an
author who did nothing.
Its own header already forbade exactly that ("a mis-wired gate must not read
as an accusation, because it would be red on every PR at once for something
no author did"), so this is the file keeping its own rule.
The read — and only the read — is now wrapped. `judge` stays outside the try
on purpose: it is pure and cannot throw on a transport, and a catch spanning
it would relabel a real crash in the verdict layer as "the board was not
read". The failure is turned into a refusal by a pure handler that returns
`{ exit, lines }` exactly as `judge` does, so the self-test drives the arm
offline with no process exit and no network.
The code is EXIT_PREREQUISITE_NOT_MET, imported from the fleet's shared
register rather than numbered locally, the way check-issue-citations and
post-stamped already spell "the board was not read". It stays NON-ZERO: a
board that could not be read is not a clean board, so the CI consumer is
still red on it.
Probe, in an agent container, before and after
(PR_NUMBER=99999999 GITHUB_REPOSITORY=objectstack-ai/objectstack):
before: unhandled rejection, stack trace, exit 1
after: "PREREQUISITE NOT MET — the board was not read — GitHub API 404
for /repos/.../pulls/99999999/files... — not a verdict, not a
clean tree", exit 3
Live reverse control, PR_NUMBER=18935: unchanged, clean verdict, exit 0.
Self-test: a new battery of 19 cases, roster floor raised 8 -> 9 batteries,
74 -> 93 cases. It pins the code and the words, drives a throwing fake api
through collect into the same handler, holds a healthy api reaching a real
verdict as the reverse control, and pins structurally that the handler is
reached after the LAST network read — the live one — so a deleted live catch
reds rather than leaving the pin satisfied by the self-test's own calls.
Claude-Session: https://claude.ai/code/session_01BTeBejoPUvRHN8WdAJC6oF
Co-authored-by: Claude <noreply@anthropic.com>
… the verdict prints, not before the async block that produces it
`selfTestReachedVerdict = true` sat on the line directly above
`return (async () => {` — the block that runs every assertion, evaluates the
battery floor and prints the verdict. The flag was therefore true before a
single case had run, so an early return or a throw anywhere inside that block
left it true and the dispatch's `if (!selfTestReachedVerdict)` branch was
unreachable. The one sentence the flag exists to make possible — "selfTest()
returned without reaching its verdict" — could never be said by this gate.
AGENTS.md, verbatim: "Set the flag as the self-test's last statement, after
its success line prints" and "SAY the self-test never reached its verdict.
An exit code is not a handshake."
The assignment now is that last statement. The failing arm is left without
one deliberately: it calls process.exit(1) on the next instruction, so no
reader survives to consult the flag — the handshake is for the SILENT ways
out, which is what the new position catches.
Negative control (deliberate early return injected inside the block, on the
committed file, restored by blob hash): red, "selfTest() returned without
reaching its verdict", exit 1. Before this commit the identical injection
printed the same partial output and exited 0.
Claude-Session: https://claude.ai/code/session_01BTeBejoPUvRHN8WdAJC6oF
Co-authored-by: Claude <noreply@anthropic.com>
Contract reviewServed-tier: ① Derived judgments
② Semver level
③ Boundary flags
Implemented-by: VERDICT: PASS Generated by Claude Code |
|
Landing record — every pre-check met, blocked at the channel (skills seat, Every check on Generated by Claude Code |
…o a flag above the returned block is no longer a HELD
The probe injected `return;` as the FIRST statement of the dispatched function
and read one answer from it. A handshake set ABOVE the block that function
RETURNS stays unset under that injection, the dispatch refuses, and the row
read HELD -- while the same `return;` one statement lower, inside the block,
leaves the handshake SET and the run silent at exit 0. The 2026-09 census
cleared `scripts/check-single-claim-paths.mjs` HELD exactly that way, on the
revision that carried that ordering defect.
A file whose dispatched function returns a block is now read at both points and
HELD requires both. `HANDSHAKE-ABOVE-BLOCK` is the new verdict for "point 1
refused, point 2 exited 0", counted apart from HELD as ACCIDENT already is.
Measured on this base (223 rows, 217 with a resolvable entry): ONE row carries a
second point -- `check-single-claim-paths.mjs` -- so no other row's reading
moves, and every other row publishes `points: 1` with the reason it has one.
before --probe --only scripts/check-single-claim-paths.mjs -> 0 DEFEATED, 1 HELD, exit 0
after same command -> 1 HANDSHAKE-ABOVE-BLOCK, exit 0; point 1 refused,
point 2 exited 0 printing 0 bytes
control PR #18986's head (03310bc, the same file with the handshake moved
inside the block) -> HELD at BOTH points, both baselines exit 0
The anchor is the returned `async () => {` IIFE at the dispatched function's own
statement level: masked, line-anchored and at depth 0. Deliberately unadmitted,
each with its reason in the docblock: an `await (async () => {` in the middle of
the body (an early return there skips a battery while the self-test still
reaches its verdict -- hole 1, never this one), a returned `.then()` callback
and an `async function` expression (no carrier in this tree). The header also
records what the point CANNOT read: work whose failure is silent, measured on
`check-system-context-census.mjs`, whose handshake sits 48 lines above its own
battery floor and where both runs print the same bytes.
Costs: the second pair of spawns runs only for a row that HELD at the first
point AND has a second point, so a red-baseline checkout costs what it did.
Controls, both directions: the pair of fixtures differing only in WHERE the
handshake line sits (above -> HANDSHAKE-ABOVE-BLOCK, inside -> HELD at both);
five anchor decoys ahead of the real block, one per rule; an awaited mid-body
block and a block-free self-test both yielding no second point; a one-point row
reading identically through the composer and through the probe; and a row
DEFEATED at the first point probed exactly once.
Claude-Session: https://claude.ai/code/session_01BTeBejoPUvRHN8WdAJC6oF
Co-authored-by: Claude <noreply@anthropic.com>
|
Landing attempt on the maintainer's word — refused a fourth time, with the allow rule on disk (skills seat, PR #19047 ( ⇒ The rules are right and on Generated by Claude Code |
|
Landing record — landed on the seat's record through the CCR route (skills seat, Channel: the two allow rules of PR #19047 ( Acts: Generated by Claude Code |
Fixes #18940
Clause-②: no
Two defects in one file, both of the same class —
scripts/check-single-claim-paths.mjsagainst the contracts it declares about itself. One commit each, so each is readable alone.
No other file is edited.
A. A transport failure exited 1, and this file defines 1 as the accusation
The live path read a PR's file list with nothing catching a throw.
githubApithrows on anynon-ok response and
fetchthrows on a dead socket, so a 404, a dead credential or a droppedconnection escaped as an unhandled rejection and node exited 1 — which this file's own exit
register defines as "judged, an earlier open PR already claims a listed path". An accusation,
about a board the run never read, against an author who did nothing.
The file already forbade exactly that, in its own header: "a mis-wired gate must not read as an
accusation, because it would be red on every PR at once for something no author did." So this is
the file keeping its own rule, not a new policy.
The probe, run in an agent container. The card carried it unrun; this is the first run of it.
Before, at the base commit
26c73fb4e:After:
Live reverse control, same tree, a PR number that really resolves — unchanged:
Shape of the change:
judgestays outside thetryon purpose: it is pure and cannotthrow on a transport, and a catch spanning it would relabel a real crash in the verdict layer
as "the board was not read" — the same lie in the other direction.
boardNotReadRefusal(error), returning{ exit, lines }exactly asjudgedoes. That is what lets the self-test drive the arm withno process exit and no network. The single
process.exitstays at the dispatch, where everyother verdict's exit already lives.
EXIT_PREREQUISITE_NOT_MET, imported from the fleet's shared register, the wayscripts/check-issue-citations.mjsandscripts/pm/post-stamped.mjsalready spell it, so thewording family is one phrase across the tree: "PREREQUISITE NOT MET — the board was not read".
(
.github/workflows/single-claim-path-guard.yml) is still red on it. That is the right answer,and 0 would have been the anti-pattern the header names.
node's unhandled-rejection status.
The CI consumer needs no change, measured. Its only comment about an exit code, at
single-claim-path-guard.yml:94-96, names 2 for one specific condition (PR_NUMBERabsent) anddoes not enumerate the register, so there is nothing there to extend. The
run:line isunchanged and any non-zero exit is a red job.
B. The self-test handshake was set before the block that produces the verdict
selfTestReachedVerdict = truesat on the line directly abovereturn (async () => {— theblock that runs every assertion, evaluates the battery floor and prints the verdict. The flag was
therefore true before a single case had run, so an early return or a throw anywhere inside that
block left it true and the dispatch's
if (!selfTestReachedVerdict)branch was unreachable. Theone sentence the flag exists to make possible could never be said by this gate.
AGENTS.md, verbatim: "Set the flag as the self-test's last statement, after its success line
prints" and "SAY the self-test never reached its verdict. An exit code is not a handshake."
The assignment is now that last statement. The failing arm deliberately gets none: it calls
process.exit(1)on the next instruction, so no reader survives to consult the flag — thehandshake is for the SILENT ways out, which is exactly what the new position catches.
The negative control — red, then green, both legs on committed files
The instrument is
scripts/ablation-replace.mjs, so the mutation and the restore are both provenagainst the disk rather than against an exit code. The injected mutation is identical in both
legs: an early
return;as the first statement inside the async verdict block.Leg 1 — the fixed file (this branch's HEAD):
Leg 2 — the same injection on the pre-fix file (commit
d4b1f2817, i.e. after A and beforeB), restored into the tree with
git restore --source=... --worktreeso the index is untouched:Leg 2 printed zero bytes from the self-test and exited 0. That is the defect, executed:
the runner had nothing to say, and "every case held" and "no case ran" were the same output. This
is why the triage asked for a RED negative control — without it, fixed and unfixed look identical
in the gate's own output on a healthy run.
git status --porcelainis empty after both legs, and the working blob equals the HEAD blob.Self-test counts
26c73fb4eSELF_TEST_BATTERY_FLOORrises 8 to 9 with the roster, because leaving it at 8 would let the newbattery be deleted without a red — which is what the roster comment already says about deletion.
The measured base is 74 cases, not the 83 the dispatch carried; the 74 is also exactly the sum of
the eight declared floors, so the floors sat at the live counts before this change and still do.
The 19 new cases pin the code and the words, drive a throwing fake api through
collectinto thesame handler, keep a healthy api reaching a real verdict as the reverse control, and pin
structurally that the handler is reached after the LAST network read — the live one — so deleting
the live catch reds instead of being satisfied by the self-test's own calls to the handler.
No existing verdict moved: the CLEAN / CONFLICT / NOT WIRED pins are byte-identical and the three
older constants keep their values, asserted as a new case (the four codes are four distinct
values).
Census — who else carries this ordering bug
The card left this unmeasured and asked for it. Measured on this branch, and not fixed here.
Population: 105 files under
scripts/that both declarelet selfTestReachedVerdict = falseandread it at the dispatch. Two independent mechanical criteria, which agree on the same 4
candidates: (1) a success line printed after the last flag assignment; (2) an assertion-registering
call (
registerCase,batteryFloorFailures,checkSelfTestFloor) after the last flag assignment.All 4 then read by hand, because the mechanical signal cannot tell "the verdict is decided at the
dispatch" apart from "assertions still run after the flag".
scripts/check-single-claim-paths.mjsscripts/check-osv-exemptions.mjsselfTest(); the flag is its last statement. The floor call at 675 is at the DISPATCH, after the flag is read at 657scripts/typecheck-configs.mjsselfTest(), floor at 377 in the dispatch, flag read at 357scripts/check-durability-degradation-log-level.mjsreadSeamsReachedVerdict(5609)The other 101 set the flag as the last statement of the function whose verdict it certifies.
So this is one card, not a class — which also answers the card's own question about whether
the B half should have been promoted to a census card. It should not.
Worth recording for the next reader:
docs/audits/2026-09-self-test-shape-census.mdscores thisvery file HELD (its row, line 321). That is correct for the axis it measured — its probe
injects
return;as the first statement of the dispatched function, which lands above the flagassignment and therefore does leave the flag false. The ordering axis is a different injection
point, inside the block, and the published census is blind to it by construction. Neither reading
contradicts the other.
Changeset
skip-changeset, measured rather than assumed. Nothing published moves: 70 non-private packagesdeclare
files[]and every one shipsdist,README.mdandCHANGELOG.mdonly — none shipsrepo-root
scripts/. The only reference to this script outsidescripts/is the private rootmanifest's own
check:single-claim-pathsline. The positive control for the method is that samescan finding 70 populated
files[]arrays, so an empty result is a reading and not a silent miss.Verification
pnpm check:single-claim-paths(the self-test) — pass, 93 cases, 9 batteries.node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackfrom this worktree, against the real change set (1 path, three-dot semantics vs merge base
26c73fb4e). Every exit code captured redirect-then-$?, never through a pipe. All 29 exited 0, the slowest beingpnpm check:pm-dispatch-gatesat 745s (1848 self-test cases) under contention with a siblingworktree running the same gate. Reconciled with
dispatch-gates.mjs --ran, each line recorded ascommand :: exit N: 29 derived, 29 run, 0 UNRUN, 0 NOT-MEASURED as a DERIVED zero rather than aclaimed one, and none of the 29 exited 3.
grep -naPover the C0 range plus DEL): no hits.very script's live invocation, whose argv takes a value only inside a CI run.
Reader test
A transport failure in CI reads PREREQUISITE NOT MET and a red job, never "an earlier PR claims
your path". A self-test that dies mid-block is reported as never reaching its verdict.
Acceptance notes
Observations from this file, noted and deliberately not filed and not fixed — none is a
reproducible defect, a declared-contract violation or a metadata-authoring trap:
ever arrives, the register would be better as a table. Style, no behaviour.
MAX_PAGESis 30 in both loops with one comment explaining the file-list ceiling; the open-PRloop borrows the same constant for a different endpoint's ceiling. Nothing observable follows
from it today at this repo's open-PR count.
Generated by Claude Code