Skip to content

Commit 0fe38a9

Browse files
docs(devx): state check-bash32-floor's BOUNDARY — the axis is the bash interpreter, not the userland binaries (#18464)
Part of #17141 Comment-only change to one file: `scripts/check-bash32-floor.mjs` gains a header section stating the **boundary** it already keeps. No rule added, no sibling gate built, no per-script pin removed. ## What landed One new header section, inserted between *"What this gate is NOT"* and *"The exemption rule"*. Verbatim (41 added lines, nothing removed): ``` * ## The BOUNDARY: the axis is the bash INTERPRETER, not the userland binaries * * A gate that states its class but not its boundary can be widened by a * well-meaning row, so the boundary is written here rather than left to be * re-derived. This gate's axis is the VERSION OF BASH THAT RUNS A SCRIPT, and * the row schema says so by construction, not by convention: every * `CONSTRUCTS` row carries a `since:` that is a bash version, and every `kind` * is a bash category — a builtin, a piece of syntax, a variable. There is no * value for "an external binary", and a row about one has nowhere to put it. * * So a difference between the GNU and BSD USERLAND BINARIES a script calls — * `mktemp` and where it will accept the `XXXXXX`, `sed -i` and whether it * demands a suffix argument, `readlink -f`, `date` and its flags — is a * DIFFERENT AXIS, and ⛔ it is not a row here. Such a row could not be written * honestly: `mktemp` has no bash version, so its `since:` would have to be a * lie or a special case, and the verdict line this gate prints ends * "constructs checked, floor bash 3.2" — which becomes false advertising the * moment a coreutils-vs-BSD rule joins the table it counts. Widening here * would silently convert a gate that means "this script needs bash 4" into one * that means "this script behaves differently on a Mac": a strictly larger * claim, made by adding a row. * * ⇒ That class wants a SIBLING gate, not a row here — and ⛔ not yet. The * measured population of real userland defects in this repo is ONE: an * `mktemp` template whose `XXXXXX` was not last. It is already fixed and * guarded by a per-script pin, which ⛔ stays — nothing here replaces it. A * shared gate for a population of one is not warranted, and the evidence that * would change that is a SECOND real instance; the place to land it is the * boundary card this paragraph came from (#17141). * * ⚠️ The basis is written out and not just the verdict, on purpose: a boundary * stated without its basis gets re-litigated by the next person who * provisionally leans yes. That is not hypothetical. This exact question — * should this gate grow a BSD/portability rule, so the `mktemp` class is * caught by a shared gate instead of a per-script pin — was asked, was * answered NO, and the seat that answered it had leaned the other way first * and put that on the record: *"measuring is what produced it — I had * provisionally leaned yes."* ⇒ Measuring is the part that is reproducible. * Read any row's `since:` and `kind:` before re-opening this; ⛔ do not * re-litigate it from intuition. ``` ## Why it says what it says — the schema argument, re-measured here Every number below was taken by this seat in this worktree, **2026-09-16 13:17Z–13:39Z**, base `e915c190e`, tip `a282182f4`. The card's own numbers were taken on `ce7bae8b44` and are ⛔ not reused — PR #18455 rewrote large parts of this file at 12:36Z on 2026-09-16. | reading (all at 13:17Z–13:39Z, tip `a282182f4`, base `e915c190e`) | value | | --- | --- | | file, before / after | 1,531 → 1,572 lines (+41, 0 removed) | | `CONSTRUCTS.length`, read by importing the module | **19** | | rows whose `since:` is present | **19 of 19**, every one a string naming a bash version (`4.0` ×10, `4.1`, `4.2` ×3, `4.3`, `4.4`, `5.0`, and three rows carrying a two-spelling range such as `4.2 (-g) / 4.3 (-n)`) | | `kind` distribution, read from the imported table | `builtin` **8**, `syntax` **9**, `variable` **2** = 19 | | a `kind` value meaning "external binary" | **0** — the three above are the whole set | | userland-binary terms in the file **before** this PR (`mktemp`, `sed -i`, `readlink -f`, `userland`, `BSD`, `coreutils`) | **0, 0, 0, 0, 0, 0** ⇒ zero userland rules and zero sentence putting them out of scope | | the same terms **after** | 4, 1, 1, 2, 3, 1 — all inside the new section | | the verdict line's tail | `19 constructs checked, floor bash 3.2.` | ⇒ The ruling the card carries is untouched by PR #18455, because it rests on that schema, and the schema is unchanged: `since:` is a bash version on every row by construction, `kind` has no external-binary value, and the verdict line counts exactly the rows of that table. A `mktemp` row would have to lie in `since:`, invent a `kind`, and make the verdict line's last clause false. ## ⚠️ The 20-vs-19 `kind:` reading, reconciled The dispatch recorded, ⛔ not as a defect, that the file appeared to carry **20** `kind:` rows against a verdict line saying 19. Measured on the base file: - all textual `kind:` occurrences: **20** - occurrences in row form (`^ kind: '`): **19** - the 20th, at base `:408`: `// 1. COMMAND POSITION — \`kind: 'builtin'\`, so CMD_POS applies.` — **prose inside a comment**, explaining the rule. ⇒ There is no discrepancy: 19 rows, 19 counted. The card's `builtin ×9` is that comment counted as a row; the true distribution is `builtin` **8** / `syntax` 9 / `variable` 2. ⭐ Note the shape — a comment naming a pattern in order to explain it, read as an instance of it — is the same one the card itself flags one paragraph earlier about `mapfile` comments. No change made for this; recorded so the next re-derivation starts from the table, not from the grep. ## ⭐ The 33 → 29 → 33 census swing: **reconciled**, with a reproduction The card stated plainly that it had not established why PR #17120's dev read **33** while `origin/main` read **29**, and the dispatch added that today it reads **33** again. Reconciled as follows. **Ruled out — a real population change.** A control replays the gate's own exported `isShell` over `git ls-tree` at each ref in play, so it reads the **committed tree** rather than any working tree (readings 13:20Z): | ref | what it is | shell files under the declared roots | | --- | --- | --- | | `8a70e1bf6` | PR #17120's base | **33** (31 by extension, 2 by shebang) | | `f836fb209` | PR #17120's merge commit | **33** (31 / 2) | | `ce7bae8b44` | the exact commit the card measured on and read **29** | **33** (31 / 2) | | `e915c190e` | today's `origin/main` | **33** (31 / 2) | Per root at both endpoints: 19 under the scripts root, 12 under the Claude hooks root, 2 under the git-hooks root. ⇒ The tree never moved. The **29** is not a reading of any commit in this repo. **Ruled out — a change in the gate's roots or its reading rule.** `POPULATION_ROOTS`, `isShell` and `listPopulation` are byte-identical at all four refs above (read with `git show`). So the reading rule did not drift either. **Established — the census is not a function of the tree at all.** `listPopulation` lists paths from the **git index** (`git ls-files` under the derived walk roots) and then reads each one **from disk**, and a path it cannot read is skipped silently: ```js try { text = readFileSync(join(root, rel), 'utf8'); } catch { continue; } // a deleted-but-indexed path is not a script to judge ``` The root is the checkout the script file itself lives in (derived from `import.meta.url`), or whatever `--root` names. ⇒ In any checkout where indexed `.sh` files are absent on disk — a sparse checkout, an interrupted or partial worktree materialisation, a `--root` pointed at such a tree — the census silently shrinks, at **exit 0**, with no diagnostic. Reproduced in a synthetic repo (readings 13:22Z), mutated and restored: | leg | on disk | still indexed | gate output | exit | | --- | --- | --- | --- | --- | | A | 6 `.sh` + 1 shebang hook | 6 | `7 tracked shell file(s) … census: 6 by .sh extension, 1 by shebang alone` | 0 | | B — 4 `.sh` deleted from disk only | 2 `.sh` + 1 hook | **6** | `3 tracked shell file(s) … census: 2 by .sh extension, 1 by shebang alone` | **0** | | C — restored (`git checkout HEAD -- …`, `git diff HEAD` empty) | 6 `.sh` + 1 hook | 6 | `7 tracked shell file(s) … census: 6 by .sh extension, 1 by shebang alone` | 0 | That is the card's shape exactly: the drop lands entirely in the extension half while the shebang half holds, which is what 33 (31 + 2) → 29 (27 + 2) → 33 looks like. **What is *not* established:** *which* four paths were absent in the filing seat's checkout, and why. That evidence lived in that container's working tree and is not recoverable from here. So — the **mechanism** of the swing is reconciled and reproducible; the **specific instance's** cause is not, and I am not guessing at it. ⚠️ That silent shrink is a real property worth a card of its own — a census gate that reports a partial population as a clean pass, while the same file's own header says an *empty* population is a refusal rather than a quiet pass. ⛔ Deliberately **not** changed here: this card's deliverable is the boundary paragraph, and changing `listPopulation` would be a behaviour change on a file fenced to comment-only work. Reported to the PM for filing. ## Gates Derived in this worktree with `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` (no paths passed — the tool takes its own change set), at tip `a282182f4`: **30 commands**. All 30 run, all **exit 0**. This file's own gate ran **both** legs. | command | exit | | --- | --- | | `node scripts/check-bash32-floor.mjs --self-test` (via `pnpm check:bash32-floor`) | 0 — `166 cases pass`; capability line: `instrument: MANUFACTURED — bash 5.2, mapfile present before enable -n`; `real tree: 33 shell file(s) — 31 by extension, 2 by shebang alone — 0 finding(s)` | | `node scripts/check-bash32-floor.mjs` (live) | 0 — `33 tracked shell file(s) … census: 31 by .sh extension, 2 by shebang alone; 19 constructs checked, floor bash 3.2.` | | `pnpm check:pm-dispatch-gates` | 0 — `dispatch-gates self-test: 1746 cases pass`, `the battery took 828.7s on this box` (detached, blocked on with `tail --pid`, ⛔ never polled; 828.7s against the 430–450s single-host figure is contention on a shared box) | | the other 27 derived commands | 0 | Six gates first exited **3 — PREREQUISITE NOT MET** in the fresh worktree (`yaml`, `typescript`, `@typescript-eslint/parser` not installed), which is neither a pass nor a finding. `pnpm install` then ran under the shared lock — `os-verify-lock: VERDICT command-exit 0 · held the lock 9s · waited 0s` — and all six re-ran at exit 0. Nothing in this PR builds or tests a package: the diff is a comment block in a repo-root script that no package compiles. ## Changeset: `skip-changeset`, with the `files[]` reading behind it ⛔ Not the reflex. Measured: exactly **one** package manifest's directory contains `scripts/check-bash32-floor.mjs`, and it is the repo root — `@objectstack/spec-monorepo`, `"private": true`, **no `files[]` key at all**. A private package publishes nothing, so there is no shipped `files[]` content for this path to move. Positive control, so the reading is not vacuous: published packages in this repo do declare shipping lists that would catch a change — e.g. `@objectstack/hono`, `@objectstack/account`, `@objectstack/setup` each ship `["dist", "README.md", "CHANGELOG.md"]`. And scanning every `packages/*/package.json` `files[]` for an entry that could reach a repo-root `scripts/` path (a `scripts` segment, or a `..` escape) returns **0 hits** — `files[]` is package-relative, and no package sits at the repo root but the private one. ⇒ Nothing published moves. `skip-changeset`. ## ⚠️ UNAUDITED — `objectui#8694` `objectui#8694` registers `check-bash32-floor.mjs` in an upstream pin ledger. That repo is **not checked out in this container** and was not fetched. ⛔ I therefore make **no claim** that the boundary text above and that pin registration agree, in either direction. **UNAUDITED.** ## Fences observed - File surface: `scripts/check-bash32-floor.mjs` and nothing else — `git diff --stat` reports `1 file changed, 41 insertions(+)`. - ⛔ No rule added to `CONSTRUCTS` — the table is 19 rows before and after. - ⛔ No sibling userland gate built. - ⛔ PR #17120's per-script pin untouched. - ⛔ `content/docs/releases/**` untouched. - #17458's subject (the `--self-test` harness legs) untouched; both legs of this file's gate pass unchanged at 166 cases. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --- _Generated by [Claude Code](https://claude.ai/code/session_017ef78bLdybu3AffehKkhfk)_ Co-authored-by: Claude <noreply@anthropic.com>
1 parent 8fe5cb8 commit 0fe38a9

1 file changed

Lines changed: 41 additions & 0 deletions

File tree

‎scripts/check-bash32-floor.mjs‎

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,47 @@
4040
* depth over the class, at the price of a regex — and the blind spot named
4141
* under "Known limit" below is exactly the half the simulation harness holds.
4242
*
43+
* ## The BOUNDARY: the axis is the bash INTERPRETER, not the userland binaries
44+
*
45+
* A gate that states its class but not its boundary can be widened by a
46+
* well-meaning row, so the boundary is written here rather than left to be
47+
* re-derived. This gate's axis is the VERSION OF BASH THAT RUNS A SCRIPT, and
48+
* the row schema says so by construction, not by convention: every
49+
* `CONSTRUCTS` row carries a `since:` that is a bash version, and every `kind`
50+
* is a bash category — a builtin, a piece of syntax, a variable. There is no
51+
* value for "an external binary", and a row about one has nowhere to put it.
52+
*
53+
* So a difference between the GNU and BSD USERLAND BINARIES a script calls —
54+
* `mktemp` and where it will accept the `XXXXXX`, `sed -i` and whether it
55+
* demands a suffix argument, `readlink -f`, `date` and its flags — is a
56+
* DIFFERENT AXIS, and ⛔ it is not a row here. Such a row could not be written
57+
* honestly: `mktemp` has no bash version, so its `since:` would have to be a
58+
* lie or a special case, and the verdict line this gate prints ends
59+
* "constructs checked, floor bash 3.2" — which becomes false advertising the
60+
* moment a coreutils-vs-BSD rule joins the table it counts. Widening here
61+
* would silently convert a gate that means "this script needs bash 4" into one
62+
* that means "this script behaves differently on a Mac": a strictly larger
63+
* claim, made by adding a row.
64+
*
65+
* ⇒ That class wants a SIBLING gate, not a row here — and ⛔ not yet. The
66+
* measured population of real userland defects in this repo is ONE: an
67+
* `mktemp` template whose `XXXXXX` was not last. It is already fixed and
68+
* guarded by a per-script pin, which ⛔ stays — nothing here replaces it. A
69+
* shared gate for a population of one is not warranted, and the evidence that
70+
* would change that is a SECOND real instance; the place to land it is the
71+
* boundary card this paragraph came from (#17141).
72+
*
73+
* ⚠️ The basis is written out and not just the verdict, on purpose: a boundary
74+
* stated without its basis gets re-litigated by the next person who
75+
* provisionally leans yes. That is not hypothetical. This exact question —
76+
* should this gate grow a BSD/portability rule, so the `mktemp` class is
77+
* caught by a shared gate instead of a per-script pin — was asked, was
78+
* answered NO, and the seat that answered it had leaned the other way first
79+
* and put that on the record: *"measuring is what produced it — I had
80+
* provisionally leaned yes."* ⇒ Measuring is the part that is reproducible.
81+
* Read any row's `since:` and `kind:` before re-opening this; ⛔ do not
82+
* re-litigate it from intuition.
83+
*
4384
* ## The exemption rule: telling a HUNTER from a USER
4485
*
4586
* The hard part of a repo-wide scan is that the files which document this floor

0 commit comments

Comments
 (0)