Found while sweeping for #12071 (PR #12142). That card repaired scripts/bump-objectui.sh; the sweep its dispatch asked for turned up three more scripts in the same class. Filed rather than fixed — the remaining three are outside that card's declared file surface, and their verification surfaces are not the one that card's gate covers.
Measurement
Swept all 17 tracked *.sh files in the repo for the bash-4/5 construct set (mapfile, readarray, declare -A/local -A/typeset -A, ${x^^}/${x,,}, &>>, EPOCHSECONDS/EPOCHREALTIME). Three carry one, all the same builtin pair:
scripts/gen-sdui-manifest.sh:585 readarray -t DUMP_DEV_ARGV < <(sdui_dev_server_cmd "$DUMP_PORT")
scripts/pm/os-regen-merge.sh:68 mapfile -t regen_paths < <(grep 'merge=os-regen' .gitattributes | awk '{print $1}')
scripts/publish-smoke.sh:674 mapfile -t DEV_ARGV < <(smoke_dev_server_argv)
mapfile/readarray are bash 4 builtins. /usr/bin/env bash is bash 3.2.57 on macOS, so on a Mac each line is command not found, status 127. All three files run set -e or equivalent, so this aborts the run rather than degrading it.
The other 12 .sh files are clean, as is scripts/bump-objectui.selftest.sh. scripts/pm/os-verify-lock.sh mentions the constructs only in its portability docblock and in the self-test that hunts them — it is the repo's existing authority on this floor and is clean by construction.
Why gen-sdui-manifest.sh is the sharp one
It is not an incidental sibling: it is the next step of the very procedure #12071 was about. bump-objectui.sh ends by printing
→ NEXT STEP — run the declaration-parity ratchet (ADR-0082 D4):
pnpm sdui:manifest
and pnpm sdui:manifest is scripts/gen-sdui-manifest.sh. Per that script's own comment the ratchet "is an on-demand gate by decision (#5960), never a CI job" — i.e. it has no CI path at all and is only ever run by hand, on an operator's laptop. So a maintainer bumping the pin from a Mac now gets past the bump (that half is repaired) and hits readarray: command not found at the step the bump just told them to run. Neither line is reachable from any CI run: CI is Linux with bash 5, where both builtins exist.
os-regen-merge.sh is the agent-facing merge-regen helper; publish-smoke.sh is release publish smoke. Both are also hand-run rather than CI-scheduled, but neither sits inside the documented macOS operator procedure the way gen-sdui-manifest.sh does.
Not verified here
I did not run any of the three on a shell without the builtins, and I did not check whether each surrounding function has other bash-4 dependencies. The grep above is a construct census, not a proof that a bash-3.2 run of each script otherwise succeeds. Sizing this card should start by running each under a BASH_ENV file containing enable -n mapfile readarray — that reproduces the macOS symptom exactly on Linux (measured on #12071: byte-identical mapfile: command not found / status 127).
Shape of the remedy, if it is wanted
#12071 landed the pattern: the bash-3.2 read loop already idiomatic in scripts/pm/os-verify-lock.sh, plus a static scan asserting the script names no bash-4-only construct. The static scan is the part worth generalizing — #12071's version is scoped to one file inside one gate's self-test. A single repo-wide check: over all tracked *.sh would cover this class permanently and would have caught all four sites at once, but that is a gate-shaped decision rather than a mechanical edit, which is the other reason this is filed rather than folded into that PR.
Found while sweeping for #12071 (PR #12142). That card repaired
scripts/bump-objectui.sh; the sweep its dispatch asked for turned up three more scripts in the same class. Filed rather than fixed — the remaining three are outside that card's declared file surface, and their verification surfaces are not the one that card's gate covers.Measurement
Swept all 17 tracked
*.shfiles in the repo for the bash-4/5 construct set (mapfile,readarray,declare -A/local -A/typeset -A,${x^^}/${x,,},&>>,EPOCHSECONDS/EPOCHREALTIME). Three carry one, all the same builtin pair:mapfile/readarrayare bash 4 builtins./usr/bin/env bashis bash 3.2.57 on macOS, so on a Mac each line iscommand not found, status 127. All three files runset -eor equivalent, so this aborts the run rather than degrading it.The other 12
.shfiles are clean, as isscripts/bump-objectui.selftest.sh.scripts/pm/os-verify-lock.shmentions the constructs only in its portability docblock and in the self-test that hunts them — it is the repo's existing authority on this floor and is clean by construction.Why
gen-sdui-manifest.shis the sharp oneIt is not an incidental sibling: it is the next step of the very procedure #12071 was about.
bump-objectui.shends by printingand
pnpm sdui:manifestisscripts/gen-sdui-manifest.sh. Per that script's own comment the ratchet "is an on-demand gate by decision (#5960), never a CI job" — i.e. it has no CI path at all and is only ever run by hand, on an operator's laptop. So a maintainer bumping the pin from a Mac now gets past the bump (that half is repaired) and hitsreadarray: command not foundat the step the bump just told them to run. Neither line is reachable from any CI run: CI is Linux with bash 5, where both builtins exist.os-regen-merge.shis the agent-facing merge-regen helper;publish-smoke.shis release publish smoke. Both are also hand-run rather than CI-scheduled, but neither sits inside the documented macOS operator procedure the waygen-sdui-manifest.shdoes.Not verified here
I did not run any of the three on a shell without the builtins, and I did not check whether each surrounding function has other bash-4 dependencies. The grep above is a construct census, not a proof that a bash-3.2 run of each script otherwise succeeds. Sizing this card should start by running each under a
BASH_ENVfile containingenable -n mapfile readarray— that reproduces the macOS symptom exactly on Linux (measured on #12071: byte-identicalmapfile: command not found/ status 127).Shape of the remedy, if it is wanted
#12071 landed the pattern: the bash-3.2 read loop already idiomatic in
scripts/pm/os-verify-lock.sh, plus a static scan asserting the script names no bash-4-only construct. The static scan is the part worth generalizing — #12071's version is scoped to one file inside one gate's self-test. A single repo-widecheck:over all tracked*.shwould cover this class permanently and would have caught all four sites at once, but that is a gate-shaped decision rather than a mechanical edit, which is the other reason this is filed rather than folded into that PR.