#119's class check asserts the swept set covers `git ls-files '*.sh'`.
bin/cast has no `.sh` extension: it enters the set through the shebang
scan, so it is covered by the DERIVATION and not by the ASSERTION. Break
or delete that scan and the shipped entrypoint drops out of the lint
while the check still exits 0 — #118's failure mode (a sweep quietly
narrowing while CI stays green) one level in from where #119 closed it.
There is no non-circular way to re-derive "every extensionless shell
script" inside the script; any second derivation would be the same
shebang scan and would break with it. So the floor is named rather than
computed: `required=(bin/cast)`, asserted present in the swept set. A
rename turns it red, which is correct — the floor is the thing that has
to be updated deliberately. A minimum-count assert was considered and
declined: given the *.sh class check already floors the set, a count
floor's only marginal coverage is "at least one extensionless script
exists", which the named floor states more precisely and with a better
error message, and it would churn on every script added or removed.
Proven to bite. With the shebang allowlist stubbed to match nothing, the
*.sh class check still PASSES and the new floor fails:
shellcheck-all: 'bin/cast' is not in the swept set
it has no .sh extension, so it enters only via the shebang scan above —
that scan is broken, or the file moved. See #121.
Reverted, the sweep is green over 8 scripts again.
Also fixed, from the same review: `IFS= read -r line <"$f" || continue`
skipped any file whose FIRST line lacked a trailing newline, because
`read` returns 1 at EOF even when it populated `line`. A shebang-only
file with no final newline was silently unswept. Now
`|| [ -n "$line" ] || continue`, which falls through on a populated
partial read and still skips genuinely empty files. Measured against a
tracked 9-byte `#!/bin/sh` with no final newline: the fixed scan sweeps 9
scripts including it, the old line sweeps 8 and omits it silently.
Closes#121
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Filed as cast's record of heavy-duty/box#116: a `shopt -s globstar;
files=(bin/* **/*.sh)` sweep never descends into `.github/`, because globs
do not match dot-prefixed names without `dotglob`. cast has no such sweep —
it has no shellcheck step at all. Its only shell gate was
bash -n install.sh bin/cast scripts/*.sh .github/scripts/*.sh
a syntax check over a hand-maintained list. The reported symptom holds
(release-notes.sh and labels-reconcile.sh ship unlinted) but so does every
other script here, and `bash -n` parses without linting: it would not catch
a quoting or unset-variable bug in any of them.
.github/scripts/shellcheck-all.sh now runs `shellcheck -x` over the tracked
tree, from CI and from `npm run check:shell`. The file list comes from
`git ls-files`, not a glob. `dotglob` was measured and does work today —
cast's dependency tree ships zero `.sh` files, so sweeping after `npm ci`
pulls in nothing — but that is a property of somebody else's package tree,
re-decided by every install. `git ls-files` does not depend on it.
Extensionless scripts are matched by shebang, which covers bin/cast without
naming it.
It carries a class check in box#112's shape: the sweep asserts its own list
covers `git ls-files '*.sh'` and fails naming the strays otherwise. Verified
by swapping the derivation for the buggy globstar glob, which reports
exactly the two .github/scripts files.
All eight scripts pass as they stood; the three findings were intentional
($PATH written literally into a profile, advice text in backticks) or a
false positive, and are annotated in place. No behavior changes.
Refs #118