docs(901): pin the artifact whole is the DEFAULT for a predicate over a grammar it does not implement
Two predicates over artifacts with a real grammar were each defeated by successive spellings and withdrawn in favour of pinning the artifact whole: #887's shell parse (nine defects from one mechanism, then seven more against a partial match of web/vite.config.ts) and #891's lexical rule over the hook preamble (five spellings). Both incidents carry a record; neither is resolvable by topic before round three, which is what this class-level record adds. Decides the two questions #901 left open: - DEFAULT, not remedy. A shape-matcher's failure is a false GREEN, so the defeat that would trigger a remedy policy is found by a reviewer or an incident and never by the guard: "not defeated yet" measures who has looked. Rejected: write the matcher and pin after the first defeat — it also understates its bill, since a withdrawal costs the rounds spent AND the proofs calibrated against the narrow clause. - The exception argument carries FOUR things: the grammar and its parser; the input space as a closed enumeration with the reason it is closed; the fail direction measured as a declared, executed mutation; and what it buys priced in a cost the pin charges. Rejected: a numeric "survives N spellings" bar (measures the reviewer's imagination) and a reviewer sign-off bar (depends on the signal that arrives late). Records both riders (a pin assumes it pins the artifact that still DECIDES; widening a clause turns a survived-clause canary into a tautology) and states the threshold as the moment the NEXT spelling is found by the reviewer rather than the author. docs/README.md's guard-convention task-signal row points at the record; catalog regenerated; 59 prose lines, under the 60-line advisory ceiling. fixes #901 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015QqCpYFsKgnAnx6jVwrKiV
This commit is contained in:
+1
-1
@@ -28,7 +28,7 @@ doc below, or that changes which sections a task signal points to.**
|
||||
| Concurrency / optimistic-locking work | `docs/api-conventions.md` §7a/b/c + `docs/decisions/optimistic-concurrency.md` |
|
||||
| Auth / security-surface work | `docs/decisions/api-auth-security.md` |
|
||||
| CI / release pipeline work | `docs/ci-cd.md` + `docs/decisions/release-ci-governance.md` |
|
||||
| Proposing a new guard / CI check / regression test convention | `docs/defect-shapes-773.md` §4 (detector menu + the classes where no detector is plausible), then the rules every guard must satisfy: `docs/decisions/records/testing/guard-derives-population-from-source.md`, `…/guard-ships-with-mutation-proof.md` and `…/mutation-claims-are-executed.md` (a `MUTATION` grade carries a DECLARED clause mutation that is re-run every suite — and so does a PROSE claim that some mutation reddens, or does not redden, a named test, wherever it is written: it is a `CLAIMS` entry in the same manifest, bound to its site and verbatim quote, or it is not written) — plus `…/verification-code-needs-its-own-proof.md`, which extends the same obligation BEYOND guards to the harness, wrapper or checker doing the checking, and says where its proof lives when the checker holds no row |
|
||||
| Proposing a new guard / CI check / regression test convention | `docs/defect-shapes-773.md` §4 (detector menu + the classes where no detector is plausible), then the rules every guard must satisfy: `docs/decisions/records/testing/guard-derives-population-from-source.md`, `…/guard-ships-with-mutation-proof.md` and `…/mutation-claims-are-executed.md` (a `MUTATION` grade carries a DECLARED clause mutation that is re-run every suite — and so does a PROSE claim that some mutation reddens, or does not redden, a named test, wherever it is written: it is a `CLAIMS` entry in the same manifest, bound to its site and verbatim quote, or it is not written) — plus `…/verification-code-needs-its-own-proof.md`, which extends the same obligation BEYOND guards to the harness, wrapper or checker doing the checking, and says where its proof lives when the checker holds no row. Then `…/guard-pins-the-artifact-not-a-shape.md` for the SHAPE of the predicate itself: over an artifact whose grammar the predicate does not implement (shell, a workflow `run:` body, TOML, YAML, a jq expression), pin the artifact WHOLE by default — matching a shape inside it is the exception and must carry that record's four-part argument |
|
||||
| Adding or bounding a consequential numeric config field (an FFmpeg profile tunable, a pipeline knob) | `docs/api-conventions.md` §3d — reject out of range with a 422 naming the bound and its consequence, never accept-then-rewrite; validate against the constants the renderer reads, keep the render-time clamp for pre-existing rows, and let an UNCHANGED legacy value through on update. Then `api.ffmpeg-profile-numeric-bounds` |
|
||||
| Testing a surface gated by config / an env var / a credential | `docs/decisions/records/testing/deny-path-at-production-config-value.md` — cover the setting absent, at its production value, and each opt-out, and assert the DENY branch |
|
||||
| Touching a full-replace write path or a hand-built request object | `docs/decisions/records/testing/full-replace-asserts-field-list.md` — derive the field list from the DTO and assert set equality; reconcile by id where child state exists. In the SPA the same rule is enforced by the type system: `docs/spa-conventions.md` §4b — build the body as `Complete<T>`, annotating BOTH the wrapper parameter and every construction site |
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,71 @@
|
||||
---
|
||||
key: testing.guard-pins-the-artifact-not-a-shape
|
||||
title: '2026-09-05 — a predicate over an artifact whose grammar it does not implement pins the artifact WHOLE; matching a shape inside it is an exception that must argue for itself (#901)'
|
||||
status: active
|
||||
since: '2026-09-05'
|
||||
supersedes: none
|
||||
superseded-by: none
|
||||
rule: 'A predicate that decides something by matching a SHAPE inside an artifact whose GRAMMAR it does not implement pins the artifact WHOLE instead — the whole file, or a whole named span, compared for equality under a STATED normalisation. The boundary is the grammar and never the file type: shell source, a workflow `run:` body, TOML, YAML, a jq expression and JSON5 all qualify, and a shape matched over the OUTPUT of a real parser for that grammar does not. Extracting a `run:` value with a real YAML parser and then matching shell inside it IS in the class, because the shell is the unparsed half. THE PIN IS THE DEFAULT FOR A NEW PREDICATE, not the remedy after a shape-matcher has been defeated once. The defeat a remedy policy waits for is a false GREEN, so it is found by a reviewer or by an incident and never by the guard: "not defeated yet" is a statement about who has looked, not about the input space. A SHAPE-MATCHER IS THE EXCEPTION, and its argument carries FOUR things, written where the predicate is and re-read whenever a clause of it is widened. (1) The GRAMMAR named, and the parser that implements it — if the shape is matched over a real parse there is no exception to write. (2) The INPUT SPACE as a CLOSED enumeration with the reason it is closed: a generator whose output form is fixed, or a canonicaliser the guard itself runs. A list of the spellings the author thought of is not that reason, and is the thing that was defeated nine, seven and five times. (3) The FAILURE DIRECTION measured rather than asserted: each defeat the matcher claims to catch is a DECLARED, executed mutation under `testing.mutation-claims-are-executed`, because a shape-matcher''s miss is a false green and an unrun claim about a fail direction is not evidence. (4) WHAT IT BUYS over the pin, priced in a cost the pin actually charges — the artifact is edited routinely or written by tooling, so a pin would redden as background noise and train its readers to ignore it; or the artifact''s population is DISCOVERED rather than fixed, so there is nothing to pin. Fewer than four is a refusal, and neither elegance nor an unwillingness to update a constant is one of the four. THE THRESHOLD for withdrawing a shape-matcher already shipped is not a count of spellings: it is the moment the NEXT spelling is found by the REVIEWER rather than by the author, which is evidence that the author''s model of the input space is not the input space. TWO RIDERS, both measured. A pin assumes it is pinning the artifact that still DECIDES — ask what else could decide this and whether the pin would still match: another FILE that outranks it, another OCCURRENCE in the same file, another WORKFLOW, or a HOOK the pinned command invokes. And WIDENING A CLAUSE RETIRES THE PROOFS CALIBRATED AGAINST THE NARROW ONE: a canary asserting that a finer mutation still SURVIVES has as its precondition that the guard is deliberately coarse there, so widening the main clause turns the canary into a tautology, which reads exactly like a passing proof — re-derive every proof calibrated against the narrow clause in the same commit that widens it. THE COST IS STATED RATHER THAN DISCOVERED LATER: any edit to a pinned artifact reddens, and the pin is updated deliberately in the same commit, with a reason. That red IS the review trigger the pin exists to be. A pin also declares its NORMALISATION and what the normalisation cannot see, because "pinned whole" invites a reader to assume byte equality.'
|
||||
signals: 'pin the artifact whole · shape-matcher is the exception that argues for itself · the boundary is the grammar not the file type · predicate does not implement the grammar it parses · defeated by the next spelling · the reviewer found the next spelling · false RED versus false GREEN · nine defects one mechanism · seven respellings of a partial config match · five spellings of a lexical preamble rule · the four-part exception argument: grammar and parser, closed input space, measured fail direction, what it buys · authority moves where the pin is not looking · an outranking config file · a decoy occurrence · an npm lifecycle hook · widening a clause retires its own canary · a survived-clause canary becomes a tautology · a pin declares its normalisation · instances: `ci.image-build-delegates-the-spa-suite`, `process.hook-resolves-inputs-from-repo-root` · paths: `scripts/tests/test_image_build_delegates_the_spa_suite.py`, `scripts/tests/test_hook_fire_log.py`, `docs/guard-inventory.md`, `docs/defect-shapes-773.md` · issues: #901, #887, #891, #774, #790, #881, #773'
|
||||
mechanics: 'Two worked pins. `scripts/tests/test_image_build_delegates_the_spa_suite.py` pins command TEXT (`PINNED_STAGE_COMMANDS`), `web/package.json`''s script map (`PINNED_PACKAGE_SCRIPTS`) and `web/vite.config.ts` entire (`PINNED_VITE_CONFIG`), the last two after a SELECTION and then a partial match were defeated; its comparison runs through `_normalise_lines`, which drops blank lines and collapses whitespace within a line, and the test states that residual rather than claiming byte equality. `scripts/tests/test_hook_fire_log.py` pins the hook preamble''s two lines byte for byte (`CANONICAL_SINK_ASSIGNMENT`, `CANONICAL_SINK_SOURCE`), named once so the checker, the mutation proofs and the hooks cannot come to mean different strings. Both failure messages name the constant to update and ask for a reason in the same commit.'
|
||||
---
|
||||
|
||||
**The class, and what approximating it costs.** A predicate over an artifact with a grammar is a
|
||||
partial reimplementation of that grammar, defeated not by a clever attacker but by an ordinary
|
||||
respelling — and each defeat looks local, one more case to handle, so the sequence stops only when
|
||||
someone stops it. Measured 2026-08-30 across two unrelated subjects: a guard parsing shell text to
|
||||
decide whether a command runs the SPA suite was wrong **nine** ways from one mechanism (#887), its
|
||||
replacement — a partial match of `web/vite.config.ts` — **seven** further ways, and a lexical rule
|
||||
over a hook preamble **five** (#891). Every fix was locally correct and none of the three sequences
|
||||
converged. All three ended in the same move, and a pin has never been defeated by a respelling
|
||||
because it does not have to RECOGNISE one in order to reject it.
|
||||
|
||||
**Why the pin is the DEFAULT and not the remedy — the first thing #901 left open.** The rejected
|
||||
alternative is the tempting one: write the shape-matcher, and pin only once it has been defeated. It
|
||||
is rejected because of what the trigger event is made of. A shape-matcher's failure is a false
|
||||
GREEN, which the guard by construction does not report; the defeat therefore arrives from a reviewer
|
||||
or from an incident, so its absence measures who has looked rather than what the input can be. Both
|
||||
measurements above are that asymmetry: neither predicate withdrew because a count got high, and both
|
||||
had the precedent available for every round. The second reason is that switching later is not the
|
||||
same price as starting there. A withdrawal costs the rounds already spent AND the proofs calibrated
|
||||
against the narrow clause (rider 2), so "pin it when it breaks" understates its own bill.
|
||||
|
||||
**What a shape-matcher's exception argument must contain — the second thing #901 left open.** The
|
||||
four requirements are chosen so a reviewer can REFUSE one: each is a thing the author either wrote or
|
||||
did not. Two alternatives are rejected. A NUMERIC bar ("survives N adversarial spellings") measures
|
||||
the reviewer's imagination rather than the input space, and the count rises while the defect stands —
|
||||
it would have licensed #887's parser through most of nine rounds. A REVIEWER SIGN-OFF bar is worse:
|
||||
it makes the exception depend on the very signal the threshold says arrives late. What survives is
|
||||
requirement 2, the only candidate that is a claim about the ARTIFACT rather than about the effort
|
||||
spent on it, and requirement 3, which forces the fail-direction claim to be executed, not reasoned.
|
||||
|
||||
**Rider 1 — a pin assumes it pins the artifact that still DECIDES.** This is where the pinned form
|
||||
fails, and it fails silently, so it is the question to ask of every new pin. In #887 every hole found
|
||||
after the first withdrawal was authority moving where the pin was not looking: another FILE (vitest
|
||||
prefers `vitest.config.*`, and `vite build` prefers `vite.config.js`/`.mjs` to the pinned `.ts`, from
|
||||
vite's own `DEFAULT_CONFIG_FILES`), another OCCURRENCE in the same file, another WORKFLOW (a `needs:`
|
||||
edge naming a different job called `test`), a HOOK the pinned command invokes (a vite plugin's
|
||||
`buildStart`, an npm `prebuild`/`preinstall` script). A pin removes the SPELLING problem outright and
|
||||
reduces the MEANING problem to a list that can be worked through; a pin sold as immune to both stops
|
||||
being re-examined.
|
||||
|
||||
**Rider 2 — widening a clause can retire its own evidence.** A canary asserting that a finer mutation
|
||||
still SURVIVES is calibrated against a deliberately coarse clause. Widen that clause and the canary
|
||||
does not go red, it goes vacuous — indistinguishable from a passing proof at the point of reading —
|
||||
so the widening commit re-derives every proof calibrated against the narrow form. Same shape as the
|
||||
`DETECTOR` grade in `testing.mutation-claims-are-executed`: a surviving mutation is worth something
|
||||
only while the precondition that made it survive still holds.
|
||||
|
||||
**Where this sits against its neighbours, so it is not read as a duplicate.** The two incidents keep
|
||||
their own records — `ci.image-build-delegates-the-spa-suite` and
|
||||
`process.hook-resolves-inputs-from-repo-root` — and this is the class they instantiate.
|
||||
`testing.guard-derives-population-from-source` ends at "when the authoritative source is missing,
|
||||
create one, never approximate it with a predicate over text"; this record answers the case that one
|
||||
does not, where the artifact IS present and authoritative and what is approximated is its GRAMMAR.
|
||||
`docs/defect-shapes-773.md` §4 argues the general form. What was missing is a rule resolvable BY
|
||||
TOPIC: `docs/guard-inventory.md` carries its precedent per incident, findable only from inside one.
|
||||
|
||||
**What this does not cover.** Predicates over structured input a real parser produces — an AST, the
|
||||
git index, parsed workflow YAML consumed as data — are not approximations and keep their shape
|
||||
matching. And a pin is not mandatory for an artifact edited continuously by many hands: that is
|
||||
requirement 4's own escape hatch, and a pin reddening as background noise is a guard nobody reads.
|
||||
Reference in New Issue
Block a user