fix(pm): net ci-failure.mjs's walk against mid-walk transport failures - #10361
Merged
Conversation
A transport failure arriving after the pre-walk probe passed escaped as an uncaught throw, and node's exit 1 collides with this file's own EXIT_RED -- so a container that read nothing about the tree handed its caller the code that means "the tree is red and here is the proof". Every network read after the probe is now wrapped. The exit-2-vs-3 fork is settled by re-probing rather than by picking a constant: an unusable fresh reading is the environment (3), a healthy one is a walk that cannot answer (2), and an unclassifiable one falls to 2 rather than blaming the container. Never 1, never 0. The partial-output invariant rides along, twice: the net's report says whether nothing was READ or nothing was JUDGED and counts what had been listed, and assertionStatus gains a fourth answer so a refused annotations call stops printing as "the check carried no annotation with any content in it" -- four lines under this same block's own disclaimer. Fixes #10155 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DdCnBGcHeufjrq7drTD3wt
…-failure-transport-net
os-zhuang
marked this pull request as ready for review
August 20, 2026 16:42
os-zhuang
enabled auto-merge
August 20, 2026 16:43
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #10155
rest()throws a plainErroron any non-ok status, and before this nothing between it and the top level caught. Node exits 1 on an uncaught exception — and this file's own exit table assigns exit 1 toRED, "failing checks, and the assertion text was retrieved for EVERY one of them. The output is the answer."So a container that could not read one byte about the tree handed its caller the code meaning "the tree is red and here is the proof" — to a caller this file's own header instructs to branch on
$?(Piping hides all of it. Read $?). That is the #4690 inversion this file exists to refuse, arriving through the one path it did not guard.#9966's probe does not cover this: it fires once, at the start, so a quota exhausted between it and the last annotations call, an access that changes under the run, a transient 5xx, or a 404 on an annotations URL all arrive behind it.
Reproduced before, measured after
Probe green, walk's first page refused, against
mainat2e39181a(post-#10275, post-#9966). No socket opened —fetchis stubbed so the measurement is deterministic:After, same harness:
EXIT=2, and the report says so in words. A second harness that revokes access under the run — the card's "a repo whose access changes under the run" — landsEXIT=3.The fork, settled by asking rather than by picking
Triage handed the exit-2-vs-3 choice down ("either candidate is honest; the current behaviour is the only wrong answer"). I took neither as a constant, because the causes are not one class and each constant is wrong for half of them:
So the transport is re-probed on the way out and its fresh verdict picks. A fresh reading is exactly what distinguishes a real transport failure from a transient status on one page.
scripts/pm/check-half-states.mjs's in-loop net makes the identical call for the identical reason — this reuses that decision rather than inventing a second one beside it. An unclassifiable re-probe (a repo-scoped 5xx) deliberately falls to 2: the classifier's narrowness is the point, and a loud undetermined beats a confident wrong claim about someone's container.Header consistency. Exit 3 previously promised it was decided before anything is read. That sentence is rewritten: exit 3 now promises a classified transport verdict, not an early one. The displaced promise was worth less than the guarantee it cost, because a transport that dies at request 40 classifies the environment exactly as much as one dead at request 1.
The partial-output invariant, in both places it was broken
The net's report carries the
sweptanalogue: it says whether nothing was read or nothing was judged, and counts the check-runs already listed — so a stopped walk cannot read as a completed one.assertionStatusgains a fourth answer. A refused annotations call leaves the same empty list a check that recorded nothing leaves, and reading it asnoneprinted this, four lines apart, on a check measured to carryannotations_count: 1:The second sentence is false and is precisely what the first forbids. It now reads
NOT RETRIEVED — ... an absence of a READING, not an absence of evidence. The blocked-log-blob paragraph no longer fires either, since it blamed the egress policy for a refusal that happened on plain REST.Verification
Every exit code captured before any pipe —
cmd > file 2>&1; EXIT=$?— since| tailreports the pipe's status, and getting that wrong while fixing this file would be its own joke.Gates re-derived at the merged head with
node scripts/pm/dispatch-gates.mjs(no args — it takes its own change set from the merge base), all green at8025ba82:node scripts/check-cross-package-test-inputs.mjs—OK: 12 package(s) read outside themselves, all declared...node scripts/check-nul-bytes.mjs—check-nul-bytes: OK (scanned 6101 text file(s) ... no raw ASCII control bytes)node scripts/pm/ci-failure.mjs --self-test—OK self-test: ...(runs inLint & Repo Gatesstep 23 since scripts/pm/ci-failure.mjs's --self-test runs nowhere in CI, unlike its four sibling PM tools #9898, so the new pins execute in CI)The new pins were shown to fail, not merely to pass. Two ablations, each confirmed on disk by grep-counting the removed text and an injected marker, each restored and re-confirmed:
unretrievablebranch ⇒ 2 failures, exactly the twounretrievablepins; thenonecontrol pin stayed green, which is what makes the fixture a pin rather than a restatement.midWalkVerdictreturnEXIT_RED(what node did before) ⇒ 11 failures, the load-bearing one readingexpected [false,false,false] / actual [true,true,true]— the defect verbatim.The zero-hit control-character sweep is backed by a positive control: the identical
grep -naPfires (exit 0) on a scratch file carrying\x01and finds nothing (exit 1) in the changed file.Notes
scripts/publishes nothing ⇒skip-changeset, applied additively.packages/spec/src/**and this is not that; on content, this changes a seat tool's own exit vocabulary — no product contract, no accept/reject behaviour of any spec/Zod schema, no widened authorable surface.assertionStatusandmidWalkVerdicthave no importers outside this file (grepped: onlyrenderand the self-test).scripts/pm/ci-failure.mjs. Notegit diff origin/main --stat(two-dot) reports 13 files here becauseorigin/mainadvanced from2e39181ato128684d5mid-task; the merge-base diff is one file.Generated by Claude Code