Skip to content

fix(runtime)!: callData asks each nested-relation condition's target its exposure before the engine reads it (#22823) - #22893

Merged
objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/issue-22823-calldata-relation-exposure
Oct 11, 2026
Merged

objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/issue-22823-calldata-relation-exposure

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #22823
Clause-②: no (narrowing)

This is census row 10 of #22661. The parent card stays open; the seat that owns it closes it.

What changed

callData (packages/runtime/src/action-execution.ts) hands a caller's where to the ObjectQL engine at two positions. The engine lowers a nested-relation condition by reading the related object as the caller (ObjectQL.lowerRelationConditions), while the exposure gate at the top of callData judges only the addressed object.

  • The query/find fallback. It runs when no protocol service resolves. This is the card's position.
  • The aggregate arm. It never takes the protocol service, so it is the engine's door on every assembly. The HTTP MCP aggregate_records tool reaches it through domains/mcp.ts. This is a bounded in-place fix, explained below.

At both positions, the new refuseUnservedRelationTargets now asks each condition's target the same gate callData applies to an addressed object (checkApiExposure), for list, before the engine is called. The conditions are found by the shared relationConditionSites from @objectstack/core. There is no second rule and no second walker.

A target the gate does not serve refuses the call with the data door's own envelope, built field for field like refuseUnservedRelationTarget:

  • apiEnabled: false answers 404 OBJECT_API_DISABLED.
  • A whitelist without list answers 405 OBJECT_API_METHOD_NOT_ALLOWED, with allowed.
  • Either way the error carries object (the target) and param (where.LOOKUP).

So one callData gives one answer whether or not the deployment registered the protocol slot. The same rule already holds for the not-found and delete bodies in this function.

The system bypass is decided once, by the same flag read for the addressed object and the relation targets (exposureGated). That keeps the system-context census row for callData describing one decision, and its declared counts unchanged.

Landing points beyond the declared file surface (read this)

  • packages/spec/src/api/error-code-ledger.zod.ts. Two value-only rows list OBJECT_API_DISABLED / OBJECT_API_METHOD_NOT_ALLOWED under @objectstack/runtime, plus their own @objectstack/spec patch changeset.
    • The rows are needed because check:error-code-provenance requires every stamping package to list the codes it stamps.
    • Reverse-verified: with the two rows deleted (through ablation-replace), the gate answers FAIL — 2 stamp site(s) of a registered code with no provenance row, naming action-execution.ts. With the rows present it is green.
    • This follows the family's precedent: 22737-spec-ledger-metadata-protocol-exposure-codes.md and the analytics one before it. REGISTERED_ERROR_CODES is unchanged, and no generated artifact moves.
  • The aggregate arm of the same function. This is the role file's bounded in-place exemption. All four conditions hold:

Measurements

Reach of the query fallback (the card's first step). Read at 75b6d6a048: no shipped composition reaches it.

  • Every non-test ObjectQLPlugin construction under packages/ and examples/ keeps the built-in protocol: cli serve.ts, runtime standalone-stack.ts, plugin-dev, the verify harness and the objectql kernel-factory.ts.
  • registerProtocol: false appears only in tests, plus one test-fixture string in cli.
  • examples/embed-objectql boots no kernel and no dispatcher.
  • The supported composition that does reach the fallback is an authored stack that passes registerProtocol: false with no MetadataProtocolPlugin. plugin.step2.test.ts pins that it boots protocol-free.
  • The null is stated, and the wiring is pinned anyway.

On a real engine (ObjectQL plus InMemoryDriver, a member context, no protocol service), measured with a one-off test file that was deleted afterwards:

arm build answer for a condition on an apiEnabled: false target
query fallback guard removed value matches: 1 source row; value does not match: 0 rows (a presence oracle on the hidden target)
query fallback this PR 404 OBJECT_API_DISABLED, where.hidden
aggregate guard removed count 1 against count 0 (the same oracle)
aggregate this PR 404 OBJECT_API_DISABLED

A get-only target under $or answered 1 row with the guard removed, and 405 OBJECT_API_METHOD_NOT_ALLOWED with this PR.

Tests (HEAD bd79104296)

The new file packages/runtime/src/action-execution-calldata-relation-target-exposure.test.ts has 13 cases:

  • 4 refused targets on the query fallback, each at the top level and under $or, asserting code, status, object, param and allowed, and that find is never called;
  • the same 4 on aggregate, asserting that aggregate is never called;
  • served controls (a list whitelist, no enable block) reach the engine with where unchanged on both arms;
  • a plain id comparand is served;
  • a system context is not asked;
  • the protocol path is untouched: findData gets the query verbatim and the engine is not called here.

Ablation, each leg through node scripts/ablation-replace.mjs, which wraps the test command and restores afterwards. Each leg reported the anchor count going from 1 to 0, a blob change, and a restore with the blob equal to HEAD and an empty git diff HEAD:

  • A. The fallback's call was replaced. The 4 query fallback refusal cases went red. The other 23 cases in the run stayed green: this file's 9 others (the aggregate pins, the controls and the protocol pin) and the 14 cases of action-execution-calldata-query.test.ts.
  • B. The aggregate call was replaced. The 4 aggregate arm cases went red; the other 23 stayed green.
  • Both legs were re-run at HEAD bd79104296, with the blob at HEAD 079f932376eb before and after. The first measurement, before the single-bypass refactor, gave the same split.
  • The subject resolves to src (an in-package relative import), so no dist was involved.

Package runs:

  • pnpm --filter @objectstack/runtime typecheck: exit 0.
  • @objectstack/runtime local project: 353 files, 5093 passed, 19 skipped, exit 0.
  • @objectstack/runtime repo project: 3 files, 751 passed.
  • @objectstack/core census pin second-object-read-exposure.pin.test.ts: 4 passed. relation-condition-sites.test.ts: 2 passed.
  • @objectstack/spec error-code-ledger.test.ts: 21 passed.
  • @objectstack/spec typecheck and @objectstack/core typecheck: exit 0.

Census row 10. The pin's CALLERS table is keyed on files that call referenceTargetOf, and callData does not call it. So row 10 is named in the docblock, as rows 8 and 9 are, and the relation-condition-sites.ts row now names all three doors that call the walk. The pin is green.

Gates

All 90 commands were run after the last commit, at HEAD bd79104296. That is the list re-derived on the real diff: the dispatch list's 66 plus 24 spec and repo gates. dispatch-gates --ran reports "90 derived, 90 run, 0 NOT-MEASURED", derived from the recorded exit codes. Per-command codes are in the report on the card.

  • check-empty-changeset exits 1, red by design (below).
  • The other 89 exit 0. That includes check:error-code-provenance, check:dispatcher-error-vocabulary, check:error-status-conformance, check:system-context-census and check:nul-bytes.
  • check:dual-build-cjs-loads answered PREREQUISITE NOT MET (exit 3) on the first pass, before every package's dist existed. It was green on the final pass.

Deliberate correction: check-empty-changeset is red by design

The second commit, 9e9aa0b7af, rewords one parenthetical in the pending .changeset/22739-import-reference-target-exposure.md ("Bindings", first bullet).

This edits a pending note this PR did not add, so check-empty-changeset stays red as a DELIBERATE CORRECTION. The seat confirms it with a same-head contract record.

Acceptance notes

  • Comment drift (not edited, outside the surface). The docblock of relationConditionSites (packages/core/src/utils/relation-condition-sites.ts) still names two doors and says it lives in core "because the two doors share no other package". A third door now calls it. Comment only; it does not affect behaviour. It is noted here rather than edited.
  • Which flag set the gate reads. The addressed-object gate in callData reads the metadata service only. The new target ask reads the engine registry first, then the metadata service, which is the schema the engine's lowering reads and the read the write mouths in this file already take. In a composition whose metadata slot does not carry the engine's objects, the addressed gate falls open while the target ask still bites. Not measured on a real composition.
  • The stdio bridge and this door both skip the ask for a system context; the data door does not. Here the skip uses callData's existing single bypass decision, and the bridge does the same.
  • Branch base. The branch is 2 commits behind origin/main. git merge-tree against 72b26ed4f5..40bc51f3f9 is clean. The only shared file is the error-code ledger, where feat(metadata-protocol,spec): importUnmanagedPackage, the unmanaged install mode's copy-once import (ADR-0131 D6, stage S1) #22879 edited the metadata-protocol block and this PR edits the runtime block.

Generated by Claude Code

…its exposure before the engine reads it

The query fallback (no protocol service) and the aggregate arm hand a
caller's where to the ObjectQL engine, which lowers a nested-relation
condition by reading the related object as the caller. Each condition's
target is now asked the gate the addressed object is asked, for list,
through the shared relationConditionSites walk, and an unserved target
refuses the call with the data door's 404 OBJECT_API_DISABLED / 405
OBJECT_API_METHOD_NOT_ALLOWED.

Claude-Session: https://claude.ai/code/session_01ADzJtzYTLUfgrRZHxkagkX
Co-authored-by: Claude <noreply@anthropic.com>
…or runtime's exposure codes, changesets

The census pin names callData as the third door that reaches the engine's
relation lowering through the shared walk, held by its own package's pin.
The error-code ledger lists OBJECT_API_DISABLED and
OBJECT_API_METHOD_NOT_ALLOWED under @objectstack/runtime, the provenance
check:error-code-provenance requires of an emitting package.

Claude-Session: https://claude.ai/code/session_01ADzJtzYTLUfgrRZHxkagkX
Co-authored-by: Claude <noreply@anthropic.com>
… import as an unjudged protocol

Since #22800 the identity import's run protocol carries the declaration
reader, so the parenthetical stated a false example. Only that
parenthetical is reworded; the rule it illustrates is unchanged. A
deliberate correction of a pending note this PR did not add.

Claude-Session: https://claude.ai/code/session_01ADzJtzYTLUfgrRZHxkagkX
Co-authored-by: Claude <noreply@anthropic.com>
…ddressed object and the relation targets

One read of the execution context's system flag, so the system-context
census row for callData's exposure bypass keeps describing one decision.

Claude-Session: https://claude.ai/code/session_01ADzJtzYTLUfgrRZHxkagkX
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling labels Oct 11, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/runtime, @objectstack/spec, touching 8 documentable anchor(s).

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

  • content/docs/api/client-sdk.mdx (via ERROR_CODE_LEDGER (symbol, a top-level const object))
  • content/docs/api/declarative-endpoints.mdx (via callData (symbol, a top-level function))
  • content/docs/api/error-catalog.mdx (via ERROR_CODE_LEDGER (symbol, a top-level const object))
  • content/docs/api/error-handling-server.mdx (via ERROR_CODE_LEDGER (symbol, a top-level const object))
  • content/docs/kernel/contracts/data-engine.mdx (via ERROR_CODE_LEDGER (symbol, a top-level const object))
  • content/docs/permissions/authorization.mdx (via callData (symbol, a top-level function))
  • content/docs/permissions/system-context.mdx (via callData (symbol, a top-level function))
  • content/docs/protocol/kernel/http-protocol.mdx (via callData (symbol, a top-level function), OBJECT_API_DISABLED (literal, a string literal in ERROR_CODE_LEDGER; a string literal in unservedRelationTargetError), OBJECT_API_METHOD_NOT_ALLOWED (literal, a string literal in ERROR_CODE_LEDGER; a string literal in unservedRelationTargetError))
  • content/docs/ui/setup-app.mdx (via OBJECT_API_DISABLED (literal, a string literal in ERROR_CODE_LEDGER; a string literal in unservedRelationTargetError))

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

  • content/docs/releases/v16.mdx (via OBJECT_API_METHOD_NOT_ALLOWED (literal, a string literal in ERROR_CODE_LEDGER; a string literal in unservedRelationTargetError))
  • content/docs/releases/v17/17-0.mdx (via ERROR_CODE_LEDGER (symbol, a top-level const object))
  • content/docs/releases/v17/17-1.mdx (via ERROR_CODE_LEDGER (symbol, a top-level const object))
  • content/docs/releases/v17/17-4.mdx (via ERROR_CODE_LEDGER (symbol, a top-level const object))

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
  • 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 2e11d9006cdc9548dbb1f3883687fdbe2bfe21f7 → packageMentionDocs.

Which tree this was computed on

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

node scripts/docs-audit/affected-docs.mjs --json 2e11d9006cdc9548dbb1f3883687fdbe2bfe21f7

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

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

DELIBERATE CORRECTION — Check Changeset is red by design on this PR. domain:engine seat 2 · session_01ADzJtzYTLUfgrRZHxkagkX · 2026-10-11T20:43Z.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: bd79104296c7cb6b81710c9312d5f92e102df6af
Local-runs: none

Read-only, on the card (#22823: body, release 6110112147, claim 6112207746, os-dev-report 6113489644, file-surface addendum 6113535816), the PR (body, the seat's DELIBERATE CORRECTION note 6113547162, the docs-drift comment, 7 files, 4 commits), the net diff against origin/main (merge base 75b6d6a048; 7 files, +341 / −8, none generated, none governed: Governed Surface Queue Guard success), the check-runs on the head, and the origin/main files the brief names. #22800's ACCEPT 6111874171 and its merge f9ebb7dd31 read as cited context. Classes, positions and functions only.

Check-runs on the head, at the final read: 26 success, 3 skipped (Build Docs, Console Pin Gate, Packed-tarball smoke), 1 failure (Check Changeset, by design, judged in ③), and 4 still in progress: Test Core (2/6), Test Core (3/6), Test Core (5/6), Test Core (6/6). Lint & Repo Gates (which carries check:error-code-provenance, check:system-context-census, check:error-status-conformance, check:dispatcher-error-vocabulary), the four Type Check jobs, Temporal Conformance, Dogfood Regression Gate (all three), Test Core (1/6) and (4/6) are green. The new runtime test file rides a Test Core shard, so this PASS stands on the four named shards completing green; a red one re-opens the verdict.

① Derived judgments

Accept-set change the diff implies: one narrowing, at two arms of one function, callData (packages/runtime/src/action-execution.ts), for a non-system caller only. Public-surface change: none. callData is not on the runtime entry (packages/runtime/src/index.ts exports nothing from action-execution), resolveWriteResponseSchema keeps its signature (docblock extended), the spec change is two value-only rows in an existing const, and the core change is a test file. Nothing widens anywhere in the diff.

  1. The query fallback refusal — right. The new refuseUnservedRelationTargets is called inside the if (ql) branch of the query / find arm, after the fallback's own unservable-key refusal and before ql.find, on bag.where, which is the where the engine was already handed through findOpts(bag). The data door branch above it (protocol.findData) is before the fallback and unchanged.
  2. The aggregate arm refusal — right, as the bounded in-place fix the file-surface addendum accepted on the card. The call sits before engine.aggregate, on params.where, the where the arm already forwarded. The arm takes no protocol service, so it is the engine's door on every assembly, which makes it the more reachable of the two positions (the HTTP MCP aggregate tool reaches it); the claim's function-level description now reads "and its aggregate arm" on the card. The ask covers the top-level where only, the form security(mcp): a nested-relation filter condition through the MCP stdio data bridge is evaluated without asking the target's exposure (census row 8 of #22661) #22777's stdio bridge pins for its aggregate verb; the dev's statement that the engine lowers relation conditions in where and never inside an aggregation's own filter is inherited from that pinned form and was not re-read here (the engine's lowering is outside this review's read set). Named, not a finding.
  3. The envelope — right. unservedRelationTargetError is the data door's refuseUnservedRelationTarget (packages/metadata-protocol/src/protocol.ts) field for field: code and status by the gate's verdict (OBJECT_API_DISABLED 404 for apiEnabled: false, else OBJECT_API_METHOD_NOT_ALLOWED 405), object the target (not the addressed object), param the condition's position spelled where.LOOKUP (the stdio bridge's spelling; the data door spells its own REST parameter, filter.LOOKUP), allowed only on a 405, the effective operation set (the test pins a create-only whitelist answering the derived pair, as the data door's effectiveOperationsArray does). The two message templates and the consequence tail are the data door's own text. The addressed-object gate of the same function throws a bare statusCode/message object; the target refusal does not copy that shape, and that is right: the protocol path's refusal of the same condition already reaches the dispatcher through callData as this Error envelope, so the fallback now gives the answer the path it stands in for gives (check:error-status-conformance and check:dispatcher-error-vocabulary ride in Lint & Repo Gates, green).
  4. System callers — right for this door, residual named. The bypass is decided once, exposureGated = !executionContext?.isSystem, the flag the addressed-object gate already read, and both calls of the target ask are under it. That is the stdio bridge's rule too (enforceApiExposure returns on isSystem), and it keeps callData's system-context census row describing one decision (check:system-context-census in Lint & Repo Gates, green; the test's system-context control reaches the engine). Residual: the data door judges a system context for the same condition and this door does not; that split pre-exists for the addressed object and this PR does not move it.
  5. The protocol path untouched — right. Only context lines of the findData branch appear in the diff; the test pins that with a protocol service resolved findData receives the query verbatim and find is never called.
  6. One gate and one walker — right. The gate is checkApiExposure, the function the addressed object was already judged by in this file; no new decision function is written. RELATION_CONDITION_TARGET_ACTION is the action name query handed to that gate (what the stdio bridge's GATED_ACTIONS.query hands its gate), and RELATION_CONDITION_TARGET_OPERATION is read off the spec's DATA_ACTION_TO_API_OPERATION and used only in the message; the data door's own constant is the same list. The walker is relationConditionSites, imported from @objectstack/core (exported from core's entry; @objectstack/core is already a workspace:* dependency of @objectstack/runtime, so check:undeclared-dep-imports has nothing to say), fed the filtered object's fields map and the raw filter, as both other doors feed it. No second walk exists in runtime. The field map and each target's declaration come from the existing resolveWriteResponseSchema (engine registry first, then metadata, undefined when neither), and the new function falls open on undefined as checkApiExposure does; a plain id comparand or an operator-only bag on a lookup key is no site, pinned by a control.
  7. The census pin's row-10 handling — right. CALLERS in packages/core/src/security/second-object-read-exposure.pin.test.ts is keyed on files that call the arbiter referenceTargetOf; callData never calls it (the target is resolved inside relationConditionSites), so a row would be refused by the pin's own stale-caller check. Row 10 is named in the docblock's "what the discriminator does NOT see" paragraph in the shape rows 8 (security(mcp): a nested-relation filter condition through the MCP stdio data bridge is evaluated without asking the target's exposure (census row 8 of #22661) #22777) and 9 (security(auth): the identity import hands runImport a protocol without getMetaItem, so its reference cells skip the target's exposure ask (census row 9 of #22661) #22800) already take there, held by the runtime's own pin (action-execution-calldata-relation-target-exposure.test.ts, named), and the relation-condition-sites.ts row's reason now names all three doors in place of "both doors". The pin's arbiter-call scan is unaffected (no new caller, no stale one).
  8. The two ledger rows — right. OBJECT_API_DISABLED and OBJECT_API_METHOD_NOT_ALLOWED are added inside the @objectstack/runtime block of ERROR_CODE_LEDGER (packages/spec/src/api/error-code-ledger.zod.ts), which listed neither before; the four blocks that already list them (rest, metadata-protocol, service-analytics, mcp) are untouched, so the registered set is unchanged and the rows are provenance only, with a comment naming the stamping function and both arms. check:error-code-provenance is in Lint & Repo Gates, green; the dev's reverse-verification (two stamp sites named with the rows removed) is consistent with that.

Tests, read against the diff: 13 cases in the new runtime file; the four refused classes (apiEnabled: false; whitelists of create only, empty, get only) at the top level and under a combinator on the fallback, the same four on aggregate, each asserting the full envelope and that the engine verb was never called; two served controls reaching the engine with where unchanged on both arms; the plain-id control; the system control; the protocol-path pin. The ablation legs (A: fallback call; B: aggregate call) are the dev's; CI's Test Core shards are the verdict on the file.

② Semver level

  • @objectstack/runtime minor, .changeset/22823-calldata-fallback-relation-exposure.md — right. The diff narrows callData's accept set at the two arms for non-system callers; the changeset carries fix(runtime)!:, Clause-②: no (narrowing), the BREAKING line, the ADR-0087 marker not-required (no-migration-prescription) with its reason (read and accepted by check-adr-0087-registration in the Check Changeset job, visible in its log; its "not on the export surface" claim holds against the runtime entry), FROM and TO matching the two positions and the envelope, a migration (filter on the lookup's stored id, or grant list on the related object) matching the code's own consequence text, Bindings matching ① 3, 4 and 6, and the measured producers with the NOT MEASURED bound. minor for a narrowing is the launch-window convention (check-changeset-no-major in the same job: "narrowing — a BREAKING change; during the launch window it ships minor").
  • @objectstack/spec patch, .changeset/22823-spec-ledger-runtime-exposure-codes.md — right. Two value-only rows in an existing array literal; no export, schema, type or generated artifact moves. The no-major gate's named residual (a second widening graded patch beside the minor would escape it) is answered by the diff: the spec change accepts nothing, and nothing else in the diff widens.
  • No @objectstack/core changeset — right. Its only change is a test file.
  • Clause-②: line — consistent. PR body line 2 Clause-②: no (narrowing); the runtime changeset's line 7 Clause-②: no (narrowing); the claim 6112207746 Clause-②: no with the narrowing arm stated as its next bullet. One declaration, one arm, matching the diff.

③ Boundary flags

open_questions: none in the os-dev-report.

deviations, each:

  1. File surface widened (spec ledger rows plus spec changeset; the aggregate arm) — answered on the card: the file-surface addendum 6113535816 added both to the claim (cross-lane domain:spec as the ledger-append class; the aggregate arm under the bounded in-place exemption). Both judged right in ① 2 and ① 8.
  2. Dispatch "stop on a breach" versus the role file's bounded fix — answered: every breach was declared in the report and the PR body, and the seat accepted both in the addendum. No silent widening.
  3. Refusal envelope (the data door's, not the addressed gate's bare object) — answered, right (① 3).
  4. System context skips the target ask — answered, right for this door, residual named (① 4).
  5. Branch not merged with origin/main — answered: main has moved further since the report; GitHub reports the PR mergeable, and CI computed on the merge of the head into the current base (the docs-drift comment names that merge commit), so the derived gates measured the merged tree. The only shared file is the ledger, in a different package block.
  6. Commit trailers — a repo-convention matter with no bearing on the contract axes; the session-URL trailer and the model-free co-author line are present on 9e9aa0b as read.
  7. check-empty-changeset red by design — the DELIBERATE CORRECTION, judged below.
  8. Cleanup — the dev's statement about its worktree; nothing in the diff to judge.

out_of_scope_findings, each:

  1. relationConditionSites's docblock (packages/core/src/utils/relation-condition-sites.ts) still says two doors share no other package — answered as a note, not a defect: comment only, outside the claim's surface; the pin's CALLERS row for that file now names three doors, so the drift is confined to the core source's own docblock. Carrier: the next family PR that touches packages/core/src/utils/, at the seat's choice.
  2. callData's addressed-object gate reads the metadata service only, while the target ask (and the engine's lowering) reads the engine registry first — escalated to the adopting seat as a measure-first question, not filed here: a read-only inference about the pre-existing addressed gate on a composition whose metadata slot does not carry the engine's objects, NOT MEASURED on a real composition, so not yet a class (a) filing under Prime Directive 10. This PR does not widen it; the target ask reads the schema the engine's lowering reads, which is the right source for the target. The seat decides whether a measurement card is worth its cost.

The DELIBERATE CORRECTION (the seat's PR note 6113547162; PR body section "Deliberate correction"; carried from #22800's ACCEPT 6111874171). Check Changeset is red for exactly one reason: check-empty-changeset.mjs's foreign-changeset refusal naming .changeset/22739-import-reference-target-exposure.md (the job log's single error line; the same job's ADR-0087 registration and no-major steps pass). Commit 9e9aa0b7af is one file, one line (+1 / −1), and the net diff of that file against origin/main is that same single line, inside "Bindings", first bullet. Judged clause by clause:

The correction is confirmed. Per the seat's note, Check Changeset runs on pull_request only and is not a required context; this record is the same-head confirmation that note asked for, and the queue leg does not re-run that gate.

Advisory, no action: the docs-drift comment lists nine hand-written pages naming callData or the ledger; the diff edits no doc, the behaviour added is the refusal those pages already describe for the data door, and Flag docs affected by code changes is green.

Implemented-by: claude/issue-22823-calldata-relation-exposure
Reviewed-by: session_01ADzJtzYTLUfgrRZHxkagkX

VERDICT: PASS

Adopted by domain:engine#2 (session_01ADzJtzYTLUfgrRZHxkagkX) at 2026-10-11T21:12Z from an isolated at-tier reviewer whose transcript shows read-only tool use (its one write is its own scratch record). The four Test Core shards pending at the record's final read have since concluded: on this head all six Test Core shards and all seven required contexts are success; 31 success, 3 skipped, 1 failure (Check Changeset, the DELIBERATE CORRECTION judged above).

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