Convention: for a predicate over shell or config TEXT, pin the artifact whole — "match a shape" is the exception that must argue for itself #901

Open
opened 2026-08-30 18:44:42 +02:00 by timothy · 1 comment
Owner

Two sessions reached the same answer today from unrelated subjects, each about two rounds later than this repo's own recorded standard. That is the signal for a convention rather than two area-specific records.

The measurements

#887 (image build). A guard parsed shell text to decide whether a command runs the SPA suite. Nine defects, one mechanism, across three cold-review rounds: executed heredocs treated as inert data (BuildKit does run RUN <<EOF) while an opener matched inside quotes and blinded 303 lines of a real workflow; shlex.shlex not clearing commenters the way shlex.split does, so # truncated a command mid-word — including the live ${#reports[@]} idiom; compound punctuation welding two commands; npm t, ./node_modules/.bin/vitest, pnpm vitest, timeout …, su -c … all invisible; true || npm test counted as a run it never performs. Withdrawn; command TEXT pinned instead.

Then the replacement was attacked. A partial match of web/vite.config.ts was defeated seven further ways across three more rounds — test: {, test: {, test : {, "test": {, the same for plugins:, plus two that never touched the marker at all ([…].concat([evil]), and a trailing ...moreTest spread — defineConfig is the identity function, so a later spread replaces what the pin matched). Withdrawn again; the file is pinned whole.

#891 (hook fire-log sink). A lexical rule over the ETV_HOOK_FIRE_LIB preamble was defeated by ${VAR:-<self-location>}, then backticks and $((…)), then $(printenv VAR) / $1 / $?, then an indented or exported reassignment a column-anchored regex cannot see, then $'…' quoting making the required token literal. Five spellings, one mechanism. Withdrawn; byte-identity of the preamble's two lines pinned instead.

The proposal

For any predicate over an artifact that has a REAL GRAMMAR the predicate does not implement, pin the artifact whole. Matching a SHAPE within it is the exception, and the exception has to argue for itself.

The boundary is the grammar, not the file type (main-3-bb, and it is a correction to this issue's first framing). One of these cases was shell source and the other was config blocks, so "shell or config TEXT" looked like the common factor. It is not: the common factor is that the artifact has a grammar and the predicate approximates it. TOML, YAML, jq expressions and JSON5 all qualify on that test and none of them would have been caught by the narrower wording.

A pin cannot be defeated by a new spelling, because it does not have to recognise one in order to reject it. Its failure direction is a false RED — a human reading a diff they should have read anyway. A shape-matcher's failure direction is a false GREEN.

Stated cost, so it is not discovered later: any edit to a pinned artifact reddens, and the pin must be updated deliberately in the same commit. That is the same review trigger KNOWN_GATE_CONDITIONS and PINNED_STAGE_COMMANDS already carry.

Two riders, both measured rather than reasoned

  1. A pin assumes it is pinning the artifact that still DECIDES. Every hole found in #887 after the first withdrawal was authority moving where the pin was not looking: to another FILE (vitest.config.* and vite.config.js/.mjs both outrank vite.config.ts — read from vite's own DEFAULT_CONFIG_FILES), another OCCURRENCE in the same file (a decoy first test: {), another WORKFLOW (a needs: edge naming a job called test that is not this one), or a HOOK the pinned command invokes (a vite plugin's buildStart(), an npm prebuild/preinstall lifecycle script). Ask of any new pin: what else could decide this, and would the pin still match?

  2. Widening a clause can retire its own evidence. From #891, and the sharper of the two. A survived_clause asserts a finer mutation still SURVIVES — its precondition is that the guard is deliberately coarse there. Widen the main clause and that precondition disappears; the canary then does not fail, it becomes a tautology, which reads exactly like a passing proof. So: when a clause is widened, re-derive every proof calibrated against the narrow one.

Why this is worth a record rather than two comments

docs/guard-inventory.md already carries a precedent — test_no_redirection_in_the_sink_PRECEDES_its_stderr_redirect went four iterations and was deleted rather than patched a fifth time. Both sessions today had that precedent available and neither applied it until a reviewer named it back: #887 took nine rounds then seven more, #891 took five. The existing lesson is recorded per-incident, so it is only found by someone already in the same incident. A convention record resolvable by topic through docs/decisions/README.md is what makes it reachable before the third round rather than after.

The threshold is not a count of spellings

This issue's first draft implied one ("about two rounds late"), and that is the wrong instrument — main-3-bb's correction, and it is better than what it replaces:

The signal is the moment you notice the NEXT spelling was found by the REVIEWER rather than by you. That is evidence your model of the input space is not the input space.

It generalises past this class, it needs no number, and both sessions had it available and misread it. Rounds 2 and 3 of #891 both had that property and were read as "one more fix" both times; #887 had it for most of nine rounds. Notably, neither session withdrew because the count got high — both withdrew when a reviewer quoted the existing precedent back, which is evidence for the "findable only from inside the incident" problem rather than against it.

Scope note

Proposal, not a decision. The evidence above is measured; the rule is not. What still wants a judgement call: whether the whole-artifact pin should be the DEFAULT for new guards or only the remedy after a shape-matcher has been defeated once, and what a shape-matcher's exception argument has to contain to be accepted.

Refs #887, #891.

Two sessions reached the same answer today from unrelated subjects, each about two rounds later than this repo's own recorded standard. That is the signal for a convention rather than two area-specific records. ## The measurements **#887 (image build).** A guard parsed shell text to decide whether a command runs the SPA suite. **Nine defects, one mechanism**, across three cold-review rounds: executed heredocs treated as inert data (BuildKit *does* run `RUN <<EOF`) while an opener matched inside quotes and blinded 303 lines of a real workflow; `shlex.shlex` not clearing `commenters` the way `shlex.split` does, so `#` truncated a command mid-word — including the live `${#reports[@]}` idiom; compound punctuation welding two commands; `npm t`, `./node_modules/.bin/vitest`, `pnpm vitest`, `timeout …`, `su -c …` all invisible; `true || npm test` counted as a run it never performs. Withdrawn; command TEXT pinned instead. Then the *replacement* was attacked. A partial match of `web/vite.config.ts` was defeated **seven** further ways across three more rounds — `test: {`, `test: {`, `test : {`, `"test": {`, the same for `plugins:`, plus two that never touched the marker at all (`[…].concat([evil])`, and a trailing `...moreTest` spread — `defineConfig` is the identity function, so a later spread replaces what the pin matched). Withdrawn again; the file is pinned **whole**. **#891 (hook fire-log sink).** A lexical rule over the `ETV_HOOK_FIRE_LIB` preamble was defeated by `${VAR:-<self-location>}`, then backticks and `$((…))`, then `$(printenv VAR)` / `$1` / `$?`, then an indented or `export`ed reassignment a column-anchored regex cannot see, then `$'…'` quoting making the required token literal. **Five spellings, one mechanism.** Withdrawn; byte-identity of the preamble's two lines pinned instead. ## The proposal > For any predicate over an artifact that has a REAL GRAMMAR the predicate does not implement, **pin the artifact whole**. Matching a SHAPE within it is the exception, and the exception has to argue for itself. **The boundary is the grammar, not the file type** (main-3-bb, and it is a correction to this issue's first framing). One of these cases was shell source and the other was config blocks, so "shell or config TEXT" looked like the common factor. It is not: the common factor is that the artifact has a grammar and the predicate approximates it. TOML, YAML, `jq` expressions and JSON5 all qualify on that test and none of them would have been caught by the narrower wording. A pin cannot be defeated by a new spelling, because it does not have to recognise one in order to reject it. Its failure direction is a false RED — a human reading a diff they should have read anyway. A shape-matcher's failure direction is a false GREEN. Stated cost, so it is not discovered later: any edit to a pinned artifact reddens, and the pin must be updated deliberately in the same commit. That is the same review trigger `KNOWN_GATE_CONDITIONS` and `PINNED_STAGE_COMMANDS` already carry. ## Two riders, both measured rather than reasoned 1. **A pin assumes it is pinning the artifact that still DECIDES.** Every hole found in #887 after the first withdrawal was authority moving where the pin was not looking: to another FILE (`vitest.config.*` *and* `vite.config.js`/`.mjs` both outrank `vite.config.ts` — read from vite's own `DEFAULT_CONFIG_FILES`), another OCCURRENCE in the same file (a decoy first `test: {`), another WORKFLOW (a `needs:` edge naming a job called `test` that is not this one), or a HOOK the pinned command invokes (a vite plugin's `buildStart()`, an npm `prebuild`/`preinstall` lifecycle script). Ask of any new pin: *what else could decide this, and would the pin still match?* 2. **Widening a clause can retire its own evidence.** From #891, and the sharper of the two. A `survived_clause` asserts a finer mutation still SURVIVES — its precondition is that the guard is deliberately coarse there. Widen the main clause and that precondition disappears; the canary then does not fail, it becomes a **tautology**, which reads exactly like a passing proof. So: when a clause is widened, re-derive every proof calibrated against the narrow one. ## Why this is worth a record rather than two comments `docs/guard-inventory.md` already carries a precedent — `test_no_redirection_in_the_sink_PRECEDES_its_stderr_redirect` went four iterations and was deleted rather than patched a fifth time. Both sessions today had that precedent available and neither applied it until a reviewer named it back: #887 took nine rounds then seven more, #891 took five. **The existing lesson is recorded per-incident, so it is only found by someone already in the same incident.** A convention record resolvable by topic through `docs/decisions/README.md` is what makes it reachable before the third round rather than after. ## The threshold is not a count of spellings This issue's first draft implied one ("about two rounds late"), and that is the wrong instrument — main-3-bb's correction, and it is better than what it replaces: > The signal is the moment you notice the NEXT spelling was found by the REVIEWER rather than by you. That is evidence your model of the input space is not the input space. It generalises past this class, it needs no number, and both sessions had it available and misread it. Rounds 2 and 3 of #891 both had that property and were read as "one more fix" both times; #887 had it for most of nine rounds. Notably, neither session withdrew because the count got high — both withdrew when a reviewer quoted the existing precedent back, which is evidence for the "findable only from inside the incident" problem rather than against it. ## Scope note Proposal, not a decision. The evidence above is measured; the rule is not. What still wants a judgement call: whether the whole-artifact pin should be the DEFAULT for new guards or only the remedy after a shape-matcher has been defeated once, and what a shape-matcher's exception argument has to contain to be accepted. Refs #887, #891.
timothy added the ci-cdpriority: medium labels 2026-08-30 18:44:42 +02:00
Author
Owner

Body updated with two corrections from main-3-bb (#891), both better than the framing they replace, both handed over rather than edited in — recording that here so the attribution is not lost when the body is read on its own:

  1. The boundary is the grammar, not the file type. "Shell or config TEXT" was pattern-matching on the two incidents we happened to have. The real test is whether the artifact has a grammar the predicate does not implement — which pulls in TOML, YAML and jq expressions that the narrower wording would have let through.

  2. The threshold is not a count of spellings. It is noticing that the NEXT spelling was found by the reviewer rather than by you. My original "about two rounds late" framing invited exactly the wrong reading: a session at round two would conclude it had headroom.

Both were volunteered by the session that was still mid-PR and stood to gain nothing from widening this one.

Body updated with two corrections from main-3-bb (#891), both better than the framing they replace, both handed over rather than edited in — recording that here so the attribution is not lost when the body is read on its own: 1. **The boundary is the grammar, not the file type.** "Shell or config TEXT" was pattern-matching on the two incidents we happened to have. The real test is whether the artifact has a grammar the predicate does not implement — which pulls in TOML, YAML and `jq` expressions that the narrower wording would have let through. 2. **The threshold is not a count of spellings.** It is noticing that the NEXT spelling was found by the reviewer rather than by you. My original "about two rounds late" framing invited exactly the wrong reading: a session at round two would conclude it had headroom. Both were volunteered by the session that was still mid-PR and stood to gain nothing from widening this one.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: timothy/ersatztv#901