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

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/main ebb487b
pinned doubles 329 331
RETAINED ledger rows 318 320
baseline (delete / update) 66 / 100 66 / 100
UNRECOGNISED 23 21

#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:

tree result
widening only, pin reverted exit 1 — x 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 kept exit 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-test exit 1 with 6 named #9877 failures — the new limbs are not vacuous
as committed exit 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 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 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

claude added 3 commits August 20, 2026 12:21
…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 free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/l skip-changeset PR has no user-facing published change; bypasses the changeset gate tests

Projects

None yet

2 participants