Skip to content

fix(tooling): read a defaulted mock initializer, name the inheritance rule, and derive check-engine-double-contract's own corpus figures - #10176

Merged
os-zhuang merged 3 commits into
mainfrom
claude/issue-9877-engine-double-recogniser
Aug 20, 2026
Merged

fix(tooling): read a defaulted mock initializer, name the inheritance rule, and derive check-engine-double-contract's own corpus figures#10176
os-zhuang merged 3 commits into
mainfrom
claude/issue-9877-engine-double-recogniser

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Part of #9877
Fixes#8553
Fixes#9943

Folded family dispatch against scripts/check-engine-double-contract.mjs, one commit per
member card. All numbers below are from this branch's own runs at ebb487b, the final
commit; the gate union was re-derived from the real diff with
node scripts/pm/dispatch-gates.mjs (no path arguments) and run at that same head.

⛔ The guardrail first: the shrink-only baseline is NOT touched

It stands at 66 delete / 100 update unguarded doubles on every one of the three
commits, exactly as on origin/main. The two constructs this widening pulls into the
population are pinned at source, so the ratchet tightens rather than being raised:

origin/mainebb487b
pinned doubles329331
RETAINED ledger rows318320
baseline (delete / update)66 / 10066 / 100
UNRECOGNISED2321

#9877 — a defaulted initializer was hiding a live unguarded engine delete

delete: overrides.delete ?? vi.fn(async (o, arg) => …) is a BinaryExpression, so
implOf answered null, consider() returned before isEngineVerbShape was asked, and
the double was in neither half of the ledger. Absent, which reads as clean.
fnInitializer now descends ?? / ||, reading the default first and the left arm as a
fallback (both are reachable implementations).

Ablation, both directions, on the committed tree. "UNRECOGNISED dropped by 2" is
not the evidence — the gate reddening on the double it was blind to is:

treeresult
widening only, pin revertedexit 1x PINNED [delete]: packages/rest/src/rest-batch-endpoint.test.ts declares 1 engine double(s) whose delete() does not route through assertEngineDeleteDispatch (line 36) (+ the [update] twin; 4 problems)
widening reverted, pin keptexit 1 — the double leaves the population despite the pin; UNRECOGNISED climbs back to 23 and the two ledger rows become x RETAINED … declares NO engine double with a delete any more
widening reverted, --self-testexit 1 with 6 named #9877 failures — the new limbs are not vacuous
as committedexit 0

Each mutation was confirmed on disk by grepping for the injected text and the removed
text before the run, then confirmed reverted the same way.

packages/rest/src/rest-batch-endpoint.test.ts is pinned at source with
assertEngineUpdateDispatch(data, options) and assertEngineDeleteDispatch(options);
@objectstack/rest passes 2134/2134. That file's delete double is never driven by
its own suite — the #4434 shape precisely.

⚠️Scope: one of the three spellings the card names. The other two (local binding,
shorthand — 21 constructs) need localFunctions-style resolution across two more
packages. Priced with a throwaway probe: +10 constructs, 7 files needing a pin, 0
needing a baseline raise
, enumerated in the new sub-issue #10175. That is the ruling's
"narrow the widening to what you can pin", so this PR says Part of rather than closing
the parent — the PM can close it by hand if the ?? half is judged to be the whole card.

#8553 — the inheritance rule is named in both ratchet remedies

Settled by measurement, not preference. --census (below) reports both Object.assign
shapes, and the corpus holds 0 override literals that restate delete/update and
6 that vary other engine members and inherit the verb — every one of the six correct.
Counting by VALUE instead of by site would therefore move zero constructs today, at
the price of a resolution step over an empty population, while every construct it might
later admit would arrive unpinned against a maintainer-only ratchet.

So the other option the card offers is the one taken: both remedies now state that
restating a double as Object.assign(base, { … }) is not a third way out, that
inheriting is correct while the override leaves the verb alone, and that it is a hole when
the override restates the verb with a different contract. On both messages, because an
author meets whichever one their file's baseline state produces. Guarded the way #8435
guarded its authority marker — a detector, self-tested on both remedies plus a negative
fixture that rejects the old two-option framing.

#9943 — the figures are derived now, not written down

Added --census: the gate's ownimplOf / unwrapCallImpl / isEngineVerbShape
walk, printed. #9943 had to hand-copy the criterion to re-scan and got 341 against the
recorded 310 with no way to say which was wrong; that question is gone. Per the ruling, no
constant was swapped for a fresher one — the constants are removed, and what remains is
either derivable or anchored past-tense with a date.

Two results from re-running rather than extending the census:

The census's population was the real defect. It was scoped to "every member whose
initializer is a CallExpression" — the shape the recogniser already reads. A census of
what the matcher already matches cannot report the matcher's blind spots however carefully
it is re-run, and both of its ZERO readings are CallExpression spellings for that reason.
#9877's defaulted initializer was never in that population at all. --census buckets by
initializer kind first, and on the #9943 commit alone that table reads
defaulted ?? — read 0/2: the blind spot, visible before the code that closes it.

⚠️ Conflation hazard, honoured: the CallExpression census figure was 310 and the pinned
ledger was 310 rows. Different populations that coincided — and they have already
diverged (the ledger was 318 rows on origin/main, 320 here, while the CallExpression
count is 335). Nothing in this PR merges them.

Verification at ebb487b

--census and the union were run after the final commit. Every family the derived
list named, all green:

check:engine-double-contract · check:cross-package-test-inputs ·
check:dispatcher-error-vocabulary · check:slot-lookup · check:query-options-erasure ·
check:where-matcher · check:type-check-coverage · check:nul-bytes ·
scripts/docs-audit/check-affected-docs.mjs · pnpm --filter @objectstack/rest typecheck
· @objectstack/rest 2134/2134.

Declared narrowing: check:type-check-debt was not run — its --re-measure needs the
whole workspace closure built. The structural half (check:type-check-coverage) is green
and @objectstack/rest typecheck is green; CI runs the ratchet.

skip-changeset: judged from the diff — root scripts/ plus one test file, nothing lands
in a published package.


Generated by Claude Code

…s instead of writing them down (#9943)
Every figure this gate's prose carried about its own corpus was a constant that
nothing regenerated: `310` CallExpression members with a callee table summing to
it, `93` admitted / `217` rejected, `250 doubles this gate discovers`, `82
PINNED`, and two spellings "measured at ZERO". By 2026-08-19 the DISCOVERED and
PINNED figures had drifted roughly 2x and 4x, and the comment carrying them ends
`re-run that count before adding a name here` — it is an instruction to a future
editor calibrating against numbers that are wrong.
Refreshing the constants would reproduce the defect with newer values (#9803 /
PR #9909), so this removes them and adds `--census`: the gate's own `implOf` /
`unwrapCallImpl` / `isEngineVerbShape` walk, printed. #9943 had to hand-copy the
criterion to re-scan and got 341 against the recorded 310 with no way to say
which was wrong; that question no longer exists.
Two results worth recording, both re-derived rather than assumed:
- The REPOSITORY_ONLY_MEMBERS claim SURVIVED the corpus growth. `--census` runs
the veto's own ablation (discovery with the veto off, diffed) and on
2026-08-20 the pair still moves EXACTLY 2 constructs, still both of #5945's
`IScopedContext` witnesses under packages/spec, and still 0 pinned doubles.
The conclusion held even though every number describing the corpus did not.
- The two "measured at ZERO" spellings are STILL zero. That claim held up too.
The census population itself is the finding. It was scoped to "every member
whose initializer is a CallExpression" — the shape the recogniser already reads
— so it could only ever find more of what it already understood, and both of its
ZERO readings are CallExpression spellings for that reason. `--census` buckets by
initializer KIND first, so a spelling this gate cannot read shows up as its own
row rather than as silence. On this commit that row reads `defaulted ?? — read
0/2`, which is #9877's blind spot, visible for the first time.
`--census` reaches no verdict and changes no exit code; `report()` does not call
it. Self-tested on both sides of every bucket it splits.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DdCnBGcHeufjrq7drTD3wt
…ive unguarded engine delete double (#9877)
`delete: overrides.delete ?? vi.fn(async (o, arg) => …)` is a BinaryExpression.
`fnInitializer` knew a function literal and a mock constructor and nothing else,
so `implOf` answered null, `consider()` returned before `isEngineVerbShape` was
ever asked, and the construct left the population with no verdict recorded — not
pinned, not baselined, not exempt. Absent, which reads to every consumer of this
script's output as clean.
`packages/rest/src/rest-batch-endpoint.test.ts` was that case, and it is not
cosmetic: its fake ObjectQL declares an engine `delete` this suite never drives,
which is exactly the shape #4434 shipped a dead REST route with. Measured on
this branch, both directions:
widening only, no pin PINNED [delete] and PINNED [update] on that file, exit 1
widening + the pin OK, exit 0; UNRECOGNISED 23 -> 21
⛔ The shrink-only baseline is NOT touched: it stays at 66 delete / 100 update
entries across all three commits. Both newly-visible doubles are PINNED at the
source — the fake now opens with `assertEngineUpdateDispatch(data, options)` and
`assertEngineDeleteDispatch(options)` — so the ratchet tightens (329 -> 331
pinned, ledger 318 -> 320 rows) rather than being raised. `@objectstack/rest`
passes 2134/2134 with the predicates in place.
The widening stops at `??` / `||`, and that is a narrowing, not an oversight.
`a ?? b` reaches the subject as one arm or the other and BOTH are read; a
conditional's arms are selected by a test this gate cannot evaluate, so it is
deliberately left out (`--census` reports it, ZERO on 2026-08-20). The remaining
21 UNRECOGNISED constructs are the local-binding and shorthand spellings, which
need `localFunctions`-style resolution and are reported, not silently widened.
The census self-test that pinned this spelling as UNRECOGNISED is retargeted
rather than deleted: the same source is now asserted absent from the census AND
present in the population, so the two walks are proved to agree. Its old limb
keeps a witness (`traced('DELETE', fn)`) or the branch would rot unreachable.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DdCnBGcHeufjrq7drTD3wt
…Object.assign is not an invisible third option (#8553)
PR #8537's CI history is an unusually clean natural experiment: the same test
file went red -> green on this gate by changing only the SPELLING of a double.
A fresh literal declaring the engine verbs was counted; `Object.assign(makeQl(),
{ … })` over the file's existing double was not, and the count returned to its
baseline with the double still there.
The mechanism is the two-engine-sibling test. This gate counts DECLARATION
SITES, so an override literal — which normally carries only the members it
varies — has too few siblings of its own, `consider()` returns, and the
construct is accounted for by whatever the base was pinned or baselined as. That
is CORRECT while the override leaves the scanned verb alone, and #8537's use was
legitimate and documented in the test. What made it a card is that the gate's
own message offers exactly two ways out ("pin the new one" / "raise it, do not")
while a third, cheaper, invisible route exists. A ratchet does not erode by
someone defeating it; it erodes when the cheapest route out is the unseen one.
Settled by measurement rather than by preference. `--census` (#9943, previous
commit) now reports both `Object.assign` shapes, and on 2026-08-20 the scanned
corpus holds:
0 override literals that RESTATE `delete`/`update` <- the hole proper
6 that vary other engine members and inherit the verb <- inheritance, correct
So counting by VALUE instead of by site would move zero constructs today, at the
price of a resolution step over an empty population — and every construct it
might later admit would arrive UNPINNED against a shrink-only, maintainer-only
baseline. The defensible act is the other one the card offers: say the rule.
Both ratchet remedies now carry it, because an author meets whichever one their
file's baseline state produces and a clause on only one of them is a coin flip.
Guarded the same way #8435 guarded its authority marker: a detector rather than
a string compare, self-tested on both remedies plus a negative fixture that
REJECTS the two-option framing this card filed against — so a rewording that
dropped the clause fails here instead of passing vacuously. And the day a
VERB-shaped override does land, `--census` names it and this becomes a priced
act rather than a hypothesis.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DdCnBGcHeufjrq7drTD3wt
@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.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/lskip-changesetPR has no user-facing published change; bypasses the changeset gatetests

Projects

None yet

2 participants

@os-zhuang@claude