Skip to content

fix: a shebang and the execute bit go together, in every directory that documents a command (#856) - #857

Merged
jdatcmd merged 4 commits into
commandprompt:mainfrom
OffgridwithJD:fix/856-documented-entry-points
Sep 1, 2026
Merged

fix: a shebang and the execute bit go together, in every directory that documents a command (#856)#857
jdatcmd merged 4 commits into
commandprompt:mainfrom
OffgridwithJD:fix/856-documented-entry-points

Conversation

@OffgridwithJD

Copy link
Copy Markdown
Collaborator

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/. Measuring
turned up a third thing that changes the shape, so this branch does more than
either of us proposed on the issue:

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 the
exemption 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_verdict reddened 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:

THE WAY OUT, IF A FUTURE SCRIPT MUST NOT BE EXECUTABLE, IS TO DROP ITS SHEBANG
AND SAY WHY IN ITS HEADER

That way out does not exist. Dropping the shebang moves the file from noexec to
noshebang. Inside the swept population there was no correct state for a
file that must not be executable; the only escape was the directory exemption.

bench/cb_guards.sh is the file that proves it. Sourced by
bench/run_clickbench.sh:176 and test/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.py was given a shebang in #853. It is invoked
as python3 .../crlf_listener.py and was never a documented bare command. It
needed no interpreter line; it got one to satisfy a check that demanded one from
every file in test/.

CONTEXT.md had the rule right before the code did

This 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:

harness_selftest sweeps every .sh and .py under test/, at any depth,
and fails if one has either without the other.

"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.  declares an interpreter  =>  must be executable      (#852's defect, unchanged)
B.  is executable            =>  must declare one        (CHANGED: was "no shebang => red")
C.  a document names it bare =>  must be executable      (NEW)

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:

test/selftest/    31 scripts    ok=31   noexec=0   nodecl=0
test/fixtures/    14 .sh/.py    ok=1    noexec=13  nodecl=0

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 ok and 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 population

docs/benchmarks.md names five bench/ scripts as bare commands:

benchmarks.md:7    BENCH_DUCKDB=1 bench/run_bench.sh ...
benchmarks.md:8    bench/run_bench_fsst.sh ...
benchmarks.md:9    bench/run_bench_readstream.sh ...
benchmarks.md:748  BENCH_SCALE=20000000 bench/run_bench_join.sh ...
benchmarks.md:818  PGC_CB_ROWS=10000000 bench/run_clickbench.sh ...

All five are 100755 today, so all five work — correct by habit, with nothing
checking it, which is exactly what test/ was before #852. The population is now
"every directory that holds a documented entry point", today test/ and
bench/.

Red before green, both arms run

arm result
unfixed tree + the revised guard 189 PASS, 1 FAIL, naming all fourteen
fixed 190 PASS, 0 FAIL
FAIL  every script that declares an interpreter is executable:
      got [[14: bench/cb_guards.sh test/fixtures/iceberg/crosscheck_dv.py
      test/fixtures/iceberg/crosscheck_eq_duckdb.py
      test/fixtures/iceberg/crosscheck_nm.py
      test/fixtures/iceberg/gen_delete_fixture.py]] want [[]]

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

mutation reds which check
cb_guards.sh regains its shebang 1 A, naming it
one fixture host tool loses its bit 1 A — proves fixtures/ is swept now
a sourced selftest part gains the bit 1 B — proves selftest/ is swept
a documented command loses shebang and bit 1 C alone; A and B blind by construction
population points at an empty directory 5 the premises
bench/ dropped from the population 1 the bench/ premise alone

The fourth is the one worth reading: test/temporal.sh with neither a shebang
nor 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.sh is a
sourced fragment, so it loses its shebang and keeps 100644. The thirteen
are programs, so they take the bit. Both then satisfy the biconditional.

Gate

preflight build_all_versions, five majors   built 5 of 5, 0 warnings, PASSED
PG18a   235 ran, 2 skipped, 0 FAIL
PG19a   237 ran, 0 skipped, 0 FAIL
versions run: 2 of 2 configured        ALL VERSIONS PASSED, 0 suite failures
harness_selftest                       190 checks, 190 PASS, 0 FAIL

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 and
reverted 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 verdict
controls, six premises, three checks), bench/cb_guards.sh loses one line,
thirteen mode changes, CHANGELOG.md and CONTEXT.md.

No docs/ change, deliberately: nothing user-facing moves and no documented
command changes. This is a contributor rule and it lives in CONTEXT.md beside
the build rules, where #852's already was.

🤖 Generated with Claude Code

https://claude.ai/code/session_014SNUrAGntEBQXuhcK2qeo9

OffgridwithJD and others added 4 commits August 31, 2026 22:24
…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
@OffgridwithJD

Copy link
Copy Markdown
Collaborator Author

Head is 9794d52, three commits past the one this PR opened with. Both of
jdatcmd's review findings are folded in and proven, and one hole I measured
myself after he named the class. 192 checks, 192 PASS.

Run the arms against this head, not 6d8997c.

What changed since the PR body was written

1. Check C's premise was a total, and a total hides the narrowing that matters.
He said he would look at whether C's population can silently narrow. It can.
Measured:

documents name 83 distinct scripts:   76 under test/,  7 under bench/
delete docs/benchmarks.md  ->  bench/ drops to 0
                               total still 75, floor 40  ->  PREMISE STAYS GREEN

So C could stop covering bench/ entirely — the directory this PR exists to add
— with nothing red. The premise is now per-directory: a directory is swept
because it holds documented entry points, so the documents must still name one
in it.

mutation result
delete docs/benchmarks.md premise reds, got [[ bench]]
add a swept directory documenting nothing (tools/) premise reds, got [[ tools]]

The second is the half I would not have written unprompted. A directory cannot
enter the sweep on an assumption that it holds documented commands — the premise
checks the reason the directory is in the population at all, which is the
question anyone extending this to a third directory will need answered.

2. A documented script that does not exist was skipped, silently. His arm:
append a fenced test/does_not_exist.sh /path/to/pg_config to docs/testing.md
and the suite stayed green, because C did [ -f ] || continue, commented "a
document may name a path that moved".

I took the make-it-red branch rather than documenting the skip. C's entire
justification is that the command in the manual does not run, and
No such file or directory is #852's defect in a different coat. C is now two
assertions:

FAIL  and every script a document names exists: got [[1: test/does_not_exist.sh]]
FAIL  and every script a document names is executable: got [[1: test/temporal.sh]]

He was right that this changes what C is for, so CONTEXT.md says both halves:
a named script must exist and be executable. All 83 named paths resolve
today, so this is a guard rather than a fix.

3. A comment describing a filter the code does not implement. C's header 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 is counted, the one occurrence of that shape today. The
behaviour is right and over-inclusive on purpose; the sentence was wrong. Same
class as the fixtures rationale on #853, corrected the same way.

4. The accepted slack is now in the file with its number, not only here: 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.

Gate at 9794d52

preflight build_all_versions, five majors   built 5 of 5, 0 warnings, PASSED
PG18a   235 ran, 2 skipped, 0 FAIL
PG19a   237 ran, 0 skipped, 0 FAIL
versions run: 2 of 2 configured        ALL VERSIONS PASSED, 0 suite failures
harness_selftest                       192 checks, 192 PASS, 0 FAIL

The mutation arms at this head, all ten

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 jdatcmd left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@jdatcmd
jdatcmd merged commit 74d0263 into commandprompt:main Sep 1, 2026
12 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Fourteen files declare an interpreter and cannot be run, and the #852 guard does not sweep bench/ where five documented commands live

2 participants