Measured during #11231 (PR #11625), 2026-08-24, and ruled out of that card's scope by the domain:services seat. Filing it because it is a general property of the gate, not a fact about that PR — the next author who writes a minimal engine double will hit the same wall and make the same choice, with no record of why.
The shape
check:engine-double-contract discovers a double by the verb plus ≥2 engine siblings. A double that declares only update is therefore never discovered, and never pinned.
Measured, not inferred: PR #11625 added two such doubles, and the gate reported OK — 397 pinned, 133 in the DEBT ledger, 2 exempt while naming neither. The dev listed all 379 pinned rows exhaustively and confirmed both files were absent — a zero read against an enumerated population, not against a query that might simply have missed.
Why it matters more than a coverage gap
The gate's headline number is a claim about how much is protected. A whole class of doubles — the minimal, single-verb ones — is structurally outside it, so the number silently overstates coverage by an amount nobody can read off the output. That is the same class as #11199 (dispatch-gates.mjs never naming check:cross-package-test-inputs for an edited test file): an instrument whose blind spot is invisible in its own green verdict.
⭐ The part that makes it a finding rather than a gap
The only way to get a minimal double under the ratchet is to make it worse.
PR #11625 tried it: adding find/insert siblings makes the gate discover and pin both, and it then prints
New pinned coverage is GOOD and nothing is wrong with your change — the ledger just has to learn about it, or it never protects this file
— so the gate actively invites the change. But those verbs exist only to be discovered; the tests never call them. A double padded to satisfy a discovery heuristic is a worse double: the next reader cannot tell which verbs are load-bearing and which are ceremony.
So today the author is forced to choose between an honest double with no ratchet protection and a padded double with it. Neither is right, and nothing in the gate's output says the choice exists.
Not measured / not decided
- How many existing doubles fall in this class. The 379-row ledger enumerates the discovered population by construction, so counting the undiscovered ones needs a different instrument than the gate itself — exactly the difficulty that makes the blind spot durable.
- Whether the discovery rule should change, the gap should be documented, or single-verb doubles should be declarable explicitly. ⛔ Not ruled here. The
≥2 siblings heuristic presumably exists to avoid false positives on ordinary objects that happen to have an update method, so relaxing it is not free.
Why it was not fixed in #11231
That card's declared surface was two source files plus their tests. Writing rows into scripts/engine-double-contract.pinned.json — a hot shared artifact that parallel agents also write — was outside it, and the seat ruled A (leave as shipped) rather than widen a three-character-flip PR into a shared ledger. The doubles honour the contract by construction anyway: they call assertEngineUpdateDispatch directly, the same predicate ObjectQL.update dispatches on, so there is no present unsoundness — only absent future protection.
Refs: #11231 / PR #11625 (where this was measured, and the seat ruling) · #11199 (the same class on a different gate) · scripts/check-engine-double-contract.mjs · scripts/engine-double-contract.pinned.json
Measured during #11231 (PR #11625), 2026-08-24, and ruled out of that card's scope by the
domain:servicesseat. Filing it because it is a general property of the gate, not a fact about that PR — the next author who writes a minimal engine double will hit the same wall and make the same choice, with no record of why.The shape
check:engine-double-contractdiscovers a double by the verb plus ≥2 engine siblings. A double that declares onlyupdateis therefore never discovered, and never pinned.Measured, not inferred: PR #11625 added two such doubles, and the gate reported
OK — 397 pinned, 133 in the DEBT ledger, 2 exemptwhile naming neither. The dev listed all 379 pinned rows exhaustively and confirmed both files were absent — a zero read against an enumerated population, not against a query that might simply have missed.Why it matters more than a coverage gap
The gate's headline number is a claim about how much is protected. A whole class of doubles — the minimal, single-verb ones — is structurally outside it, so the number silently overstates coverage by an amount nobody can read off the output. That is the same class as #11199 (
dispatch-gates.mjsnever namingcheck:cross-package-test-inputsfor an edited test file): an instrument whose blind spot is invisible in its own green verdict.⭐ The part that makes it a finding rather than a gap
The only way to get a minimal double under the ratchet is to make it worse.
PR #11625 tried it: adding
find/insertsiblings makes the gate discover and pin both, and it then prints— so the gate actively invites the change. But those verbs exist only to be discovered; the tests never call them. A double padded to satisfy a discovery heuristic is a worse double: the next reader cannot tell which verbs are load-bearing and which are ceremony.
So today the author is forced to choose between an honest double with no ratchet protection and a padded double with it. Neither is right, and nothing in the gate's output says the choice exists.
Not measured / not decided
≥2 siblingsheuristic presumably exists to avoid false positives on ordinary objects that happen to have anupdatemethod, so relaxing it is not free.Why it was not fixed in #11231
That card's declared surface was two source files plus their tests. Writing rows into
scripts/engine-double-contract.pinned.json— a hot shared artifact that parallel agents also write — was outside it, and the seat ruled A (leave as shipped) rather than widen a three-character-flip PR into a shared ledger. The doubles honour the contract by construction anyway: they callassertEngineUpdateDispatchdirectly, the same predicateObjectQL.updatedispatches on, so there is no present unsoundness — only absent future protection.Refs: #11231 / PR #11625 (where this was measured, and the seat ruling) · #11199 (the same class on a different gate) ·
scripts/check-engine-double-contract.mjs·scripts/engine-double-contract.pinned.json