fix: a shebang and the execute bit go together, in every directory that documents a command (#856) - #857
Conversation
…at documents a command (commandprompt#856) commandprompt#852 gave every documented test script its execute bit. Two things were left, and the second is why the first had no clean answer. THE RULE FLAGGED BOTH STATES, SO A SOURCED FRAGMENT COULD NOT BE CORRECT. The guard reddened a file that declared an interpreter without the bit, and it also reddened a file with no interpreter line at all. Its header documented the way out as "drop its shebang and say why", and that way out did not exist: dropping the shebang moved the file from one red to the other. bench/cb_guards.sh is the file that proved it -- sourced by bench/run_clickbench.sh:176 and test/bench_guards.sh:43, its header saying "Sourced, not executed" since it was written, and unfixable under the old rule. The rule is now a biconditional. A file that declares an interpreter must be executable, and a file that is executable must declare one. A file with neither is a fragment meant to be sourced, and passes. AND THAT REMOVED THE EXEMPTIONS. test/selftest/ and test/fixtures/ were pruned by path because the old rule would have reddened them wholesale. Measured under the new one before the prune came out: selftest/ is 31 scripts and every one is already correct, fixtures/ is 14 .sh/.py of which 13 are the host tools this commit gives the bit. Nothing is excluded now, so nothing is concealed -- the thirteen the exemption used to hide are inside the population and the suite reddens if one loses its bit. bench/ JOINS THE SWEPT POPULATION. docs/benchmarks.md names five bench/ scripts as bare commands -- run_bench.sh, run_bench_fsst.sh, run_bench_readstream.sh, run_bench_join.sh and run_clickbench.sh. All five are executable today, so all five work: correct by habit with nothing checking it, which is exactly what test/ was before commandprompt#852. A THIRD CHECK, ANCHORED ON THE DOCUMENTS, CLOSES THE HOLE THE BICONDITIONAL OPENS. Delete a documented command's shebang AND its bit and the file is internally consistent and still broken for the reader, so neither of the first two rules can see it. The third reads what the documents tell a reader to type and requires those files to be executable. It is deliberately over-inclusive: the cost of a false positive is one execute bit on a file that is an entry point anyway. Red before green, both arms run. Unfixed, the guard names all fourteen files; fixed, 190 of 190. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014SNUrAGntEBQXuhcK2qeo9
CONTEXT.md had it right before the code did: it already described the rule as failing "if one has either without the other", which is the biconditional this branch implements. The code was stricter than the documented rule, and that gap is what left a sourced fragment no way to be correct. The bullet now also says the population is every directory that documents an entry point -- today test/ and bench/ -- that nothing is excluded, and that a script a document names must be executable whatever its first line says. No docs/ change: nothing user-facing moves and no documented command changes. This is a contributor rule and it lives in CONTEXT.md beside the others. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014SNUrAGntEBQXuhcK2qeo9
…rrowing (commandprompt#856) jdatcmd said in review that he would look at whether check C's population can silently narrow, since that is the objection commandprompt#853's own header raises against a prose-derived population. It can, and the measurement is worse than the objection: the documents name 83 scripts, 76 under test/ and 7 under bench/. Delete docs/benchmarks.md and the bench/ half goes to ZERO while the total is still 75 -- over any floor, so the premise stayed green and C silently stopped covering the directory commandprompt#856 added it for. A total is the wrong premise for this check. It is now per-directory, and it is the population's own definition turned into an assertion: a directory is swept BECAUSE it holds documented entry points, so the documents must still name one in it. Two arms, both red where the old premise was green: delete docs/benchmarks.md premise reds, got [[ bench]] add a swept directory documenting nothing (tools/) premise reds, got [[ tools]] The second is the better half. It means a directory cannot be added to the sweep on somebody's assumption that it holds documented commands: the premise checks the reason the directory is in the population at all. A single deleted line still narrows C by one script and stays green, and that is accepted rather than overlooked. C is an additional check over A and B, not the population, so a one-script narrowing weakens C alone and cannot make either sweep pass falsely. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014SNUrAGntEBQXuhcK2qeo9
…xtraction (commandprompt#856) Two findings from jdatcmd's review, both measured on the branch rather than read off it. A DOCUMENTED PATH THAT DOES NOT EXIST WAS SKIPPED, SILENTLY. Check C did `[ -f ] || continue`, commented "a document may name a path that moved". He appended a fenced line naming test/does_not_exist.sh to docs/testing.md and the suite stayed green. A reader who types that command gets "No such file or directory" instead of "Permission denied", which is commandprompt#852's defect wearing a different error message -- and C is the check whose whole justification is that the command in the manual does not run. The skip is now a check. All 83 named paths resolve today, so this is a guard rather than a fix, and C is two assertions instead of one: a named script must exist, and it must be executable. CONTEXT.md says both halves now. AND THE COMMENT DESCRIBED A FILTER THE CODE DOES NOT IMPLEMENT. It said the extraction counts any path "not directly preceded by an interpreter". There is no such exclusion: the leading character class admits a space, so `bash test/smoke.sh` in docs/testing.md is counted like any other, which is the one occurrence of that shape today. The behaviour is right -- over-inclusive is the point -- and the sentence was wrong, which is the same class as the fixtures rationale on commandprompt#853 and worth the same correction. The accepted slack is now stated in the file with its number rather than only in a pull request body: 43 over the floor, so deleting one line narrows C by one script and stays green. C is additive over A and B, so that weakens C alone and cannot make either sweep pass falsely. The narrowing that matters, a whole document going and taking a directory's coverage with it, is what the per-directory premise catches. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014SNUrAGntEBQXuhcK2qeo9
|
Head is Run the arms against this head, not What changed since the PR body was written1. Check C's premise was a total, and a total hides the narrowing that matters. So C could stop covering
The second is the half I would not have written unprompted. A directory cannot 2. A documented script that does not exist was skipped, silently. His arm: I took the make-it-red branch rather than documenting the skip. C's entire He was right that this changes what C is for, so 3. A comment describing a filter the code does not implement. C's header said 4. The accepted slack is now in the file with its number, not only here: 43 Gate at
|
| mutation | reds | which |
|---|---|---|
cb_guards.sh regains its shebang |
1 | A |
| a fixture host tool loses its bit | 1 | A — proves fixtures/ is swept |
| a sourced selftest part gains the bit | 1 | B — proves selftest/ is swept |
| a documented command loses shebang and bit | 1 | C-executable alone; A and B blind |
| population points at an empty directory | 5 | premises |
bench/ dropped from the population |
1 | the bench/ premise |
delete docs/benchmarks.md |
1 | the per-directory premise |
| add a swept directory documenting nothing | 1 | the per-directory premise |
| a document names a script that does not exist | 1 | C-exists |
| a documented command loses only its bit | 2 | A and C-executable together |
The last is not an isolation failure: temporal.sh keeps its shebang there, so A
fires too. Arm 4 is the one that isolates C.
One process note, because it nearly produced a false green twice
My first mutation run silently tested the old rule from arm 2 onward: the
restore step ran git checkout -- . against an uncommitted guard and reverted
it. The tell was the check count falling 190 → 186, not any verdict — the
reverted guard reported plain green and would have read as "this check cannot
fail". It happened a second time with the per-directory premise, from the same
reused restore().
The rule is not "watch the count", it is commit before mutating. The harness
now asserts the committed check count on every arm, so a guard that reverts under
it cannot pass unnoticed.
jdatcmd
left a comment
There was a problem hiding this comment.
Re-reviewed at 9794d52. Approving. Ten arms run here, each asserting checks run: 192 — the count you gave me rather than one I inferred, and it held on all ten, so no arm read a stale tree or a reverted guard.
0 baseline 192 fail=0
1 fixture host tool loses its bit 192 fail=1 A: gen_iceberg_day.py -> fixtures/ IS swept
2 sourced selftest part gains the bit 192 fail=1 B: selftest part 310 -> selftest/ IS swept
3 documented command loses shebang AND bit 192 fail=1 C-executable ALONE: temporal.sh
4 cb_guards.sh regains its shebang 192 fail=1 A, in bench/
5 bench/ dropped from the population 192 fail=1 premise: bench/ in population
6 documented command keeps shebang, loses bit 192 fail=2 A and C together, correctly
7 a document names a script that is gone 192 fail=1 C-exists: does_not_exist.sh
8 docs/benchmarks.md deleted 192 fail=1 premise, naming [ bench]
9 a swept directory documenting nothing 192 fail=1 premise, naming [ tools]
Arm 7 is the one I care about, because it is the same probe that was green four hours ago. At 6d8997c I appended test/does_not_exist.sh /path/to/pg_config to docs/testing.md and the suite reported 190 checks, 0 FAIL. At this head the same edit reds and every script a document names exists, naming the file. That is a fix proved by the thing that exposed it, which is the only kind of proof I trust from myself.
Arms 8 and 9 verify your own finding, and 9 is the better half. Deleting docs/benchmarks.md took bench/ coverage to zero while the old total-based premise stayed green over its floor of 40 — the directory #856 exists to add could have stopped being covered silently. Arm 9 goes further than that: a directory cannot enter the sweep on somebody's assumption that it documents commands. That is the premise checking the reason the population has the shape it has, and it is the question anyone extending this to a third directory will need answered.
On the scope you flagged in your first paragraph. You were right to state it rather than let me find it, and I reviewed the larger cut because the premise justifying it checked out: on main, _tsm_verdict returned noshebang whatever the mode, so a file with neither a shebang nor the bit was red and the header's documented escape moved it from one red to the other. CONTEXT.md already said "fails if one has either without the other". The code was stricter than the rule the project had documented, and closing that gap is smaller than redefining anything. cb_guards.sh losing its shebang while 100644 — a fragment saying so in the only self-consistent way — is the shape of the whole change.
Two things I raised and both are closed: C's comment now matches its extraction, and a named path that does not exist is a red rather than a silent continue.
CI 12 of 12 at this head, headSha matched. I am not merging this. The owner's merge approval covered #851, #853 and #855 by name; this is a new PR and needs his word, and I would rather ask than assume an approval extends.
Closes #856.
Read this part first: the scope grew past the issue, and past what was agreed
#856 as filed asked for two things — resolve fourteen files that declare an
interpreter and cannot be run, and extend the #852 sweep to
bench/. Measuringturned up a third thing that changes the shape, so this branch does more than
either of us proposed on the issue:
test/selftest/300-..., which you approved andmerged in fix: every test script is executable, so documented commands run (#852) #853 four hours ago;
test/selftest/andtest/fixtures/— thatyou reviewed and that I twice wrote justifications for;
against as a population.
Every one of those is defended below with a measurement, and I think the change
is right. But it is a larger claim on your review than "extend the sweep", and
you should have it stated rather than find it in the diff.
If you would rather have less: keep the biconditional and
bench/, drop theexemption removal, and leave the thirteen fixture files alone. That is roughly
half the diff and touches nothing you reviewed. Say so and I will cut it back.
The defect: the rule flagged both states, so a sourced fragment could not be correct
_tsm_verdictreddened a file that declared an interpreter without the bit(
noexec), and it also reddened any file with no interpreter line(
noshebang), whatever its mode. The header documented the escape:That way out does not exist. Dropping the shebang moves the file from
noexectonoshebang. Inside the swept population there was no correct state for afile that must not be executable; the only escape was the directory exemption.
bench/cb_guards.shis the file that proves it. Sourced bybench/run_clickbench.sh:176andtest/bench_guards.sh:43, its header has said"Sourced, not executed" since it was written, and it is unfixable under the old
rule: it cannot keep its shebang, and it cannot drop it.
It is also why
test/crlf_listener.pywas given a shebang in #853. It is invokedas
python3 .../crlf_listener.pyand was never a documented bare command. Itneeded no interpreter line; it got one to satisfy a check that demanded one from
every file in
test/.CONTEXT.mdhad the rule right before the code didThis is the part that turns "I am redefining your rule" into "I am making the
code match what the project already documented".
CONTEXT.md, unchanged since#852:
"Either without the other" is the biconditional. The code was stricter than the
documented rule, and that gap is the whole defect.
The rule now
A file with neither a shebang nor the bit passes. It is a fragment meant to
be sourced, and that is the only self-consistent way to say so.
C is anchored on prose, and that is a deliberate exception rather than a
lapse. #853 rejected a documentation-derived list as the population, and it
was right: delete a line from a document and the sweep silently narrows. As an
additional check it cannot create a false green for A or B, and it is the only
one that states #852's defect directly — the command in the manual does not run.
It closes the hole B opens: strip a documented command's shebang and its bit
and the file is internally consistent and still broken for a reader, so A and B
are both blind to it. It is over-inclusive on purpose; the cost of a false
positive is one execute bit on a file that is an entry point anyway.
Removing the exemptions, and why that is now safe
The prune existed because the old rule would have reddened both directories
wholesale. Under the biconditional it is unnecessary. Measured, both directories,
before the prune came out:
The thirteen are the fixture host tools — three crosschecks that document
V/bin/python <path>, ten generators recording how a committed fixture was made.This branch gives them the bit, which puts them at
okand lets the prune go.That also answers an objection I had to my own plan. Fixing those thirteen while
they sat in an exempt directory would have been a change no check covers.
Removing the exemption is what makes them testable — mutation 2 below takes the
bit off one of them and the suite reddens.
bench/joins the populationdocs/benchmarks.mdnames fivebench/scripts as bare commands:All five are
100755today, so all five work — correct by habit, with nothingchecking it, which is exactly what
test/was before #852. The population is now"every directory that holds a documented entry point", today
test/andbench/.Red before green, both arms run
Repo-wide after: 0 files with a shebang and no bit (was 14), 0 executable
without a shebang.
Six mutations, each reddening exactly one thing
cb_guards.shregains its shebangfixtures/is swept nowselftest/is sweptbench/dropped from the populationbench/premise aloneThe fourth is the one worth reading:
test/temporal.shwith neither a shebangnor a bit is internally consistent, so A and B say nothing, and only C reports it.
The two fixes are split by kind rather than by directory.
cb_guards.shis asourced fragment, so it loses its shebang and keeps
100644. The thirteenare programs, so they take the bit. Both then satisfy the biconditional.
Gate
One process note, because it nearly cost a false green
My first mutation run silently tested the old rule from mutation 2 onward:
the restore step ran
git checkout -- .against an uncommitted guard andreverted it. The tell was the check count falling 190 → 186, not the verdicts —
mutation 2 simply reported green and would have read as "this check cannot fail".
I committed before re-running, and the mutation harness now asserts the check
count on every arm, so a guard that reverts under it cannot pass unnoticed.
Contents
test/selftest/300-...rewritten (the rule, the population, four verdictcontrols, six premises, three checks),
bench/cb_guards.shloses one line,thirteen mode changes,
CHANGELOG.mdandCONTEXT.md.No
docs/change, deliberately: nothing user-facing moves and no documentedcommand changes. This is a contributor rule and it lives in
CONTEXT.mdbesidethe build rules, where #852's already was.
🤖 Generated with Claude Code
https://claude.ai/code/session_014SNUrAGntEBQXuhcK2qeo9