Uh oh!
There was an error while loading. Please reload this page.
fix(scripts): close the bash 4.0 operator gap in check-bash32-floor's denylist - #12764
Merged
Conversation
check-bash32-floor's construct table carried `&>>` (append-both) but not
`|&` (pipe-both) — same bash release, same failure class, one caught and
one not. A denylist's absences read as approvals, so this sweeps the whole
bash 4.0 operator set rather than adding the one entry:
pipe-both `|&` hard syntax error on 3.2
case-fallthrough-next `;&` hard syntax error on 3.2
brace-increment `{x..y..incr}` SILENT — left literal, exit 0
bashpid `$BASHPID` unbound (the 4.0 sibling of epoch-vars)
Versions read off the bash NEWS text, not asserted. The operators left out
(`**`, `test -v`/`[ -v ]`, new flags on already-refused builtins, post-4.0
variables) are now written down in the header with their reasons, so the
next reader inherits the list instead of re-deriving it.
The `;&` row carries a lookbehind because `;;&` contains `;&`; both
directions of that disjointness are pinned. A new coverage floor makes row
DELETION loud: every other leg is parameterised by the table, so removing a
row removed its own tests and the suite stayed green.
15 -> 19 constructs; self-test 98 -> 130 cases; real tree still 0 findings.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PfaSTikked61BkcsB5Rn69
This was referenced Aug 27, 2026
os-zhuang
marked this pull request as ready for review
August 27, 2026 17:25
os-zhuang
enabled auto-merge
August 27, 2026 17:25
Uh oh!
There was an error while loading. Please reload this page.
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 freeto 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#12634
check-bash32-floor.mjsrefused&>>(append-both) but not|&(pipe-both) — same bash release, same class, one caught and one not. Because the table is a denylist, that absence read as an approval. Triage set the scope wider than the one entry for exactly that reason, so this sweeps the whole bash 4.0 operator set in one pass.Premise, re-derived on this base rather than inherited
The dispatch brief carried a reading; it was re-measured here on
168941cea, with the control that makes the zero mean something:The absence is real, not a grep that could not match.
What the sweep adds
pipe-both|&case-fallthrough-next;&brace-increment{x..y..incr}bashpid$BASHPIDset -u, otherwise EMPTY15 to 19 constructs. Two things worth calling out:
Versions were read, not asserted. The dispatch brief's own assumption — that
|&is 4.0 and its 3.2 behaviour is a hard syntax error, louder than&>>merely backgrounding — was checked against the bash NEWS text rather than taken on faith, and it holds:|&is "a synonym for2>&1 |",;∧;&are the two new case terminators,$BASHPIDis "a new variable", all four under 4.0. The file's header records that these four rows differ from the pre-existing tier-2 rows in exactly that respect.The sweep found a construct whose 3.2 failure is quiet, and the entry says so.
{x..y..incr}is not an error on a shell that cannot parse it — bash leaves any unparseable sequence expression literal. Measured on this host with a deliberately invalid increment:echo {1..10..x}prints back its own ten characters. So the 3.2 symptom is a loop that runs exactly ONCE over a nonsense value at exit 0. The row'sbreakstext encodes that distinction the wayappend-bothalready encodes "BACKGROUNDED".bashpidis the one row here that is a variable rather than an operator. It is included deliberately: it is the last remaining bash 4.0 addition absent from this table, it is a one-row mirror of the existingepoch-varsdeclaration, and leaving it out would have re-paid this card's cost on the next pass — which is the precise thing triage asked to stop. Flagged here rather than slipped in.What is deliberately NOT in the table, and why
Now recorded in the gate's own header so the next reader inherits the list instead of re-deriving it:
**(globstar) — not a construct on its own. Withoutshopt -s globstar,**is two ordinary globs and legal on 3.2, so what is refusable is the option, and theshopt-4row already refuses it. A literal**token would also fire on arithmetic exponentiation and on every doubled asterisk in afindargument.test -v/[ -v ]— a real hole, and NOT closed here.[ -vis also the opening of an ordinary bracket expression (tr -d '[ -v]'is the character range space-to-v), so widening thehas-vrow needs a false-positive judgement this sweep did not take, and a wrong one reddens the tree on correct 3.2 code. Filed separately and out of scope for this PR: check-bash32-floor'shas-vrow sees only the[[ -v ]]spelling —test -vand[ -v ]pass #12760.mapfile -d,readarray -C) — abuiltinrow refuses its builtin at every flag.BASH_XTRACEFD,BASH_ARGV0,SRANDOM,PROMPT_DIRTRIM) — outside the 4.0 line this sweep drew, named in the header so they are a written worklist rather than silent approvals.Two patterns that carry a judgement, both pinned
;&needs a lookbehind.;;&contains;&, so a bare token double-flags every;;&line and half of each pair names the wrong construct and the wrong repair. The row's token is(?<!;);&, and the disjointness is pinned in both directions — the ablation below shows why: stripping the lookbehind reds only the;;&case, so a one-sided pin would have passed.brace-incrementis tighter than its siblingsyntaxrows. A loose "two dot-runs inside braces" pattern matches an ordinary brace LIST of relative paths —cp {../a,../b} .— so the token requires a real sequence-expression shape. Pinned both ways.The negative half, which is the load-bearing one
A
|&pattern that also matched2>&1 |would redden every correct pipeline in the repo — the gate refusing the very remedy its failure text prescribes. Pinned:t('`2>&1 |`, the replacement `|&` is a synonym FOR, is not flagged', ...), plus an ordinary pipe, an ordinary background&,{1..10},{a..z}, and the relative-path brace list.A coverage floor, because deleting a row was silent
Every existing leg iterates
CONSTRUCTS, so deleting a row deleted its own tests and the suite stayed green — the same species this gate exists to refuse, one level up, in the instrument instead of the tree. Five new cases are not parameterised by the table: they name real lines and require that something still refuses each. Written against behaviour rather than ids, so renaming a row is free and removing its coverage is loud.&>>is in that list as the control.Evidence
End to end, both directions, one fixture tree, five bash-4.0 constructs on five lines:
Ablation — every pin added, mutate to RED, restore to GREEN. Restores are
git checkout HEAD -- ABS_PATH(never bare, which reads a polluted index); each mutation proved on disk by counting the anchor before and after, never by the editor's exit code; each restore proved bygit hash-objectequality against the HEAD blob plus an emptygit diff HEAD, with an empty hash treated as failure. The harness carries atrap ... EXIT INT TERM, used as a crash convenience only, not as proof.Gate union, run at
1a3bee27c— the final commit on this branch:The gate's own verdict lines, quoted rather than an exit code:
The list was re-derived for the actual diff (
node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack, which reads its own changeset from the merge base) and matched the brief's nine families with nothing added. The two convention-triggered obligations the tool names for a gate-script edit —bare-root-worklist --self-testandcheck:pm-dispatch-gates— were run and are green; neither needed a ledger row, becausePOPULATION_ROOTSis untouched by this diff.check-ci-filter-parity.mjsfirst exited 1 withPREREQUISITE NOT MET — the dependency yaml is not installed. That is a missingpnpm installin a fresh worktree, not a finding; it is green above after installing.Declared narrowing: repo-wide
pnpm lintwas not run locally. eslint was run on the changed file only (--no-inline-config --format json: 1 file, 0 errors, 0 warnings). CI runs the full farm regardless, and this is a declared narrowing rather than a proven one — no claim is made here about untouched files.Notes
|&occurrences already exist in the population, all inscripts/pm/os-verify-lock.shand all in full-line comments, so E1 exempts them and the tree stays at 0 findings. That was checked before writing the pattern, not after.scripts/is not a published package, so this ships nothing to consumers.skip-changesetapplied.bash4Constructslist insidescripts/objectui-changeset-digest.mjscarries a narrower construct set than this table. It is not a coverage hole — the repo-wide gate coversscripts/bump-objectui.shwith a superset — so no issue was filed for it.Generated by Claude Code