Conversation
…envelope The facade is trusted and mints its own elevated ExecutionContext, which the runtime spreads last — so a caller-supplied `context` was overridden, never honoured, while the parameter type went on declaring it. That is ADR-0049's declared-but-unenforced shape on the one key that carries identity and tenant. Take the remove arm, at the declaration layer only: the parameter is now `Omit<EngineQueryOptions, 'context'>`. No runtime behaviour changes — the facade arm still accepts the key from the untyped channel and still overrides it, because refusing an identity key there is a runtime permission change no ruling covers. The asymmetry is recorded on both halves rather than closed. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LvwGppdonww4zGLWZo5rho
…d slot One shard, one semantic hunk: `find(object, query: EngineQueryOptions)` becomes `find(object, query: Omit<EngineQueryOptions, 'context'>)`, plus the member doc that states why. No declaration-emit ORDER churn in the other seven shards — this diff adds no import, so the d.ts chunking does not move. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LvwGppdonww4zGLWZo5rho
…cade-find-context-declared-unenforced
📓 Docs Drift CheckThis PR changes 2 package(s): 1 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
What this run could not see
Coarse fallback — 141 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 69a5322b5601432be75b19417d00c63193c15ab6 && git checkout 69a5322b5601432be75b19417d00c63193c15ab6
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin e3b3cdd2df3bda349ef7a41b0de39c8de4fddc87 d55c3d9e77279677b81cf3beb5177d3e88229582 && git checkout -B drift-repro e3b3cdd2df3bda349ef7a41b0de39c8de4fddc87 && git merge --no-ff d55c3d9e77279677b81cf3beb5177d3e88229582
node scripts/docs-audit/affected-docs.mjs --json e3b3cdd2df3bda349ef7a41b0de39c8de4fddc87
|
Contract reviewServed-tier: Isolated review for the ① Derived judgments1. The narrowing is real at the type level and it bites — measured, not read off the body.
2. Narrowing, not widening — the accept set re-derived by a compiled probe, not by reading the diff. A probe file compiled under the same test tsconfig held all four assertions: the key set removed from 3. Mechanism and the runtime half, re-traced on head. 4. Published surface. 5. Changeset and ADR-0087 signal (4), re-derived. The changeset bumps only 6. The census the premise rests on — re-taken with my own instrument. Receiver-chain regex plus a balanced-paren scan of the second argument plus a top-level 7. Head, base and collisions — one correction to the brief I was handed. The PR API reports 8. Hygiene. Draft PR; first body line ② Semver level
③ Boundary flags
Implemented-by: VERDICT: PASS Generated by Claude Code |
Fixes #19237
Clause-②: no
Omit(EngineQueryOptions, 'context')means theOmitutility type. The platform rewrites tag-shaped fragments in a body, and a fence does not protect them, so the real spelling lives in the diff.The action facade's
findaccepted a caller-writtencontextthat type-checked and the runtime did not honour — ADR-0049's declared-but-unenforced shape on the one key that carries identity and tenant. This takes the remove arm, at the declaration layer only: the parameter becomesOmit(EngineQueryOptions, 'context'). No runtime behaviour changes.The premise, measured FIRST — it HOLDS
The dispatch made the ruling conditional on a census: no call site writes a
contexton a facade query and relies on it to narrow identity or tenant. Measured before a line of fix was written.Instrument (
census3.mjs, three arms, run against the tree at1739f71879f):RECV.engine.VERB(whereRECVis an action-ctx nameengine.VERB(in a file that destructuresengineout of a ctxexamples/app-todowrites it this wayV.VERB(whereVis assigned frombuildActionEngineFacade(...)Each call's second argument is extracted by balanced-paren scan, not a line regex, so a multi-line envelope is read whole.
Radius: 8292 tracked text files — the whole repository, not the importers of
ActionEngineFacade. That denominator is deliberate, and it is the one PR #19223 warned about: the facade is reached throughActionHandlerContext.engine, so an importer count of the facade type is the wrong population.Readings, exit codes captured before any pipe:
findcontextkey: 11findsites writing acontextkey: 1That one is
packages/runtime/src/action-engine-facade-find-envelope.test.ts:126— the pin that asserts the key is NOT honoured, added by #19223. It is the instrument's firing control: arm C demonstrably sees a real facadefindcarrying acontext.The other 10 are not facade sites, and each was classified by reading the file rather than by name:
packages/objectql/src/internal-fields.test.ts—ctxthere isAwaited(ReturnType(typeof buildEngine)), a real ObjectQL engine; sibling calls tofindOneandaggregateare membersActionEngineFacadedoes not declare.action-engine-facade-find-envelope.test.ts:209-211— a differentengine, built by the file's ownmakeRealEngine(); they pass a third argument, and the facade'sinserttakes two.Dark control: the same instrument with a member and a builder that cannot exist (
.engineZZZQ,buildActionEngineFacadeZZZQ) — exit 1,FACADE_SITES total=0on all three arms.Sibling radius:
objectuiatdda8f3815df—git grepforActionEngineFacade,ActionHandlerContextandctx.engine.exits 1 / 0 hits, with a firing control in the same tree (a token that certainly exists) exiting 0.⇒ Zero live call sites. The p0 upgrade trigger does not fire.
priority:p1stands.Mechanism: OVERRIDE, not drop — traced to a named line
At
origin/main=1739f71879f, read 2026-09-20T08:42Z:packages/runtime/src/action-execution.ts:1620contextis spread last, after the caller's envelope, so the facade's own elevatedExecutionContext(minted at:1560bybuildActionExecutionContext(ec)) replaces whatever the caller put under that key. The key reaches the engine; the caller's value does not. PR #19223's body claim holds on today's tree, and it is override rather than drop.The third card fact: the sibling arms do NOT share the shape
ActionEngineFacadedeclares exactly four members, and only one takes an options bag:There is no
findOneand nocounton this facade. The write doors have nowhere to carry acontextat the type level, so there is nothing to price and nothing to widen this diff onto. Reported as measured, per the order.What changed
packages/spec/src/ui/action-params.zod.ts— the declaration.find(object, query: Omit(EngineQueryOptions, 'context')), plus the member doc rewritten: why the key is gone, and the asymmetry it leaves.packages/spec/src/ui/action-params.test.ts— finding(spec):ActionEngineFacade.find'sFilterConditionslot still admits the ObjectQL envelope{ where: … }at compile time — closing the bar is a vocabulary claim (no field namedwhere) the spec does not declare #15124's identity pin retargeted to the narrowed shape; a second pin that reds only whencontextbecomes writable again; a value-level refusal pin with a positive control.packages/runtime/src/action-execution.ts— comment only, zero behaviour. The arm's docblock now states that the type no longer admits the key while this arm still does, and why closing that half is not a type narrowing's business.content/docs/ui/actions.mdx— the callout gains the one subtraction.api-surface-declarations/ui.txt, one shard, one semantic hunk. The other seven did not move — this diff adds no import, so the d.ts chunking does not shift.The pin is TYPE-level, and that is deliberate
FindQueryCarriesNoContextKeyasserts the key is absent from the declared slot; the two@ts-expect-errordirectives red if a literal carryingcontextstarts compiling. A runtime pin would assert a refusal that does not exist and must not: adding one makes the facade throw on an identity key, which is a runtime permission change no ruling covers. The runtime's own pin is untouched and still green.The file is inside the checked zone —
check:test-typecheckreportspackages/spec/tsconfig.test.jsoncompiling 54 files — so these are not phantom directives.Reverse verification
Fix committed first, then the declaration alone reverted to
EngineQueryOptions:3f73de3ae0f→58f5dc6b90e; a no-op edit would have been caught here and the reading voided.pnpm --filter @objectstack/spec typecheck→ exit 1,src/ui/action-params.test.ts: 4 type error(s)— the two asserts plus the two now-unused@ts-expect-errordirectives.git checkout HEAD -- PATH(never a bare checkout, which reads the polluted index):git diff HEADempty andgit hash-objectback to3f73de3ae0f, byte-identical. A trap on EXIT/INT/TERM carried the restore, with an absolute repo root.Direction predicted before the run and observed: red.
Verification — per consumer package, on the merged head
d55c3d9e772@objectstack/spectypecheckexit 0@objectstack/runtimetypecheckexit 0@objectstack/objectqltypecheckexit 0@objectstack/example-todotypecheckexit 0@objectstack/objectqlis not on the dispatched floor list: the census found it, atpackages/objectql/src/engine-write-not-found-gate.test.ts, which builds a real facade throughbuildActionEngineFacade. Run because it is a consumer, and said so.runtimerun answered 242 test files failed / 7 tests failed, which wasCannot find packageon an unbuilt dependency closure — PREREQUISITE NOT MET, not a red. Re-run afterpnpm --filter '@objectstack/example-todo^...' buildand reported above.Gates.
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack, derived from this tree, re-derived after the merge (same 107, no families added or dropped): 107 of 107 green, each exit code redirected to its own file and read back before any pipe, then reconciled with--rancarrying the codes —107 run, 0 NOT-MEASURED (a DERIVED zero).Two needed a second run, and both were prerequisite misses rather than reds:
check:skill-examples(exit 1,packages/client-react/distunbuilt) andcheck:dual-build-cjs-loads(exit 3, its ownPREREQUISITE NOT MET — ⛔ This is NOT a pass). Both green after building the missing packages.check:pm-widening-tellsis green — the T1 tell that card #19099 records against this shape did not fire, so theClause-②: nodeclaration needed no over-declaring to get past a gate.Lint, repo-wide rather than narrowed:
eslint . --no-inline-configover all 6916 files eslint's own config judges — 0 errors, 0 warnings, exit 0, atd55c3d9e772. The file count is read from eslint's own--format jsonoutput, not estimated. No type-aware linting is configured (eslint.config.mjsstates it carries noparserOptions.projectand no typed rules), so nothing in this diff can move an untouched file's verdict.Declaration
Clause-②: no— this puts no new key on a published payload; it removes one from a parameter type. The lane charter's line that a narrowing does not trigger clause ② is the criterion, andcheck:pm-widening-tellsagrees with it mechanically. The changeset separately carriesClause-②: no (narrowing), which is signal (4) tocheck-adr-0087-registration: an accept-set narrowing on a published type is exactly what #16421 built that signal for, so it is declared rather than left to prose, with analready-registereddisposition namingaction-engine-facade-find-query-envelope— the entry #19223 landed, which already tells an upgrader that a caller-suppliedcontextis ignored. That gate is green.Acceptance notes
Noted, not filed — the asymmetry this leaves, stated so nobody reads it as an oversight. After this diff the facade's
findarm refuses (at runtime) every top-level key the envelope does not carry, accepts-and-honours the ones it does, and accepts-and-overrides exactly one:context, for untyped callers only. Closing that last cell means a runtime refusal on an identity key — the maintainer's floor, not a dev's and not a seat's, and the dispatch prohibited taking it here. It is recorded on both halves of the contract (the spec member doc and the runtime arm's docblock, the latter with an explicit "do not finish the job here without a ruling"). Carrier: whoever holds the next ruling on this surface — there is no PR or person this file is waiting on today, so it is written down where the next editor of either half will read it, rather than filed as a card nobody is dispatched to..engine.is not sound here — 10 of the 11contextwriters it flags are the data engine, which honours the key. Only a type or construction anchor (buildActionEngineFacade, or thefindOne/aggregatemembers the facade lacks) separates the two populations.Generated by Claude Code