Uh oh!
There was an error while loading. Please reload this page.
fix(scripts): admit a single-verb engine double on the contract it declares - #11849
Conversation
…clares `check:engine-double-contract` discovered a double by the verb plus two engine siblings, so a double declaring only the verb under test was never discovered and never pinned -- and the only way into the ratchet was to pad the double with `find`/`insert` verbs its test never calls. The gate's own admission criterion rewarded ceremony, and a padded double is a worse double: the next reader cannot tell which verbs are load-bearing. Sibling inference stays exactly where it is; a second admission route is added alongside it. A construct that DECLARES the engine contract -- `const engine: IDataEngine`, `as unknown as IDataEngine`, a factory return type, `implements IDataEngine` -- is admitted at any sibling count. That is a different kind of evidence rather than a looser guess, and it is the repo's standing rule that declared means enforced. The local name must be bound by an import from where the contract lives, so a file-local look-alike declares nothing. Measured before designing: dropping the sibling threshold to zero admits 74 constructs, 32 of them HTTP routers and servers whose `delete` is the HTTP verb. Exactly 2 declare `IDataEngine`, and both were already calling `assertEngineUpdateDispatch` and getting no protection for it. So the widening reddens nothing and only adds -- 401 pinned -> 403, DEBT and EXEMPT unchanged. Discovery and the census move together, or the two walks are scoped differently (#10175). `--census` output is byte-identical before and after. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015ahemw8RcTgqtxrj15PEZx
os-steve
commented
Aug 24, 2026
ACCEPT — PM review, Verified against the branch content at The load-bearing claim, checked in the code rather than in its comment. The PR says the declaration "replaces the sibling threshold and nothing else — a declared construct is still put through The shape veto is at 1209, after the admission at 1207. So a declared double is genuinely held to the same standard; announcing itself buys entry to discovery and nothing else. The second walk is gated the same way ( The population delta, recomputed from the ledger rather than restated. Parsed both revisions of
The added pair is exactly What I did NOT verify, stated as a narrowing. The run-derived verdicts — Two things the dev did better than the card. The card's sharp half was the padding incentive. Rather than accepting it, the dev measured it and narrowed the card: no padded double exists in the tree, so this is a hazard with a documented near-miss, not a standing harm. The two ceremonial-looking candidates in And the population work came first and decided the design. Relaxing the threshold was falsified rather than assumed unattractive — threshold 0 admits 74 further constructs, 32 of them HTTP routers whose The residual gap is declared rather than left to be rediscovered — an undeclared single-verb double is still invisible, and the reason is stated in the gate header: the only evidence for it is member names, i.e. the inference route at a threshold already measured to sweep in 32 routers. Leaving that open with the reason attached is the right call. #11850 is correctly shaped — filed unassigned, Flipping to ready and arming auto-merge. Generated by Claude Code |
Uh oh!
There was an error while loading. Please reload this page.
Fixes#11626
check:engine-double-contractdiscovered a double by the verb plus ≥2 engine siblings, so a double declaring only the verb under test was never discovered and never pinned. The card's sharp half is the incentive that follows: the only way into the ratchet was to pad the double withfind/insertverbs its test never calls. The gate's own admission criterion rewarded ceremony, and a padded double is a worse double — the next reader cannot tell which verbs are load-bearing.Population, measured before designing anything
The 383-row ledger enumerates the discovered population by construction, so counting the undiscovered ones needed a different instrument. I patched a copy of the gate to parameterise the sibling threshold and changed nothing else — two hunks, diffed and read before use — so the criterion is the gate's own code rather than a hand-copy (the #9943 hazard). Positive control: the probe at threshold 2 reports
401 pinned, exactly what the gate prints.At threshold 0 the scan admits 74 further constructs — 39 with zero engine siblings, 35 with one:
deleteis the HTTP verb)IHttpServerEmailPersistence/SettingsSecretStoreIDataEngineanyor no declaration)So relaxing the threshold is not available: it sweeps in routers by the dozen, and reddening correct code is the one cost this gate's ledger cannot absorb.
Falsifications the card asked for
Do single-verb doubles exist? Yes, and the two that matter are the strongest possible case for the card.
packages/metadata/src/loaders/database-loader-update-id-fold-wins.test.ts:65andpackages/services/service-settings/src/settings-engine-id-fold-wins.test.ts:65each declare onlyupdate, each is annotatedIDataEngine, and each already callsassertEngineUpdateDispatch. They did the right thing by hand and got no ratchet protection for it — if either dropped the pin tomorrow, nothing would have reddened.Is the padding incentive a standing harm? No — a hazard with a documented near-miss, which prices the card differently and is worth saying plainly. I found no padded double in the tree. The two ceremonial-looking candidates in
seed-tenancy-backfill.test.tsare not padding:resolveSeedTenancyLedgerduck-types on['getObject','find','insert','update'], so those siblings are load-bearing for the production resolver. What the repo does have ispackages/services/service-automation/src/plugin-startup-log-cause.test.ts:145-167, where an earlier draft carried an unexercisedasync delete() { return true; }; the author removed it and proved dormancy with an injected marker (0 prints, controlfind11 in the same run). Authors do write unexercised verbs on doubles; here the pressure was documented and declined rather than acted on.Is widening expressible without a heuristic? Yes, and not by guessing better. The declaration is a different kind of evidence: the author writes down which contract the object implements.
const engine: IDataEngine = …,… as unknown as IDataEngine, a factory return type,class F implements IDataEngine. Declared means enforced. The local name must be bound by an import from where the contract lives — measured spellings only (@objectstack/spec/contracts36,@objectstack/core5, relative./data-engine2) — so a file-local look-alike declares nothing, the same propertypinnedImportsOfalready holds for the predicates.The declaration replaces the sibling threshold and nothing else: a declared construct still goes through
isEngineVerbShape, so a driver-shaped verb is still vetoed. Announcing yourself does not buy a looser standard.Population after, every delta explained
--censusoutput--writereported2 added or grown, 0 lost. No construct left any population; nothing new is unpinned; no correct double reddened.Discovery and the census admit the same set — applying the route to one walk and not the other is #10175's scoping split one layer up, and the self-test pins both sides.
Non-vacuity, both directions, mutation proven on disk
Both legs ran under
trap '…' EXIT INT TERM, anchored on text unique to the site meant, with the anchor counted before and after and the sha compared.Leg A — remove the declaration route (
const declared = siblings.length < 2 && declaresEngineContract(…)→const declared = false;). Anchor count 1→0, replacement 0→1, shab99fef0d…→10197f1f…,git diffshowing the changed line:check:engine-double-contractcannot see a single-verb double — and the only way in is to pad the double with verbs its test never calls #11626 population caseRETAINED [update]naming both filesLeg B — restore.
RESTORED_SHA=b99fef0d…equalsSHA_BEFORE,cmpbyte-identical, anchor back to 1, mutant text absent.Leg C — the negative half (
declaresEngineContractforced toreturn true). shab99fef0d…→9c0f87fb…, injected line present. Self-test exit 1, 9 failures: all five new negative cases (router, file-local look-alike, wrong-module import, type argument, driver-shaped verb) plus three pre-existing sibling guards. Restore byte-identical. So neither the positive nor the negative cases are vacuous.There is no build between the edit and the reading — the gate runs from source (
node scripts/check-engine-double-contract.mjs), so nodist/can stale a leg.The residual gap, stated in the header rather than left to be rediscovered
A single-verb double that declares nothing is still outside discovery, and this route does not reach it. That is left open deliberately: the only evidence available for such a construct is its member names, which is the inference route at a threshold already measured to sweep in 32 routers. The remedy an author has is now cheap and honest — annotate the double with the contract it stands in for — where before it was to pad it. The gate's output also now prints how many doubles came in through each route, because its headline number is a claim about how much is protected.
Verification
Union re-run at
dd8e1580c, the final commit. Each verdict is the gate's own printed line, exit codes captured by redirect-then-capture.check:engine-double-contract—OK — 403 pinned, 133 in the DEBT ledger, 2 exempt·385 (file, verb) row(s) held by the RETAINED ledger; self-testOK(all pre-existing cases pass unchanged)check:nul-bytes—OK (scanned 6597 text file(s) … no raw ASCII control bytes)check:cross-package-test-inputs—All 116 self-test cases passed·OK: 16 package(s) read outside themselvescheck:entry-guard—148 scripts/ file(s) — every entry guard goes through invoked-as.mjscheck:pnpm-filter-targets—134/167 --filter occurrence(s) … resolvecheck:agent-test-spelling,check:parse-guard— exit 0check:where-matcher,check:pm-dispatch-gates— exit 0. Both run becausedispatch-gates.mjs --residueflags them as artifact rosters sitting underscripts/, where asilentverdict is not evidence in either direction.pnpm lint— full repo, no narrowing (eslint . --no-inline-config), exit 0 in 51s underos-verify-lock.shGate family derived with
node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack(no paths — the script takes its own change set from the merge base). No changeset: this changes CI tooling only and publishes nothing, so the PR carriesskip-changeset.Generated by Claude Code
Generated by Claude Code