Skip to content

fix(objectql): an in-process engine verb refuses an object name the registry does not resolve (#21516) - #21545

Draft
objectstack-fleet[bot] wants to merge 15 commits into
mainfrom
claude/issue-21516-unresolved-name-refusal
Draft

objectstack-fleet[bot] wants to merge 15 commits into
mainfrom
claude/issue-21516-unresolved-name-refusal

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Oct 3, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #21516

Clause-②: yes (narrowing)

An in-process engine verb now refuses an object name the schema registry does
not resolve with the data door's own OBJECT_NOT_FOUND (404), instead of
handing that name to the driver as a raw table name. One name space for the
in-process verbs and the generic data door (triage ruling). The engine's accept
set narrows; no surface is widened. The yes half of the clause line is the one
new export, objectNotFoundError, in @objectstack/core.

What changed

  • packages/core (new objectNotFoundError). One factory for the
    OBJECT_NOT_FOUND / 404 envelope, beside recordNotFoundError and for the
    same ADR-0076 D2 reason (the engine closure cannot import the package where
    the door's envelope was written). Both doors now build the refusal here.
  • packages/metadata-protocol (the door). assertObjectRegistered raises
    that shared factory: same wire status, same code.
  • packages/objectql (the engine). resolveObjectName throws
    objectNotFoundError for a name the registry does not resolve, rather than
    returning it as a physical table name. Every in-process verb
    (find, findOne, count, aggregate, insert, insertMany, update,
    delete, validate) resolves through it, so all refuse uniformly: no
    spelling allow-list, no per-caller marker. judgeFilter keeps judging the
    filter for an unresolved name (it reads nothing and reaches no driver), as its
    contract states.
  • Three platform-internal, constant-name best-effort probes that read a
    known system object and were already fail-soft on a missing table now treat
    the engine's refusal (attributed to their own object) as the same "not
    provisioned in this composition" case. A body cannot reach these paths:
    ObjectQL.probeInstallOrganizations (registry-presence guard),
    SeedLoaderService.resolveSoleOrganizationId and SysMetadataRepository's
    history counters (refusal recognised by code and object).
  • packages/spec. The IObjectQLEngine.judgeFilter docblock states that
    execution refuses an unknown object before admission (comment only; it ships
    in the built type declarations).

Why (classes, doors, roles, codes only)

An action body invoked through the actions door could name a protected member
of the stored-metadata family by a spelling the registry does not resolve and
receive its stored content, whether a member or an administrator invoked it.
The in-process verb handed that name to the driver as a raw table name, and
every name-keyed in-process guard (PR #21513's reader seam among them) was
addressed by the registered name only. The generic data door answers
OBJECT_NOT_FOUND for the same name. The engine now answers the same, so the
two doors share one name space and no name-keyed guard can be stepped around by
naming its target some other way.

Census: does any legitimate platform reader rely on the raw-table fall-through?

Instrumented the resolver's fall-through and ran the objectql,
metadata-protocol and runtime suites, plus a full boot, seed and door drive
of four example apps (crm, showcase, todo, multi-package). The instrument was
reverted in the branch; the net diff carries none of it.

composition fall-through reads reader relies on it?
4 example apps booted, seeded, driven through the doors 0 no: every platform reader addresses a registered object
unit/integration suites (partial registries + tolerant stub drivers) many only test harnesses, plus the three constant-name probes below

Every in-repo caller that passes a possibly-unresolved name, by function:

caller object (constant) pre-change disposition
ObjectQL.probeInstallOrganizations the org object fail-soft on missing table moved: registry-presence, empty answer
SeedLoaderService.resolveSoleOrganizationId the org object fail-soft on missing table moved: recognise the refusal as not-provisioned
SysMetadataRepository history counters the history object fail-soft on missing table moved: recognise the refusal as not-provisioned
ObjectQL.cascadeDeleteRelations / planCascadeAtomicity / referenceExists a relation ref already try/catch unchanged: already tolerate a throw
the data door's existence gate the requested name raised the 404 itself now raises the shared factory

Conclusion: no production or example reader relies on the fall-through.

Fixture triage (the test-only fallout, per the seat's answer)

Every test that encoded the raw-table fall-through, by disposition. No ADR text
is edited, and no pin ruled under the noise-discipline decision (card 7929)
changes what it asserts.

  1. The unregistered name is the deliberate probe: the test now asserts the
    refusal
    (code + status, and where the test watched the driver, that
    the driver saw nothing).
  2. The fall-through was incidental: the harness now registers what the
    platform reader resolves
    (registered after boot or DDL, so nothing new is
    provisioned and every outage/absence subject keeps its meaning).
    • objectql metadata-write harnesses (delete, save, publish-meta,
      publish-package-drafts, protocol-derived-provenance,
      protocol-save-meta-repo-path, protocol-picklist,
      protocol-publish-canonical-fold): the stored-metadata family.
    • rest (14 harness files): the stored-metadata family, after DDL.
    • plugin-security (4 files) and the http-conformance stack: the authz
      resolver's read set, unprovisioned, so the missing-table answer is still
      what they measure.
    • plugin-approvals status-mirror cascade: the delegation object and the
      org object, unprovisioned. service-settings: the secret and
      setting-audit objects.
  3. Noise pins: same subject, reshaped reading. The org probe for an
    unregistered org object is now refused before any driver, so a pin that read
    "the probe reached the driver and was withheld" now reads "the probe reached
    no driver" (tablesSeen() equal to empty where silentChannels() was
    read). runtime (about 17 files) and trigger-record-change.
    expected-read-refusal-noise.channel-asymmetry.test.ts registers its probe
    object, unprovisioned, so the real driver refusal is still what its two
    channels measure.
  4. cli served-boot control (schema-migrate.host-composition): each
    hook's probe read is witnessed by its recorded OBJECT_NOT_FOUND answer, not
    by a driver line; the SQL driver suppresses that line for its own deferred
    set, so the line never was the subject.
  5. engine.test.ts: one mock parameter typed (name: string), the
    @objectstack/objectql#typecheck red of the earlier heads.

New pin: the measured public door

packages/runtime/src/unresolved-object-name.actions-door.pin.test.ts boots
the plugin set bootStack uses and drives REST /actions. The target is a
table that exists and holds a sentinel row, created out of band at the driver
and registered nowhere (the class the card measured, naming no protected
table). For an administrator and a member, the action body's read answers
404 OBJECT_NOT_FOUND and the sentinel appears nowhere in the answer. Control:
the same body shape on a registered name is served. Reference: the generic data
door's answer for the same name is the same 404. PR #21513's reader-seam pins
stay green.

Ablation (one-shot; nothing left in the tree)

Mutation leg, scripts/ablation-replace.mjs on engine.ts: the refusal in
resolveObjectName replaced by the old raw-table return plus a marker branch
(marker on disk 1, refusal on disk 0); rebuilt; ablation-dist-preflight:
marker present in 4 built files. Pins under mutation:

  • objectql (protocol-unregistered-object, engine-20822-...,
    query-expression-conformance, engine-judge-filter):
    5 failed | 245 passed (250)
  • runtime actions-door pin: 2 failed | 4 passed (6) (both role cases)

Restore leg: git checkout HEAD -- engine.ts; blob equals the HEAD blob
f5793bff919d, git diff HEAD empty, porcelain clean; rebuilt; marker absent
from all 14 built files. Pins restored: 250 passed (250), 6 passed (6).
engine.ts is byte-identical on the final head (the later merge of main
carried no objectql source).

Verification on the final head 6752a29827 (merge of origin/main at 1ca1eb0972)

  • objectql full suite: 367 files, 7384 passed.
  • metadata-protocol full suite: 206 passed, 3 skipped files; 3187 passed, 19 skipped.
  • runtime full suite: 317 files; 5176 passed, 19 skipped.
  • service-analytics full suite: 175 files; 4152 passed, 253 skipped.
  • cli unit tier: 252 files, 3685 passed; integration tier, the three files
    this branch or the merged main touched: 36 passed.
  • mcp: 35 files, 389 passed; dogfood (registry-gate-wiring + the two
    files main added): 16 passed; the main-added example, metadata and
    core files: green.
  • plugin-security, plugin-approvals, service-settings,
    http-conformance, trigger-record-change test tasks: turbo 38/38
    (5 test tasks run, 33 cached builds).
  • Build closure for the above: 63/63 turbo tasks.
  • Before the merge (head bab0903840): typecheck of every touched package
    76/76 tasks; rest repo project 177 passed; the 14 rest harness files
    552 passed, 21 skipped.
  • CI on 6752a29827: every check green except "Part-of PR must not also close
    its card", which read the earlier body; this body is its input.

Acceptance notes

  • Recorded decision of card 3770 ("the engine deliberately does not reject;
    internal callers unaffected") is narrowed at the engine: the door's gate is
    unchanged and the engine now gives the same answer. ADR-0053's type-blind
    lowering is kept for a registered object with no field map, and removed for an
    unregistered name (the bypass itself). No ADR text is edited here.
  • Changesets: @objectstack/core minor (Clause-②: yes, the new export);
    @objectstack/objectql minor (Clause-②: no (narrowing), with its ADR-0087
    disposition); @objectstack/metadata-protocol patch; @objectstack/spec
    patch (docblock).
  • packages/cli/test/refusal-renders-once.e2e.test.ts (added on main) sits
    in neither of the cli package's two vitest projects and was not run here;
    it drives refusal rendering of os init / os compile, which this change
    does not reach.

🤖 Generated with Claude Code

https://claude.ai/code/session_01DDZNkDVwPQnevTFcYE47H3

claude added 4 commits October 3, 2026 03:14
…all-through

Records every object name the engine's resolver hands to the driver
without a registry entry, with its caller frames, into the file named by
OS_TEST_UNRESOLVED_CENSUS. Reverted before the refusal lands.

Claude-Session: https://claude.ai/code/session_01DDZNkDVwPQnevTFcYE47H3
Co-authored-by: Claude <noreply@anthropic.com>
The census it measured is recorded in the pull request.

Claude-Session: https://claude.ai/code/session_01DDZNkDVwPQnevTFcYE47H3
Co-authored-by: Claude <noreply@anthropic.com>
… does not resolve

resolveObjectName no longer hands an unresolved name to the driver as a raw
table name. It throws the data door's own OBJECT_NOT_FOUND 404, built by one
factory in @objectstack/core that the door's object-existence gate now calls
too. judgeFilter keeps judging the filter for such a name (it reads nothing).

Claude-Session: https://claude.ai/code/session_01DDZNkDVwPQnevTFcYE47H3
Co-authored-by: Claude <noreply@anthropic.com>
…e engine's refusal as the not-provisioned case

The engine now refuses an unresolved name, so three best-effort platform
probes that read a system table by a constant name no longer reach the
driver when that object is not registered in a lean/bare composition:
ObjectQL.probeInstallOrganizations (sys_organization),
SeedLoaderService.resolveSoleOrganizationId (sys_organization) and
SysMetadataRepository's history counters (sys_metadata_history). Each now
recognises OBJECT_NOT_FOUND attributed to its own object as the same benign
"not provisioned here" case it already recognises for a missing table — a
path a body cannot reach, never the resolver's old raw-table fall-through.

Claude-Session: https://claude.ai/code/session_01DDZNkDVwPQnevTFcYE47H3
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added the size/m label Oct 3, 2026
@github-actions

github-actions Bot commented Oct 3, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 4 package(s): @objectstack/core, @objectstack/metadata-protocol, @objectstack/objectql, @objectstack/spec, touching 11 documentable anchor(s). ⚠️ 1 changed file(s) yielded no anchor (packages/core/src/index.ts), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

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

  • content/docs/api/error-catalog.mdx (via OBJECT_NOT_FOUND (literal, a string literal in a comment on a changed line; a string literal in historyCounterVerdict; a string literal in objectNotFoundError; a string literal in resolveSoleOrganizationId))
  • content/docs/automation/flows.mdx (via sys_organization (literal, a string literal in resolveSoleOrganizationId))
  • content/docs/capabilities/permissions.mdx (via OBJECT_NOT_FOUND (literal, a string literal in a comment on a changed line; a string literal in historyCounterVerdict; a string literal in objectNotFoundError; a string literal in resolveSoleOrganizationId))
  • content/docs/concepts/metadata-lifecycle.mdx (via ObjectStackProtocolImplementation (symbol, a top-level class))
  • content/docs/deployment/cli.mdx (via sys_organization (literal, a string literal in resolveSoleOrganizationId))
  • content/docs/deployment/seed-tenancy-repair.mdx (via sys_organization (literal, a string literal in resolveSoleOrganizationId))
  • content/docs/kernel/cluster.mdx (via OBJECT_NOT_FOUND (literal, a string literal in a comment on a changed line; a string literal in historyCounterVerdict; a string literal in objectNotFoundError; a string literal in resolveSoleOrganizationId))
  • content/docs/permissions/administrator-guide.mdx (via sys_organization (literal, a string literal in resolveSoleOrganizationId))
  • content/docs/protocol/kernel/config-resolution.mdx (via sys_organization (literal, a string literal in resolveSoleOrganizationId))
  • content/docs/protocol/kernel/error-handling.mdx (via OBJECT_NOT_FOUND (literal, a string literal in a comment on a changed line; a string literal in historyCounterVerdict; a string literal in objectNotFoundError; a string literal in resolveSoleOrganizationId))
  • content/docs/protocol/objectql/schema.mdx (via sys_organization (literal, a string literal in resolveSoleOrganizationId))

⛔ 6 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/implementation-status.mdx (via sys_organization (literal, a string literal in resolveSoleOrganizationId))
  • content/docs/releases/v16.mdx (via ObjectStackProtocolImplementation (symbol, a top-level class))
  • content/docs/releases/v17/17-0.mdx (via ObjectStackProtocolImplementation (symbol, a top-level class))
  • content/docs/releases/v17/17-1.mdx (via sys_organization (literal, a string literal in resolveSoleOrganizationId))
  • content/docs/releases/v17/17-4.mdx (via sys_organization (literal, a string literal in resolveSoleOrganizationId))
  • content/docs/releases/v17/17-5.mdx (via IObjectQLEngine (symbol, a top-level interface), judgeFilter (symbol, a method of class ObjectQL), OBJECT_NOT_FOUND (literal, a string literal in a comment on a changed line; a string literal in historyCounterVerdict; a string literal in objectNotFoundError; a string literal in resolveSoleOrganizationId), sys_organization (literal, a string literal in resolveSoleOrganizationId))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 1 changed file(s) yielded no anchor (packages/core/src/index.ts) — pages documenting those are invisible to this run
  • 1 anchor(s) matched too much of the corpus to be a work list: ObjectQL (symbol, 71 pages)
  • the SDK route bridge reached 54 of 206 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 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.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 144 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 1ca1eb09727d7769bc605428ad1e6fb62d743f49 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 4de02f7e9cdae2e127379cf9205941ae71e0d812 — the merge of head 6752a29827847c463c90f5171d0ee9f7b083e5d8 into base 1ca1eb09727d7769bc605428ad1e6fb62d743f49, 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 4de02f7e9cdae2e127379cf9205941ae71e0d812 && git checkout 4de02f7e9cdae2e127379cf9205941ae71e0d812
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 1ca1eb09727d7769bc605428ad1e6fb62d743f49 6752a29827847c463c90f5171d0ee9f7b083e5d8 && git checkout -B drift-repro 1ca1eb09727d7769bc605428ad1e6fb62d743f49 && git merge --no-ff 6752a29827847c463c90f5171d0ee9f7b083e5d8

node scripts/docs-audit/affected-docs.mjs --json 1ca1eb09727d7769bc605428ad1e6fb62d743f49

⚠️ 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 1ca1eb09727d7769bc605428ad1e6fb62d743f49 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

claude added 3 commits October 3, 2026 05:08
… family they write through

Claude-Session: https://claude.ai/code/session_01DDZNkDVwPQnevTFcYE47H3
Co-authored-by: Claude <noreply@anthropic.com>
Deliberate probes of an unregistered name now assert the refusal (the data
door's OBJECT_NOT_FOUND envelope, nothing reaching the driver); harnesses where
the fall-through was incidental register the objects they write through. The
#3770 case-B pin keeps the door's 404 and flips its engine assertion.

Claude-Session: https://claude.ai/code/session_01DDZNkDVwPQnevTFcYE47H3
Co-authored-by: Claude <noreply@anthropic.com>
claude added 7 commits October 3, 2026 05:44
…s refusal

An unregistered organization object (and a view's unregistered probe
object) is no longer read through the driver: the engine refuses the name
first, so the declared refusal no longer occurs. The capture stays declared
and each pin now asserts nothing was withheld, so a returning read turns it
red. The channel-asymmetry pin registers its probe object (unprovisioned) so
it still measures a real driver refusal.

Claude-Session: https://claude.ai/code/session_01DDZNkDVwPQnevTFcYE47H3
Co-authored-by: Claude <noreply@anthropic.com>
… readers resolve

The engine now refuses an object name its registry does not hold, so partial
compositions register what a deployment's plugins register: the authz
resolver's read set (left unprovisioned, so it still reads "no grants"), the
settings service's secret and audit objects, and the approvals fixture's two
expected-absent probes (unprovisioned, so its withheld-refusal pin is
unchanged).

Claude-Session: https://claude.ai/code/session_01DDZNkDVwPQnevTFcYE47H3
Co-authored-by: Claude <noreply@anthropic.com>
…l reads

The engine now refuses an object name its registry does not hold, so the
real-engine import/export and classification harnesses register the family
after their DDL; an unprovisioned store still answers the driver's own
"no such table", which those pins classify.

Claude-Session: https://claude.ai/code/session_01DDZNkDVwPQnevTFcYE47H3
Co-authored-by: Claude <noreply@anthropic.com>
…r answer

The control's driver lines existed only because the hooks read objects the
boot never declared through the engine's raw-table fall-through; the engine
now refuses those names before any driver. Each probe read now records its
answer, and the control asserts OBJECT_NOT_FOUND and no driver line.

Claude-Session: https://claude.ai/code/session_01DDZNkDVwPQnevTFcYE47H3
Co-authored-by: Claude <noreply@anthropic.com>
… refusal; type a mock

The record-change org-probe pin asserts the absent organization object is
quiet by construction; the conformance stack registers the authz resolver's
read set (unprovisioned) that its stubbed auth service never did; the
engine.test expand mock types its parameter (test-typecheck ledger).

Claude-Session: https://claude.ai/code/session_01DDZNkDVwPQnevTFcYE47H3
Co-authored-by: Claude <noreply@anthropic.com>
… not resolve; changesets

An action body through REST /actions reading an out-of-band table by its
unregistered name now answers 404 OBJECT_NOT_FOUND for an administrator and a
member, with nothing of the table in the answer; the same body on a registered
name is served (the control); the data door's own 404 is the reference.

Changesets: core minor (new objectNotFoundError export), objectql minor
BREAKING narrowing with its ADR-0087 disposition, metadata-protocol patch,
spec patch (contract docblock).

Claude-Session: https://claude.ai/code/session_01DDZNkDVwPQnevTFcYE47H3
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/xl documentation Improvements or additions to documentation tooling and removed size/l labels Oct 3, 2026

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

2 participants