Skip to content

fix(rest): the shipped objectQLProvider absorbs only the branded never-registered rejection - #14250

Merged
os-justin merged 2 commits into
mainfrom
claude/issue-13904-objectql-provider-absorb
Sep 1, 2026
Merged

fix(rest): the shipped objectQLProvider absorbs only the branded never-registered rejection#14250
os-justin merged 2 commits into
mainfrom
claude/issue-13904-objectql-provider-absorb

Conversation

@os-justin

@os-justin os-justin commented Sep 1, 2026

Copy link
Copy Markdown
Collaborator

Fixes #13904

What this repairs

The shipped objectQLProvider in packages/rest/src/rest-api-plugin.ts handed RestServer an engine provider that absorbed every ctx.getService('objectql') throw into undefined. The sync accessor throws for THREE distinguishable registry facts, and only one of them means "no engine is wired":

  1. never registered — the supported no-data-plane embedder shape (this plugin declares optionalDependencies: ['com.objectstack.engine.objectql']);
  2. registered as a service FACTORY — the sync accessor can only refuse it ("wrong accessor");
  3. a registration that FAILED to build — a real resolution failure.

The catch-all collapsed all three into the undefined the seam contract (correctly) reads as "no engine is wired", so the transport repair that landed with the #13476 family (wiredEngineOrLoud in rest-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 (isServiceNotRegisteredError from @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:

  • never registered → branded → resolves undefined, quiet — unchanged, and PINNED as the positive control;
  • registered as a factory → the async accessor CONSTRUCTS and returns the engine — the "wrong accessor" condition dissolves structurally instead of being classified (previously this wiring was refused as "no engine" even though a constructible engine was wired);
  • registration failed to build → unbranded → re-raised → wiredEngineOrLoud answers the 503 outage.

A KernelBase-shaped host (LiteKernel) has no getServiceAsync and cannot hold service factories at all (its registerServiceFactory throws "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

registry fact (driven for real on an ObjectKernel) before after
nothing registered under objectql (supported embedder) 403 FORBIDDEN 403 FORBIDDEN — unchanged, pinned
engine registered as a factory, grants the capabilities 403 FORBIDDEN (refused as absent) 200
engine registered and FAILS to construct 403 FORBIDDEN (the disguise) 503 SERVICE_UNAVAILABLE
engine registered SCOPED, resolved with no scope id 403 FORBIDDEN (read as absent) 503 SERVICE_UNAVAILABLE

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: minor under 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-review is 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 offers getServiceAsync but is NOT backed by @objectstack/core's PluginLoader will 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 core ObjectKernel). The loud default is the deliberate direction of the #13905 module doc.

Consumers of this provider, swept

rest-server.ts reads objectQLProvider at three sites (file NOT touched by this PR — #13906 pins its seams):

Verification


Generated by Claude Code

… 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
@github-actions github-actions Bot added the size/m label Sep 1, 2026
@github-actions

github-actions Bot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/rest, touching 2 documentable anchor(s).

7 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/permissions/authentication.mdx (via createRestApiPlugin (symbol, a top-level function))
  • content/docs/plugins/anatomy.mdx (via com.objectstack.engine.objectql (literal, a string literal in createRestApiPlugin))
  • content/docs/plugins/development.mdx (via com.objectstack.engine.objectql (literal, a string literal in createRestApiPlugin))
  • content/docs/plugins/index.mdx (via com.objectstack.engine.objectql (literal, a string literal in createRestApiPlugin))
  • content/docs/plugins/packages.mdx (via createRestApiPlugin (symbol, a top-level function))
  • content/docs/protocol/kernel/index.mdx (via com.objectstack.engine.objectql (literal, a string literal in createRestApiPlugin))
  • content/docs/protocol/kernel/lifecycle.mdx (via com.objectstack.engine.objectql (literal, a string literal in createRestApiPlugin))
What this run could not see
  • the SDK route bridge reached 47 of 219 client-bound route-ledger rows — the other 172 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 172: 14 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 13 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json e7645078d8680027e1d2b9760dfc686c2cefb8e8packageMentionDocs.

Which tree this was computed on

This run read content/docs from 3a1ed038e73ba2a06e5a655382b51c0791ddeec7 — the merge of head 0df4cdbc65bbc4358af83b8fcdc442cc137a7533 into base e7645078d8680027e1d2b9760dfc686c2cefb8e8, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# 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

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs e7645078d8680027e1d2b9760dfc686c2cefb8e8 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

…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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

2 participants