Skip to content

check:engine-double-contract has no ratchet on its DISCOVERED population — deleting a pinned double's delete() member takes 319 pinned to 318 and the gate stays green #9680

Description

@os-steve

Filed unassigned by the dev seat working #9165, session session_01XqDQYVU5smx29ts9pAErja. Measured on origin/main @ c07d6e8b9, two-direction ablation, reproduction below. Not a claim about the gate's own slice logic, which is correct on the population it holds.

No domain:* and no labels set — triage's field.

The gap

scripts/check-engine-double-contract.mjs discovers its population by looking for an object literal / class / mock-constructor whose member named delete or update has an engine-shaped signature. Discovery therefore requires the member to exist. A double that stops declaring it simply leaves the population.

Nothing watches for that. The four invariants in the header are DISCOVERED, PINNED, RECONCILED, DECLARED:

  • DISCOVERED fires at zero — "Zero is not 'a clean repo', it is a broken scan". It does not fire at one fewer than yesterday.
  • RECONCILED reconciles in both directions, but only over baseline entries. A file in the DEBT ledger whose count drops is an error. A pinned file whose double disappears is invisible: pinned files are not enumerated anywhere durable, so the pinned count is a printed number, not a checked one.

So the population is unratcheted in exactly the direction that loses coverage.

Measured — two-direction ablation on a PINNED double

Subject: packages/core/src/utils/migration-journal.test.ts, whose fake declares both write verbs and routes each through the producer predicate.

Baseline @ c07d6e8b9:

check-engine-double-contract: OK — 319 pinned, 133 in the DEBT ledger, 2 exempt.

Ablation A — delete the whole async delete(...) member (the shape a partial double actually has: the method is absent, not wrong):

 pinned [update] packages/core/src/utils/migration-journal.test.ts
check-engine-double-contract: OK — 318 pinned, 133 in the DEBT ledger, 2 exempt.
exit 0

Green. The delete-slice pin for that file evaporated and the only trace is a printed integer nobody compares.

Control — keep the member, delete only the assertEngineDeleteDispatch(options); line:

 x PINNED [delete]: packages/core/src/utils/migration-journal.test.ts declares 1 engine
double(s) whose delete() does not route through assertEngineDeleteDispatch (line 50). …
check-engine-double-contract: 1 problem(s).
exit 1

Red, named, actionable. So ablation A's green is a real blind spot, not a broken harness.

Reproduce:

git worktree add ../os-ablate origin/main && cd ../os-ablate && pnpm install
node scripts/check-engine-double-contract.mjs # 319 pinned
# remove the 4-line `async delete(...)` member from packages/core/src/utils/migration-journal.test.ts
node scripts/check-engine-double-contract.mjs; echo $? # 318 pinned, exit 0
git checkout -- packages/core/src/utils/migration-journal.test.ts

Why it matters, stated without inflating it

This is the #4868 family the script's own DISCOVERED note names — a check that runs, is green, and structurally cannot reach its subject — with the reach lost gradually rather than all at once. The gate's whole value is that a hand-rolled engine fake cannot silently diverge from the real one; a fake that silently stops being a fake is the same loss reached by a cheaper route.

What this card does NOT claim. No instance is demonstrated. I did not sweep for a double that lost a verb, and I am not asserting one exists — this is a property of the checker, measured by ablation, exactly as #8639 and #8553 were. The engine gate is also not the gate that would have caught either instance on #9165 (both were registry doubles; neither declares delete or update at all).

Not a duplicate of the neighbours, and why

  • #8553 (open, pm:on-hold) — a double restated as Object.assign(base, …) is not counted as new, so a new contract goes unpinned while the count stays put. Fixing it ("count by the value that reaches the subject") would not fix this: a double that drops the member leaves the population however you count values.
  • #8639 (closed) — implOf dropped a call-expression initializer, so delete: vi.fn(async …) was never discovered. That was a double the scan never had. This is a double the scan had and lost.
  • #8058 / #8194 (both closed, #8194 by PR #8423) — existence coherence of a double's members. Different property: theirs is a member that answers wrongly, this is a member that is absent.

Common root with #8553: the discovered population is a printed quantity rather than a checked one. They want different fixes.

Candidate shapes, none costed

  1. Ratchet the discovered count the way the DEBT ledger is ratcheted — a declared expected minimum per slice, shrink-fails. Cheapest, and reuses machinery the file already has, but it is a number to maintain and a legitimate test-file deletion reddens it.
  2. Enumerate the pinned set, not just its size, so a pin that disappears names itself. Bigger artifact, but the diff says which file lost which verb, which is the message an author can act on.
  3. Report the population delta as a distinct verdict rather than folding it into OK — the generalisation of DISCOVERED from zero to delta, and the same idea #9657 and #9165 want in the sibling gates: "I could not recognise this shape" must not read the same as "clean".

Option 3 is a design act with a maintainer in it (same class as #8901), so it is deliberately not recommended here.

Related

#9165 (the card this was measured under — its H1 answer) · #8553 · #8639 · #8058 · #8194 · #9657 · #8845 · #8901

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions