fix(rest): the shipped objectQLProvider absorbs only the branded never-registered rejection - #14250
Conversation
… rejection (#13904) The shipped provider collapsed three distinguishable registry facts into one undefined, so the wiredEngineOrLoud transport seam never saw a wired-and-broken engine on the single-kernel wiring. It now resolves through kernel.getServiceAsync and absorbs only the branded isServiceNotRegisteredError rejection: never-registered stays quiet (pinned), a factory-registered engine resolves, a failed construction re-raises into the 503 outage. KernelBase-shaped hosts keep the sync leg, whose only fault is never-registered. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015YPiiDdw96RGS25WLctCQP
📓 Docs Drift CheckThis PR changes 1 package(s): 7 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 — 13 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 3a1ed038e73ba2a06e5a655382b51c0791ddeec7 && git checkout 3a1ed038e73ba2a06e5a655382b51c0791ddeec7
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin e7645078d8680027e1d2b9760dfc686c2cefb8e8 0df4cdbc65bbc4358af83b8fcdc442cc137a7533 && git checkout -B drift-repro e7645078d8680027e1d2b9760dfc686c2cefb8e8 && git merge --no-ff 0df4cdbc65bbc4358af83b8fcdc442cc137a7533
node scripts/docs-audit/affected-docs.mjs --json e7645078d8680027e1d2b9760dfc686c2cefb8e8
|
…h-window convention The at-tier contract review's one finding: the #13476 family's changeset (engine-unresolvable-fails-loud.md, PR #13910) ships the identical class of public-door behaviour change as minor and names the convention; this change goes strictly further (one refused -> served row), so patch was an undeclared divergence. Declaration and convention now named in the body. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015YPiiDdw96RGS25WLctCQP
Fixes #13904
What this repairs
The shipped
objectQLProviderinpackages/rest/src/rest-api-plugin.tshandedRestServeran engine provider that absorbed everyctx.getService('objectql')throw intoundefined. The sync accessor throws for THREE distinguishable registry facts, and only one of them means "no engine is wired":optionalDependencies: ['com.objectstack.engine.objectql']);The catch-all collapsed all three into the
undefinedthe seam contract (correctly) reads as "no engine is wired", so the transport repair that landed with the #13476 family (wiredEngineOrLoudinrest-server.ts, PR #13910: a provider REJECTION is the loud 503 outage) could never fire on the shipped single-kernel wiring — the fact was destroyed one layer before the seam could see it.The shape
The provider now resolves through the ASYNC path —
kernel.getServiceAsync, i.e.PluginLoader.getService— and absorbs ONLY the branded "never registered" rejection, using the discriminator #13905 landed (isServiceNotRegisteredErrorfrom@objectstack/core, merged in PR #14005). That classification is the REGISTRY's own, never message text; its set is closed and its default is LOUD (every unbranded rejection re-raises). Where each of the three conditions lands now:undefined, quiet — unchanged, and PINNED as the positive control;wiredEngineOrLoudanswers the 503 outage.A KernelBase-shaped host (
LiteKernel) has nogetServiceAsyncand cannot hold service factories at all (itsregisterServiceFactorythrows "not supported"), so its context accessor has exactly ONE fault to report — never registered. The provider's sync fallback leg absorbs exactly that: the same classification, not a second collapse. Both LiteKernel answers are pinned byte-identically.Capability detection (
typeof kernel.getServiceAsync === 'function') follows the precedent already in this file (typeof kernelResolver.resolveKernel === 'function'); it is a declared-API probe, not error-text matching.Wire consequence, measured at the package door
objectql(supported embedder)The fourth row (declared in the provider's code comment): the single-kernel provider passes no scope id, so the loader's "Scope ID required" rejection is unbranded and re-raises. That is wired-but-unresolvable — an engine that IS registered and cannot be produced through this accessor — and loud is the closed set's correct default for it.
Clause-2 declaration
Clause-2: yes — the CONTENT limb fires. This changes what a public door answers for real deployment conditions (the non-pinned rows above). The direction is the one the #13279 ruling chose and #13910 already shipped for the transport seam: an unknown was answered as a refusal and is now answered as the outage it is; nothing refused becomes served EXCEPT a correctly-wired, constructible factory engine that now actually serves — that row moves refused → served, and it is the "declared = enforced" direction (the engine WAS wired; refusing it was the misreading). The PATH limb does not fire: nothing under
packages/spec/src/, no new published symbol, no schema or ledger entry — the diff consumes an already-published discriminator. The changeset ships@objectstack/rest: minorunder the repo's launch-window convention — the same convention the #13476 family's changeset (engine-unresolvable-fails-loud.md, PR #13910) and #13279's name for this class of public-door change.needs:contract-reviewis attached; this PR parks as DRAFT for an at-tier review and is not enqueued by its author.Not claimed
No security claim. No route was unprotected before and none becomes newly reachable: the unknown was answered as a refusal (403) and is now answered as a refusal (503) or as correctly-authorized service through the normal capability gate. The damage repaired is diagnostic and operational.
Boundary, stated
A host whose
ctx.getKernel()surface offersgetServiceAsyncbut is NOT backed by@objectstack/core'sPluginLoaderwill not brand its "never registered" rejection, and the provider will treat it as the outage (loud). The closed set is core's own; a kernel-shaped object that reimplements the resolution path without the discriminator is outside the declared contract (PluginContext.getKernel()is typed as the coreObjectKernel). The loud default is the deliberate direction of the #13905 module doc.Consumers of this provider, swept
rest-server.tsreadsobjectQLProviderat three sites (file NOT touched by this PR — #13906 pins its seams):computeExecCtx— the intended seam: rejection →AuthzStoreUnavailableError→ 503 (pinned by the [finding] after the #13279 repair, an UNRESOLVABLE data engine still answers 403 FORBIDDEN — the last surviving GRANTS-LOST disguise at the package door #13476 fault-reachability suite with hand-made providers; this PR's suite chains the REAL provider into the same door);.catch(() => undefined)): behavior unchanged (404) for every condition;resolveExecCtxFIRST, which reachescomputeExecCtx's engine seam, so a broken engine already answers 503 before the route's own un-caught provider call is reached.Verification
packages/rest/src/rest-api-plugin-objectql-provider-three-state.test.ts— every condition driven FOR REAL: each scenario registers (or withholds)objectqlon a realObjectKernel/LiteKernel, boots the REALcreateRestApiPlugin(), and drives the provider instance the plugin actually handed toRestServer(captured at the constructor — the slot-lookups technique). No condition is simulated by throwing a hand-made error into a stub. The three conditions are asserted side by side with three distinct answers, at the provider boundary AND at the package door (403 / 200 / 503).objectQLProviderinrest-api-plugin.tsabsorbs before the transport sees it — the #13476 repair does not reach the single-kernel wiring #13904) reverts the provider to the superseded absorb shape and shows exactly the four distinguishing rows go red while every pin stays green — a binary shape cannot pass this suite.@objectstack/restsuite (166 files / 2785 tests),@objectstack/restbuild (dts 2/2), and the derived 34-family gate union ran at the reviewed commitd1d6cd5864; the patch-round head0df4cdbc65differs from it by the changeset file alone, and the gate union was re-derived and re-run there (including the ratchet families and origin/main's newer copy ofcheck-changeset-no-major, green). Evidence in the report comments on [finding] the shippedobjectQLProviderinrest-api-plugin.tsabsorbs before the transport sees it — the #13476 repair does not reach the single-kernel wiring #13904.Generated by Claude Code