Skip to content

test(plugin-auth): declare IDataEngine on the member-role migration double so its update pin ratchets - #12259

Merged
yinlianghui merged 1 commit into
mainfrom
claude/issue-11850-engine-double-idataengine-annotations
Aug 25, 2026
Merged

test(plugin-auth): declare IDataEngine on the member-role migration double so its update pin ratchets#12259
yinlianghui merged 1 commit into
mainfrom
claude/issue-11850-engine-double-idataengine-annotations

Conversation

@yinlianghui

Copy link
Copy Markdown
Collaborator

Fixes#11850

Verified at 238cc7c73c.

The card's table is 4/5 measurement artifact — re-measured, not assumed

The card asked for one IDataEngine annotation on each of five update-slice doubles. Re-deriving the population with the card's own recipe (a patched copy of scripts/check-engine-double-contract.mjs with the two siblings.length < 2 thresholds made configurable, run over testFiles() for both slices, selecting pinned === true && siblings.length < 2 && !declared) reproduces the card's five rows exactly — same files, same lines, same sibling and member sets.

But the predicate that produced them cannot tell an engine double outside the ratchet from a test-harness return tuple that re-exports an already-pinned function, and four of the five are the latter:

rowwhat the construct at that line actually isalready in the ledger?
protocol.bulk-record-not-found.test.ts:114return { engine, rows, update, del, findOne }yes — update, pinned: 1
protocol.many-data-atomic.test.ts:93return { engine, update, del, rows, commits, rollbacks, handle }yes — update, pinned: 1
protocol.record-not-found.test.ts:53return { p: new ObjectStackProtocolImplementation(engine), findOne, update, del, store }yes — update, pinned: 1
protocol.update-path-id-wins.test.ts:128return { p: …, findOne, update, del, store }yes — update, pinned: 1
member-role-canonical.test.ts:393return { rows, calls, find, update } — a real minimal engine doubleno

In each of the four metadata-protocol files the real engine double is a separate literal a few lines above (const engine: any = { registry, update, delete: del, findOne, … }), it carries ≥2 engine siblings, it is discovered by inference, and its file already holds {"verb": "update", "pinned": 1} in scripts/engine-double-contract.pinned.json. The update the probe sees on the tuple is the samevi.fn object, seen a second time through the harness's return value. There is no unprotected pin there.

Annotating those four would have been a false declaration — the tuples carry p, store, commits, rollbacks, handle, and are not engines — and would have pushed each file's ledger row from pinned: 1 to pinned: 2, double-counting one function. That is the padding failure mode #11626 was filed about, inverted. Per the card's own rule that the annotation must state what the object already is, those four are not candidates and are reported rather than padded.

What this PR does

The fifth row is genuine. makeMemoryEngine in packages/plugins/plugin-auth/src/member-role-canonical.test.ts returns a real minimal engine double: it already called assertEngineUpdateDispatch by hand, spelled one engine sibling (find) — below the inference threshold — and appeared nowhere in the pinned ledger. Real protection that no ledger row named.

It now declares the contract, entering discovery through #11626's declaration route:

  • import type { IDataEngine } from '@objectstack/spec/contracts' (plugin-auth already depends on @objectstack/spec).
  • The factory returns IDataEngine & MemoryEngineHandles, with the literal cast as unknown as IDataEngine & MemoryEngineHandles.
  • One ledger row, written by the gate's own --write.

On the spelling — the card predicted as unknown as IDataEngine, matching both #11626 doubles. That is not right here: the tests read engine.rows and engine.calls, so a bare IDataEngine return type would break those reads. The gate accepts an intersection, and its own docblock calls that "the commonest spelling in this repo — a double that adds test-only handles to the contract it implements". The intersection is what keeps this honest rather than padded: the contract is asserted, and rows/calls stay declared as the test handles they are. No member was added or changed to satisfy discovery.

The delete slice — measured, and empty

The card left this unmeasured. The probe ran both slices:

SLICE delete: scanned=749 doubles=294 pinnedLowSibling=0 selected=0
SLICE update: scanned=631 doubles=354 pinnedLowSibling=7 selected=5

The delete slice has no equivalent population on this tree — 294 doubles discovered at threshold zero, zero of them pinned-with-fewer-than-two-siblings. Nothing to fold in and nothing to file. On the update side, 7 pinned-low-sibling constructs, 2 of which already declare IDataEngine (the #11626 pair), leaving the card's 5.

Verification

The gate before the ledger write, quoting its own verdict line:

x RETAINED [update]: packages/plugins/plugin-auth/src/member-role-canonical.test.ts pins 1
engine double(s) that the pinned ledger does not record.

After --write (391 (file, verb) row(s), 1 added or grown, 0 lost — a single-row diff):

check-engine-double-contract: OK — 409 pinned, 133 in the DEBT ledger, 2 exempt.
check-engine-double-contract: 3 of them single-verb or near-minimal doubles admitted because
they DECLARE IDataEngine — the class that was structurally invisible before #11626

That count moved 2 → 3: the double entered by the declaration route, as intended.

Reverse verification. With the annotation reverted from the committed state (mutation confirmed on disk by anchored greps — import absent, intersection absent, bare signature restored) the gate goes red, exit 1:

x RETAINED [update]: … is still on disk but declares NO engine double with a update any more,
while the pinned ledger records 1. This is the #9680 shape exactly

Restored via a trap … EXIT INT TERM; tree confirmed byte-identical to the commit afterwards.

Runtime inertness.member-role-canonical.test.ts — 35/35 passing before and after, identical. The four metadata-protocol files are untouched; their 60/60 baseline is recorded for completeness.

The type check that mattered.pnpm --filter @objectstack/plugin-auth typecheck is green — but plugin-auth's tsconfig.json excludes **/*.test.ts, so that run never read the edited file. That is exactly why the package carries a TEST_DEBT entry, recorded at errors: 97 with the note "no margin, so the next new error here goes red immediately". Measured through the gate's own remeasureProject shape (its tsconfig with the test exclusion dropped, written outside the repo, 175 test files in the program): 97 errors, zero of them in the edited file — the recorded count exactly, so the ratchet does not move.

Gate union re-derived at the final commit with node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack; the family list is identical to the pre-rebase derivation. All 14 matched families plus the convention-triggered ones green, each exit code captured before any pipe:

check:agent-test-spelling · check:cross-package-test-inputs · check:entry-guard · check:parse-guard · check:pnpm-filter-targets · check:published-files · check:slot-lookup · check:test-source-alias · check:type-source-resolution · check-ci-filter-parity · check-cross-package-test-inputs · check-plugin-teardown-shape · check-affected-docs · check-drift-comment · check:query-options-erasure · check:type-check-coverage · check:where-matcher · check:engine-double-contract · check:nul-bytes · check-type-check-coverage --self-test

Declared narrowing:check:type-check-debt's --re-measure half re-runs tsc for all ~40 ledger entries and requires the whole workspace built. The full-closure build could not get the shared verify lock (three agents queued; exit 99 after a 9-minute wait). Narrowed with warrant: the ratchet compares per-entry counts, and this diff touches exactly one package, so only TEST_DEBT['@objectstack/plugin-auth'] can move — and that entry was measured directly at 97, unchanged. The --self-test half ran in full and passes. CI runs the whole farm regardless.

No changeset: test-only plus a repo ledger, nothing published — skip-changeset.

Generated by Claude Code


Generated by Claude Code

…ouble so its update pin ratchets
`makeMemoryEngine` in `member-role-canonical.test.ts` already bound its
`update` to ObjectQL's own dispatch predicate by hand, but spelled only one
engine sibling (`find`) — below `check:engine-double-contract`'s inference
threshold. So the pin was real protection that no ledger row named: drop the
`assertEngineUpdateDispatch` call, or the `update` member itself, and nothing
reddens.
Declaring the contract admits it through #11626's declaration route. The
intersection (`IDataEngine & MemoryEngineHandles`) is what keeps this honest
rather than padded — the contract is asserted and `rows`/`calls` stay declared
as what they are, test handles read by the assertions. No member was added to
satisfy discovery, and the double's behaviour is unchanged: 35/35 tests pass
identically before and after.
The ledger row is written by the gate's own `--write` (391 rows, 1 added, 0
lost).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UjM2ia8Av1v5NqfqQEQmC6
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

Nothing in this diff resolved to a documentable surface (no symbol, route or SDK anchor derived from 0 changed package(s)), so this run has no opinion about the docs.

What this run could not see
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 0 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 107bb4ba4b96bb74913e19b46f7756dd30029d4dpackageMentionDocs.

@yinlianghui
yinlianghui marked this pull request as ready for review August 25, 2026 15:38
@yinlianghui
yinlianghui added this pull request to the merge queueAug 25, 2026
Merged via the queue into main with commit c435751Aug 25, 2026
40 checks passed
@yinlianghui
yinlianghui deleted the claude/issue-11850-engine-double-idataengine-annotations branch August 25, 2026 16:23
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

skip-changesetPR has no user-facing published change; bypasses the changeset gatetests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

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

2 participants

@yinlianghui@claude