fix(scripts): correct the pipe-shape exit-code advisory in every sibling banner - #13828
Merged
os-project-manager merged 2 commits intoAug 31, 2026
Merged
Conversation
…ing banner
The advisory printed by `import-prerequisite.mjs` and copied literally into four
more gates named `| tail -4` as the shape that "reads green either way" and left
`| head` unnamed. Measured, that is backwards for anyone reading
`${PIPESTATUS[0]}` or running under `pipefail`: `| tail` forwards the true
status.
All nine banner sites now carry one identical mechanism paragraph, so a single
grep finds every copy — the property whose absence made this change necessary.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pk26oZ12t5N1hwGW1m1MgC
…pe-shape-advisory-siblings
os-project-manager
marked this pull request as ready for review
August 31, 2026 14:32
Contributor
⛔ merge queue 构建失败 — 先分诊,再决定要不要重排队列构建 33404373114 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集), 失败的 job(日志抽取,best effort):
跨 PR 相同签名(24h,按失败测试文件聚合):
历史信号:
分诊清单:
Generated by Claude Code · merge-queue-triage workflow (#4859) |
os-project-manager
deleted the
claude/issue-13429-pipe-shape-advisory-siblings
branch
August 31, 2026 15:06
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 #13429
Nine banner sites across six files now carry one identical pipe-shape paragraph, so a single grep finds every copy — the property whose absence made this card necessary.
Zone 1 binds me to name
| head -Nand the EPIPE mechanism, to name the safe capture, and to keep| tailidentified as the shape that forwards the true status. All three are honoured. What I could not do is repeat PR #13427's specific sentence:Measured 2026-08-31, node 22.22.2 / bash 5, on a real refusing gate:
| headdid not turn${PIPESTATUS[0]}orpipefailgreen. It is not a small-output artifact — a 5 MB producer behaves the same, and so do four other constructed shapes (process.exitCode, backpressured async writes,pnpm execin front, a bash wrapper): every one preserved its non-zero code through| head -1.And EPIPE genuinely happens — this is not "head read everything":
The producer takes EPIPE and still reaches its own exit code. node ignores SIGPIPE and swallows the stdout write error, so a node gate's status survives
| headintact.What is actually true, and what all nine sites now say:
$?after any pipe — it is the LAST command's status, andhead/tailboth essentially never fail. It is not a property of one shape, and no choice of shape repairs it.${PIPESTATUS[0]}/pipefaildo recover the gate's own code.| tailreads to EOF and forwards it.| head -Ncloses the read end early, the gate takes EPIPE, and what that really costs is the verdict text (truncated) plus — for a producer that does not ignore SIGPIPE, unlike node — a real code replaced by 141:seq 1 100000000 | head -1reports${PIPESTATUS[0]}= 141. That is a false red, the opposite direction from the one claimed.Because the reference site would otherwise diverge from the other eight and keep the false claim,
scripts/check-i18n-bundles.mjsis corrected here too. That is the one file the card explicitly says is already fixed; the diff is one paragraph, same defect class, same gate family, and it is what makes the single-grep census property actually hold. Flagging it for the maintainer rather than burying it.The measurement is anchored where the next author will look: a block comment on
reportPrerequisiteNotMet, with an explicit "do NOT write that| headturns PIPESTATUS/pipefail green" and why.Census instrument
Verbatim, the command used to find the sites:
The first alone is insufficient and the card says so; the second is what surfaced two sites the card's list does not carry (both named below). Going forward the instrument is one grep on the corrected mechanism:
which now returns all nine sites across the six files.
Inheritor count: measured 45, not 44 and not 43
Derivation, verbatim:
45 files. Both circulating numbers are wrong, in opposite directions:
git grep -l "import-prerequisite" -- 'scripts/**'. Of those 44, one is the module itself and one —scripts/pm/dispatch-gates.mjs— only mentions the filename in a comment and imports nothing. So underscripts/**the real importer count is 42.scripts/**pathspec, which is itself too narrow: three real importers live inpackages/lint/scripts/and noscripts/**glob can see them.42 + 3 = 45. Verified live: the corrected banner rendered from
packages/lint/scripts/check-doc-formula-expressions.mjsduring this PR's gate run.A2.4 inherit-vs-shadow split: the sets are disjoint
Zero of the 45 inheritors carries its own literal copy of the banner. All four literal-copy files named by the card (
check-i18n-coverage.mjs,check-half-states.mjs,ci-failure.mjs,check-test-completeness.mjs) import nothing from the module — they hard-code instead of inheriting. So the one-line edit corrects all 45 with no shadowing, and the remaining per-file work is exactly the four files the card lists. The dispatch's worry that the card's file list might be incomplete does not materialise on this axis.(
scripts/check-dual-build-cjs-loads.mjsalso carries a localPREREQUISITE NOT METbanner, but it contains no pipe advisory at all — an omission, not a copy of the wrong fact. Left alone.)Sites the card's instrument missed
Two, both surfaced by the mechanism census:
scripts/pm/ci-failure.mjs:170— a module-header comment,Piping hides all of it (\… | tail` reports the PIPE's status). Read `$?`.Same file already in scope, same wrong fact (names| tail` as the thing that hides), missed because the card grepped the old sentence. Corrected here as a bounded in-place fix: same defect class, mechanical, one line, no new verification surface.packages/cli/scripts/check-app-nav-i18n.mjs:620— true but incomplete (prescribesecho "EXIT=$?"without saying before any pipe, names no shape). Different package, different gate family, so not touched: filed as [finding]packages/cli/scripts/check-app-nav-i18n.mjsprerequisite banner saysecho "EXIT=$?"but never says BEFORE any pipe, and names no shape #13825.Judged out of class and deliberately untouched: three
.github/workflows/*.ymlcomments that saycmd | tailreports the pipe's status. Each is scoped to bare$?, where it is true, and each sits beside code that already does redirect-then-capture.check:ratchet-remedy-authority(A2.5)The dispatch was right that no path derivation names it.
node scripts/pm/dispatch-gates.mjsderived 23 families for this diff andcheck:ratchet-remedy-authorityis not among them. Run explicitly anyway — its own verdict line:Green. This diff adds no remedy prose and no ratchet vocabulary, so that is the expected direction — recorded as the data point the dispatch asked for either way.
Tests
All gate results below are from
8f739a646, the final commit, and exit codes were captured by redirect-then-capture, never through a pipe.Self-tests — the constraint the card names (each must stay green, no exit code may change):
--self-testscripts/import-prerequisite.mjsscripts/check-i18n-coverage.mjsscripts/check-i18n-bundles.mjsscripts/check-test-completeness.mjsscripts/pm/check-half-states.mjsscripts/pm/ci-failure.mjsExit codes unchanged, spot-checked on the live refusal path:
node scripts/check-test-completeness.mjsanswered 3 before the edit and 3 after.New pins.
reportPrerequisiteNotMetis split into a pure text builder plus a thin printer (no new export, no caller change,process.exit(1)untouched) so the self-test can assert the advisory as a value. Five pins: capture-before-any-pipe; the shape-independent false green;| tailkept as the forwarding shape;| head -Nplus EPIPE plus its real cost; and a negative pin refusing the claim that any pipe shape defeats${PIPESTATUS[0]}/pipefail.Ablation — the pins bite. Committed first, then the banner was reverted to the old wrong text with a restore trap on absolute paths. Mutation confirmed on disk by counting both the injected and the deleted text plus a
git hash-objectmove (55b7512b->b511968f); no build/distleg exists here, these.mjsfiles are executed directly. Result: 4 of the 5 new pins went red, self-test exit 1. The fifth (capture-before-any-pipe) stayed green because the mutation replaced only the mechanism paragraph — reported rather than smoothed over. Restoration proven byte-for-byte: on-disk hash back to55b7512b,git diff HEADempty,git status --porcelainempty, self-test back to 41/41 green.Derived gate union — 25 commands, all green.
node scripts/pm/dispatch-gates.mjs(no paths passed; it derives its own changeset from the merge base) named 23 families plus 2 convention-triggered ones. Every one run to exit 0, pluscheck:ratchet-remedy-authorityandcheck:nul-bytes.Two of them are not locally runnable in their bare form and were run in the shape CI uses:
check:partof-closing-keywordneedsPR_BODY(its package.json script is--self-testonly, which is what CI runs), andnode scripts/check-test-completeness.mjsneeds a real CI log (CI runs it inside Test Core). Three gates first exited 1 with aPREREQUISITE NOT METrefusal because this worktree had nonode_modulesyet — read as NOT MEASURED, not red; all three went green afterpnpm install.check:i18nandcheck:i18n-coverageeach refused first for an unbuilt closure and both went green once the closure they named was built.ESLint — narrowed, with the three things a narrowing owes. Instead of repo-wide
pnpm lint:scripts/from its config, not from my guess, and that is every file in the tree my diff touches plus 198 more.--format json, not from console text.eslint.config.mjswhich never enables type-aware linting (noparserOptions.project, no typed@typescript-eslintrules) for any file — stated ateslint.config.mjs:327-329and measured there with a positive control. So no verdict on an untouched file can move because of this diff.Control bytes:
check:nul-bytesexit 0 (7594 files scanned), plus a directgrep -naPcontrol-character scan over the six changed files — clean.Changeset:
skip-changeset, and why this contradicts the dispatch orderThe dispatch says a changeset is required. Measured, this repo offers no legal changeset for this diff:
private: truewith nofiles, and@objectstack/lint— the only package with importers in this diff's blast radius — publishes["dist","README.md","CHANGELOG.md"], sopackages/lint/scripts/is not in its tarball either. A changeset naming a package would manufacture a release whose tarball is byte-identical.scripts/check-empty-changeset.mjsrefuses a PR that newly adds one, and its header carries the measurement (an empty changeset is a real input tochangesets/actionand can stall a release; the label produces no input at all).So
skip-changesetis the route, applied to this PR. Noted rather than done silently, because the diff is not a clean fit for that label's own closed path list either: four of the six files are rootscripts/*.mjsrather thanscripts/pm/**. The label's criterion — publishes nothing from any package — is satisfied outright. One label removal plus one file reverses this if the PM reads it differently.Generated by Claude Code