Skip to content

finding: 5 engine doubles already call the producer predicate and are still outside the ratchet — one IDataEngine annotation each would admit them #11850

Description

@os-steve

Measured while implementing #11626 (PR #11849), and deliberately left out of that PR's surface — its declared surface was scripts/check-engine-double-contract.mjs plus its ledger, and this needs edits in five test files across four packages.

The measurement

check:engine-double-contract admits a double either by ≥2 engine siblings or, after #11626, by a declared IDataEngine. Probing the tree with the sibling threshold dropped to zero (a patched copy of the gate, so the criterion is the gate's own code) surfaced 7 constructs that already call assertEngineUpdateDispatch and were invisible to the gate. #11626 admitted the 2 that carry an IDataEngine annotation. Five remain invisible, all on the update slice, all genuinely pinned to the producer's predicate:

file:lineengine siblingsmembers
packages/metadata-protocol/src/protocol.bulk-record-not-found.test.ts:1141 (findOne){del, engine, findOne, rows, update}
packages/metadata-protocol/src/protocol.many-data-atomic.test.ts:930{commits, del, engine, handle, rollbacks, rows, update}
packages/metadata-protocol/src/protocol.record-not-found.test.ts:531 (findOne){del, findOne, p, store, update}
packages/metadata-protocol/src/protocol.update-path-id-wins.test.ts:1281 (findOne){del, findOne, p, store, update}
packages/plugins/plugin-auth/src/member-role-canonical.test.ts:3931 (find){calls, find, rows, update}

Why it is worth closing

These are the good case, not the bad one: five authors wrote a minimal double and bound it to the producer's decision by hand, which is exactly what the gate asks for. They get no ratchet for it. If any of them drops its assertEngineUpdateDispatch call tomorrow, or drops the update member entirely, nothing reddens and no ledger row names the loss — the #9680 shape, on doubles that are currently correct.

The remedy, and why it is cheap

One type annotation per double, e.g. const engine: IDataEngine = { … } as unknown as IDataEngine or a factory return type, plus the import type { IDataEngine } from '@objectstack/spec/contracts' where it is missing. Each then enters discovery through #11626's declaration route and lands in the pinned ledger via --write. No behavioural change, no padding — the annotation states what the object already is.

⚠️ Worth confirming per file rather than applying blind: packages/metadata-protocol cannot import from @objectstack/objectql (turbo rejects the resulting cycle — see the modules note in scripts/check-engine-double-contract.mjs), but the four doubles above already import the predicate successfully, and IDataEngine is a type-only import from @objectstack/spec, which every one of these packages already depends on. Check that the annotation type-checks against each double's actual member set — a double narrower than IDataEngine needs as unknown as IDataEngine rather than a bare annotation, which is the spelling both #11626 doubles already use.

Not measured

Whether the delete slice has an equivalent population. The probe above ran both slices and found the pinned-but-invisible set to be update-only on this tree, but that is a fact about today's corpus, not a property.

Re-derive: patch a copy of scripts/check-engine-double-contract.mjs to make the two siblings.length < 2 thresholds configurable, run it over testFiles() for both slices, and select rows with pinned === true && siblings.length < 2 && !declared.

Generated by Claude Code

Metadata

Metadata

Assignees

Type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions