Skip to content

fix(metadata-protocol)!: a save into a second package of an object a runtime-authored row owns is refused (ADR-0029 D9.9) - #23510

Merged
objectstack-fleet[bot] merged 7 commits into
mainfrom
claude/issue-23416-runtime-owner-overlay-refusal
Oct 14, 2026
Merged

objectstack-fleet[bot] merged 7 commits into
mainfrom
claude/issue-23416-runtime-owner-overlay-refusal

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #23416
Clause-②: no

What this does

classifyObjectContribution (packages/metadata-protocol/src/protocol.ts) asked the registry one question, getPackagedObjectOwner, which answers for a CODE artifact only. An object that package P's sys_metadata row created at runtime has no packaged owner, so a save of it naming package Q classified as own and was written. The write-through then called registerObject(…, Q, …, 'own'), the registry's single-owner rule (ADR-0029 D3) refused it, and applyObjectRegistryMutation's best-effort console.warn swallowed the refusal. The save answered 200 while sys_metadata held a Q-bound row the registry could not hold: the disagreement ADR-0029 D9.9 (overlayPackageMismatchError) exists to prevent.

The classifier now asks one more question when there is no packaged owner: the registry's own own contributor (getObjectOwner(name), keyed on the object name exactly as registerObject keys its contributors). When that owner is bound to a real package and the row's binding names a different package, it answers the existing mismatch arm. So the save door refuses with the code-owner path's refusal and prescription (OBJECT_OVERLAY_PACKAGE_MISMATCH / 422), before ensureOverlayIndex and every put. One arm answers both owners; no second refusal path, no new error code, no new message.

Unchanged by construction:

  • A binding equal to the owner's package: own, an ordinary re-registration (the owner's re-save stays 200).
  • A save naming no package: still own. A direct publish-mode save naming no package is addressed to the owner's row before this runs (packagelessAddress).
  • An owner keyed by the sys_metadata sentinel (a package-less runtime object) or by no package: left to today's verdict. It names no package the prescription could direct a save to. See the acceptance notes.
  • A code-packaged owner: the existing table, untouched.

Premise check (the seat's ruling premise)

The ruling needed a runtime owner's package that can be read deterministically at that point of saveMetaItem. It holds. The registry keeps exactly one own contributor per object name (registerObject enforces it), and that contributor is the very fact registerObject judges an incoming own claim against. The arm predicts the registry's own refusal and judges nothing else. The object write-through is not kernel-gated (applyRegistryWriteThrough's object branch), so every saved runtime object is in the registry on every kernel topology. Measured at the HTTP door on both topologies (below).

Measured before the fix (real kernel, base cb9841aa25)

ObjectKernel + ObjectQLPlugin + an in-memory driver, saveMetaItem as PUT /api/v1/meta/object/NAME?package=Q calls it:

setup Q's save rows stored registry owner warn
P saved qa_probe_gadget success: true, "Saved object 'qa_probe_gadget' (env-wide, package=com.qa.q, state=active)" com.qa.p, com.qa.q com.qa.p registerObject failed … already owned by package "com.qa.p" … cannot claim ownership

Measured after the fix

  • Real kernel pin (new file, 4 cases): Q's save answers OBJECT_OVERLAY_PACKAGE_MISMATCH / 422 (with packageId Q and ownerPackageId P). No row and no history entry is stored for Q. The registry and sys_metadata both still say P. CONTROLS: P's own re-save answers success and the registry follows it; a save naming no package addresses P's row; a code-packaged owner answers NOT_OVERRIDABLE / 403, as it did before this change (measured on the base).
  • HTTP door, one-time reading (the real RestServer PUT /api/v1/meta/object/:name route over a real better-sqlite3 engine, control-plane and environment kernels): P 200; Q 422 with body code: OBJECT_OVERLAY_PACKAGE_MISMATCH and the D9.9 sentence; P again 200; stored bindings [com.probe.p].
  • Other faces of the same door, one-time readings on the real kernel, before → after:
    • a draft save into Q: success with a Q draft row stored → 422, no draft row;
    • duplicatePackage P → Q of an object whose name lacks P's namespace prefix (so the copy keeps the name): listed in copied with a Q row the registry never held → listed in failed with the D9.9 text, nothing stored;
    • boot (loadMetaFromDb) with a stored Q row beside P's: P registered and the Q row counted in errors either way. The warn line changes from code=OBJECT_OWNERSHIP_CONFLICT to code=OBJECT_OVERLAY_PACKAGE_MISMATCH.

Fixtures re-judged (the rule's consumer radius)

Two suites built the two-package runtime shape by saving the second package's row through the door. Both are RE-SEEDED, not weakened: the first package's row is still saved through the door, and the second row is written straight into the store, as a row stored before this refusal existed.

Each re-seeded fixture was ablated against the behaviour it pins, to show it still bites (mutation through scripts/ablation-replace.mjs, dist/ proven by scripts/ablation-dist-preflight.mjs, restored blob equal to HEAD and the tree clean):

ablation mutated restored
this PR's arm (mismatch return replaced by own), new pin 1 failed / 3 passed (the refusal case: "expected null to be an instance of Error") 4 passed
#23408's heal (its remaining-rows branch made unreachable), heal file 5 failed / 10 passed (the four re-seeded object cases + the dashboard case) 15 passed
#23464's drop refusal (its remaining-rows check made unreachable), rest file 3 failed / 2 passed 5 passed

Tests (consumer contract face)

All readings below are at this PR's head 55bb8619e7, which merges origin/main at 394f65e89. Each command ran under the shared verify lock (scripts/pm/os-verify-lock.sh), and each verdict is that wrapper's VERDICT command-exit line.

command result
pnpm --filter @objectstack/metadata-protocol exec vitest run --maxWorkers=2 exit 0 · 236 files passed, 3 skipped · 28397 tests passed, 19 skipped
pnpm --filter @objectstack/metadata-protocol typecheck exit 0
pnpm --filter @objectstack/objectql exec vitest run --project local --maxWorkers=2 exit 0 · 416 files · 8097 tests passed
pnpm --filter @objectstack/objectql typecheck (tsc + check:test-typecheck) exit 0 · the test layer compiles, debt unchanged
pnpm --filter @objectstack/rest exec vitest run --project local --maxWorkers=2 exit 0 · 283 files · 5852 tests passed, 326 skipped (first attempt: lock exit 99, re-run with the same slot)
pnpm --filter @objectstack/rest typecheck (tsc + check:test-typecheck) exit 0

The objectql suite's repo project, which holds the tests that read outside the package, is CI's (test:repo). The red-before reading for the new pin was taken at the base: 2 failed of 4. The refusal case answered null (a success). The code-owner control then read NOT_OVERRIDABLE, and that measured value is what the control now pins.

Gates

Derived with node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack at 55bb8619e7: 67 commands. All 67 ran at that head with exit codes recorded, and --ran reconciles: "67 derived, 67 run, 0 NOT-MEASURED, 0 UNRUN". Every command exited 0. check:type-check-debt first got lock exit 99 (its own PREREQUISITE NOT MET, exit 3) and passed when re-run under the lock: "1 ledger entr(ies) re-measured … none above its recorded number". On an earlier head, check-adr-0087-registration refused a ## FROM → TO section label beside no-migration-prescription. The changeset now states the FROM/TO as request answers, and the gate passes (not-required (no-migration-prescription)).

CI owns everything the derivation does not place: the artifact-roster families, the wide-population families, the type-check lanes, Test Core, Build Core, dogfood and the temporal conformance job. packages/runtime and packages/qa/dogfood suites were not run locally. A heuristic sweep of their tests for a second package's object save through the door found none.

origin/main has moved past 394f65e89 by two commits (ad5a7d7c39 spec page-action rows, 111b5821ce a runtime test). Neither touches this diff's packages or behaviour; CI's merge ref covers them.

Acceptance notes

  • Package-less owner, same family, not fixed here. An object a row bound to NO package owns (sys_metadata sentinel) behaves as this card did before the fix. On a real kernel at cb9841aa25, a package-less save then a ?package=Q save answered success with rows [null, com.qa.q], while the registry kept the sentinel owner and warned already owned by package "sys_metadata". The arm deliberately excludes it: the D9.9 prescription would tell the author to bind to 'sys_metadata', which is not a package, and the right remedy (orphan adoption, or saving without a package) is a wording decision. Reported to the seat for the D9.9 family's close-out, not filed by this PR.
  • duplicatePackage now reports what it used to fake. A source object whose name lacks the source namespace prefix keeps its name in the copy. Before this PR that copy was a row the registry refused, over the same physical table (object name = table name). Now it lands in failed[]. The docblock of duplicatePackage already calls a same-name copy a collision. The prescription text it carries ("bind … to P, or have Q extend") is the code-owner path's and fits the save door better than the duplicate face; renaming the object is the remedy there.
  • The prescription's second remedy is a code-package affordance. "have Q extend the object instead" names objectExtensions, which a code package declares. No runtime metadata door authors an extension. The ruled prescription is reused unchanged; the first remedy (bind to P) is always available.
  • Callers left as they are. applyObjectRegistryMutation and loadMetaFromDb already route a mismatch to their warn and per-record errors. The heal comment in restoreArtifactRegistryView ("a second package's runtime-authored row never registered (its write-through was refused at save)") stays true of stored rows and sits in the delete heal that deleteMetaItem runs, outside this card's surface.

Generated by Claude Code

…runtime-authored row owns is refused (ADR-0029 D9.9)

classifyObjectContribution asked only getPackagedObjectOwner, which answers
for code artifacts, so a save naming package Q of an object package P's
sys_metadata row owns was written and then refused by the registry's
single-owner rule into a best-effort warn: 200 while sys_metadata held a row
the registry could not hold. The classifier now also asks the registry's own
contributor (getObjectOwner) and answers the same mismatch arm, so the save is
refused with OBJECT_OVERLAY_PACKAGE_MISMATCH / 422 before any write.

Claude-Session: https://claude.ai/code/session_0121ooBYRmS8ziFXm9ai5YHm
Co-authored-by: Claude <noreply@anthropic.com>
…hat the door refuses it

The save door refuses a save into a second package of an object a
runtime-authored row owns (ADR-0029 D9.9), so the fixture saves the first
package's row through saveMetaItem and seeds the second directly, with the
repository's own checksum. A fixture change, not a weakened pin.

Claude-Session: https://claude.ai/code/session_0121ooBYRmS8ziFXm9ai5YHm
Co-authored-by: Claude <noreply@anthropic.com>
…f a runtime-owned object

Claude-Session: https://claude.ai/code/session_0121ooBYRmS8ziFXm9ai5YHm
Co-authored-by: Claude <noreply@anthropic.com>
…directly, now that the door refuses it

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

The change prescribes no rewrite of metadata or code, so the disposition is
no-migration-prescription and the body says FROM/TO in bullets, as other
accept-set narrowings do.

Claude-Session: https://claude.ai/code/session_0121ooBYRmS8ziFXm9ai5YHm
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 14, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/metadata-protocol, touching 3 documentable anchor(s).

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

  • content/docs/api/error-catalog.mdx (via sys_metadata (literal, a string literal in a comment in ObjectStackProtocolImplementation; a string literal in classifyObjectContribution))
  • content/docs/api/index.mdx (via sys_metadata (literal, a string literal in a comment in ObjectStackProtocolImplementation; a string literal in classifyObjectContribution))
  • content/docs/automation/flows.mdx (via sys_metadata (literal, a string literal in a comment in ObjectStackProtocolImplementation; a string literal in classifyObjectContribution))
  • content/docs/concepts/metadata-lifecycle.mdx (via ObjectStackProtocolImplementation (symbol, a top-level class), sys_metadata (literal, a string literal in a comment in ObjectStackProtocolImplementation; a string literal in classifyObjectContribution))
  • content/docs/data-modeling/drivers.mdx (via sys_metadata (literal, a string literal in a comment in ObjectStackProtocolImplementation; a string literal in classifyObjectContribution))
  • content/docs/data-modeling/objects.mdx (via sys_metadata (literal, a string literal in a comment in ObjectStackProtocolImplementation; a string literal in classifyObjectContribution))
  • content/docs/deployment/cli.mdx (via sys_metadata (literal, a string literal in a comment in ObjectStackProtocolImplementation; a string literal in classifyObjectContribution))
  • content/docs/deployment/environment-variables.mdx (via sys_metadata (literal, a string literal in a comment in ObjectStackProtocolImplementation; a string literal in classifyObjectContribution))
  • content/docs/deployment/validating-metadata.mdx (via sys_metadata (literal, a string literal in a comment in ObjectStackProtocolImplementation; a string literal in classifyObjectContribution))
  • content/docs/kernel/cluster.mdx (via sys_metadata (literal, a string literal in a comment in ObjectStackProtocolImplementation; a string literal in classifyObjectContribution))
  • content/docs/kernel/contracts/metadata-service.mdx (via sys_metadata (literal, a string literal in a comment in ObjectStackProtocolImplementation; a string literal in classifyObjectContribution))
  • content/docs/kernel/services-checklist.mdx (via sys_metadata (literal, a string literal in a comment in ObjectStackProtocolImplementation; a string literal in classifyObjectContribution))
  • content/docs/permissions/authorization.mdx (via sys_metadata (literal, a string literal in a comment in ObjectStackProtocolImplementation; a string literal in classifyObjectContribution))
  • content/docs/permissions/permission-sets.mdx (via sys_metadata (literal, a string literal in a comment in ObjectStackProtocolImplementation; a string literal in classifyObjectContribution))
  • content/docs/plugins/packages.mdx (via sys_metadata (literal, a string literal in a comment in ObjectStackProtocolImplementation; a string literal in classifyObjectContribution))

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

  • content/docs/releases/implementation-status.mdx (via sys_metadata (literal, a string literal in a comment in ObjectStackProtocolImplementation; a string literal in classifyObjectContribution))
  • content/docs/releases/v16.mdx (via ObjectStackProtocolImplementation (symbol, a top-level class), sys_metadata (literal, a string literal in a comment in ObjectStackProtocolImplementation; a string literal in classifyObjectContribution))
  • content/docs/releases/v17/17-0.mdx (via ObjectStackProtocolImplementation (symbol, a top-level class), sys_metadata (literal, a string literal in a comment in ObjectStackProtocolImplementation; a string literal in classifyObjectContribution))
  • content/docs/releases/v17/17-3.mdx (via sys_metadata (literal, a string literal in a comment in ObjectStackProtocolImplementation; a string literal in classifyObjectContribution))
  • content/docs/releases/v17/17-5.mdx (via sys_metadata (literal, a string literal in a comment in ObjectStackProtocolImplementation; a string literal in classifyObjectContribution))
  • content/docs/releases/v17/17-6.mdx (via sys_metadata (literal, a string literal in a comment in ObjectStackProtocolImplementation; a string literal in classifyObjectContribution))
  • content/docs/releases/v17/17-7.mdx (via sys_metadata (literal, a string literal in a comment in ObjectStackProtocolImplementation; a string literal in classifyObjectContribution))
  • content/docs/releases/v17/index.mdx (via sys_metadata (literal, a string literal in a comment in ObjectStackProtocolImplementation; a string literal in classifyObjectContribution))

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 — 11 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 f9b421716dd79df6c7c3a34bbd5a3a9f425e84f4 → packageMentionDocs.

Which tree this was computed on

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

node scripts/docs-audit/affected-docs.mjs --json f9b421716dd79df6c7c3a34bbd5a3a9f425e84f4

⚠️ 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 f9b421716dd79df6c7c3a34bbd5a3a9f425e84f4 → 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

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 55bb8619e73ad18a8a08a776e7bdee92bca25d03
Local-runs: none

① Derived judgments

Face: a narrowing of the metadata save door's accept set for object (ADR-0029 D9.9). Read from the net diff of the head against main at merge-base 394f65e89 (5 files, 359 added, 23 removed; the GitHub diff and a local merge-base diff are byte-equal), card #23416 with every comment but the dispatching seat's own conclusions, and the PR body.

  1. classifyObjectContribution's runtime-owner arm — right.

    • Keying: getObjectOwner(fqn) reads objectContributors.get(fqn) directly, and registerObject keys a contributor on computeFQN(namespace, schema.name), which is identity (packages/objectql/src/registry.ts, computeFQN returns the short name). Both seams that register a sys_metadata object row (applyObjectRegistryMutation and loadMetaFromDb) pass namespace undefined, so the owner the arm reads sits under the same key the registration it predicts would use. The arm predicts the registry's own ADR-0029 D3 refusal exactly: registerObject throws ObjectOwnershipConflictError when the sitting own contributor's packageId differs from the incoming one and the late-install branch does not fire (both bodies carry _provenance: 'org'), which is binding !== runtimeOwnerPackageId. The classifier is asked with request.name and the registration carries item.name, the same pairing the packaged-owner arm has carried since D9.8; nothing new there.
    • Exclusions, each right: (a) an owner keyed 'sys_metadata' (a package-less runtime object) is left to today's verdict, because the prescription would name the sentinel as a package; escalated (③.7). (b) an empty binding: at the save door a publish save naming no package is re-addressed by packagelessAddress to the one bound row before the classifier runs (the request.packageId == null, source === undefined, writeFace !== 'package-duplicate' block), several bindings are refused as ambiguous, a draft save's binding is resolved by the repository and a draft never registers, and the server-stated rewrites pass null only for a package-unbound row; so no own verdict on an empty binding reaches registerObject against a foreign owner. (c) the owner's own package: own, the same-package re-registration branch, pinned by a control. (d) an owner with no packageId at all (a bare programmatic registerObject(schema)) is also left to today's verdict; it is not a sys_metadata shape, since both hydration seams always bind a string, and the docblock says so.
    • Reusing the D9.9 refusal: one arm, the existing code OBJECT_OVERLAY_PACKAGE_MISMATCH, status 422, the existing sentence; ownerPackageId names the runtime owner's package, so "bind the customization to 'P'" reads correctly to a runtime author. The second remedy, "have 'Q' extend the object", names a code-package affordance; a wording debt of the ruled text, not of this PR (③.9).
  2. The three callers — each as the changeset states.

    • Save door (saveMetaItem): the classifier runs after packagelessAddress and before ensureOverlayIndex() and repo.put, the method's first writes, so the 422 lands before any row and any history entry. Pinned on a real kernel (no sys_metadata or sys_metadata_history row for Q; registry and store still say P).
    • Write-through (applyObjectRegistryMutation): a mismatch throws overlayPackageMismatchError into the existing best-effort console.warn. Unreachable from saveMetaItem, which refuses upstream; reachable from rollback and draft promotion, where the registry's own refusal landed in the same warn before. A log line only.
    • Boot hydration (loadMetaFromDb): a mismatch throws into the per-record catch, is counted in errors, and the first-hydrated row stays registered, as before; the Failed to hydrate warn now carries code=OBJECT_OVERLAY_PACKAGE_MISMATCH in place of OBJECT_OWNERSHIP_CONFLICT. Stated in the changeset.
  3. duplicatePackage, copied to failed[] — correct, and at least not worse. The copy's saveMetaItem call sits in the catch that pushes failed[] with the declared refusal text, and the receipt's success holds only when failed is empty and copied is not. Before this PR a same-name copy (a source object whose name lacks the source namespace prefix) was listed in copied with a Q-bound row the registry refused over the same physical table, the D9.9 disagreement under a success receipt. Reporting the failure is the right direction (Prime Directive chore: version packages #10, absence must be loud). The prescription's fit for the duplicate face is metadata-protocol: duplicatePackage cannot duplicate an object whose name lacks the source namespace prefix — the copy keeps the name and becomes a row the registry never holds, or (after PR #23510) a failure with a prescription that does not fit #23511, not this PR.

  4. The two re-seeded fixtures — real setup changes, assertions untouched.

    • packages/objectql/src/protocol-delete-object-registry-heal.test.ts, twoPackageObject: the first package's row is still saved through the door; the second is written into the store as the first row's own shape, rebound and re-labelled, with the repository's checksum (hashSpec, proved on the row the door just wrote) under a distinct rowKey (the key includes package_id). The fixture now also asserts the registry holds the first package. Every delete and heal assertion below it is unchanged, and both orders still measure the heal.
    • packages/rest/src/meta-delete-drop-storage-other-rows.test.ts: seedUnboundRow is generalised to seedStoredRow(packageId), the same direct engine.insert('sys_metadata', …) with seedUnboundRow kept as its null case; twoPackageObject saves P through the door and seeds Q. The registry state equals what the refused write-through left (P), and every [#23415] assertion is untouched.
  5. New pin packages/objectql/src/protocol-runtime-owner-overlay-refusal.test.ts: a real ObjectKernel with ObjectQLPlugin and a memory driver; the refusal (code, status, packageId, ownerPackageId, no row, no history, registry and store agree on P) and three controls (the owner's re-save answers success and the registry follows; a package-less save is addressed to P's row; a code-packaged owner still answers NOT_OVERRIDABLE / 403). @objectstack/metadata-core (hashSpec) is a declared dependency of @objectstack/objectql.

  6. Public surface: classifyObjectContribution is private; no export, spec key or stored shape moves; every type except object is untouched (the classifier runs only for singularType === 'object'). No governed path in the file list.

② Semver level

  • @objectstack/metadata-protocol minor, carrying **BREAKING**, a ! summary and Clause-②: no (narrowing) — right. A narrowing of a published door's accept set is breaking, and minor is the launch-window level (check-changeset-no-major). No other published package changes: the other four files are tests and the changeset.
  • ADR-0087 marker not-required (no-migration-prescription) — right on substance. No spec key, authorable spelling, export or stored shape is added, removed, renamed or re-shaped, so objectstack migrate meta has nothing to rewrite; the remedy is on the request a caller sends. A Q-bound row stored before this change was never held by the registry and was already counted in boot errors; its exit is an ordinary delete, not a conversion. The body's FROM … TO … bullets describe a request and response behaviour change, a consumer-facing reading, not a prescription for the metadata upgrader, the distinction ADR-0087 itself draws for this category. The dev's first spelling, a FROM → TO section label, was refused by the gate's label convention and rewritten into bullets; the substance of the claim did not move, and the gate passes at the head.
  • Clause-②: no — right. The card widens no accept set and no public face. The PR body carries no with no arm; the changeset carries no (narrowing), which is where check-adr-0087-registration reads the breaking signal. The arm is optional on the body line and the two values agree; no contradiction.
  • FROM … TO bullets and the remedy — right and complete for the three faces that move (publish save, draft save, duplicatePackage), with the unchanged cases stated (the owner's re-save, a package-less save, a code-shipped owner, every other type). The one-line remedy (?package=P, no package, or a distinct name; DELETE …?package=Q for a leftover row) is what an upgrading agent needs after the 422.

③ Boundary flags

Dev report 6171892674, each deviation and finding answered or escalated:

  1. Surface increment, packages/rest/src/meta-delete-drop-storage-other-rows.test.ts, outside claim 6168029604's declared surface: accepted. It landed with PR fix(metadata-protocol)!: a delete with dropStorage drops an object's table only with its last stored row, refused 409 while another row remains #23464 after the claim, it saves Q through the door, and this PR reds it without the re-seed; a consumer-radius sweep, fixture-only, declared in the PR body.
  2. Three callers of the classifier, not two: verified in ①.2; none needed an edit.
  3. The FROM → TO label against check-adr-0087-registration: answered in ②.
  4. Two origin/main merges before opening: the net diff against the merge base is the five files and nothing else.
  5. Probe tests created and deleted, ablations never committed: the file list carries none.
  6. Commit trailers: the model-free pair on all five non-merge commits.
  7. The sentinel-owner case (a package-less runtime object's owner; a ?package=Q save answered success with rows [null, Q]): the same D9.9 disagreement, deliberately excluded here because the prescription would name 'sys_metadata' as a package and the remedy wording is a decision. Escalated to the D9.9 family's close-out, metadata-protocol: a save naming a package beside an existing package-less row of the item mints a second row, and the read naming no package keeps serving the unbound one (S6, reachable through today's doors) #23291, comment 6171964742. Not a reason to hold this PR: the card's scope, a runtime owner bound to a package, is complete.
  8. duplicatePackage's prescription misfit (a same-name copy is told to bind to P or extend; renaming is the remedy there): filed as metadata-protocol: duplicatePackage cannot duplicate an object whose name lacks the source namespace prefix — the copy keeps the name and becomes a row the registry never holds, or (after PR #23510) a failure with a prescription that does not fit #23511. This PR's change on that face is not worse (①.3).
  9. The prescription's second remedy names a code-package affordance (objectExtensions): noted; the ruled D9.9 text is reused unchanged and the first remedy is always available. Carrier: the D9.9 family's close-out, with 7.
  10. The heal comment in restoreArtifactRegistryView stays true of stored rows: no action.

Check-runs on the head, read at 2026-10-14T15:44Z: 30 success, 6 skipped (the main-only battery and the opt-in smoke), 3 in_progress: Test Core (2/6), Test Core (3/6), Test Core (4/6). Of the required contexts, Lint & Repo Gates, TypeScript Type Check, Build Core and Governed Surface Queue Guard are success; Test Core is not yet complete. The landing waits for those three shards; the verdict below is on the contract face and does not stand in for them.

Implemented-by: claude/issue-23416-runtime-owner-overlay-refusal
Reviewed-by: session_0121ooBYRmS8ziFXm9ai5YHm

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对

domain:spec seat 4 (#18917) · sunhaolin · session session_0121ooBYRmS8ziFXm9ai5YHm · claim 6168029604 on #23416 · reviewed head 55bb8619e73ad18a8a08a776e7bdee92bca25d03 · 2026-10-14T15:51Z

Verdict: ACCEPT. Not governed (check-governed-merges --pr 23510), 5 files, +359 / −23. The CONTRACT_REVIEW_TIER review on this head is a PASS (6172375603). It lands after the landing clauses in item 6.

  1. Shape.
    • The PR is a draft. Its first line is Fixes #23416, and Clause-②: no sits at line start.
    • The changeset makes @objectstack/metadata-protocol minor and marks it BREAKING with Clause-②: no (narrowing).
    • Its ADR-0087 marker reads not-required (no-migration-prescription), and the gate passes at the head.
    • FROM … TO … bullets cover the publish save, the draft save, duplicatePackage and the boot log line.
  2. Premises against the dispatch.
    • The seat's ruling premise held. The registry keeps one own contributor per object name: getObjectOwner(fqn) reads objectContributors.get(fqn), under the key registerObject uses (computeFQN is identity; both hydration seams pass no namespace).
    • The new arm predicts the registry's own ADR-0029 D3 refusal exactly. It reuses the existing D9.9 mismatch refusal (422 OBJECT_OVERLAY_PACKAGE_MISMATCH), with no new code and no new text.
    • The exclusions are right: the 'sys_metadata' sentinel owner, an empty binding (re-addressed upstream by packagelessAddress), the owner's own package, and an owner with no packageId.
  3. Gates and evidence.
    • Head check-runs at this record: 31 success, 6 skipped, 2 in progress (Test Core (3/6) and (4/6)), 0 failures.
    • The dev's local reconciliation ran 67 of 67 derived families.
    • The suites are green: metadata-protocol 236 files / 28397 tests, objectql 416 / 8097, rest 283 / 5852.
    • The new real-kernel pin went 2F/2P at base. Ablation A went 1F, as predicted, and the restore was proven.
    • The two re-seeded fixtures were each ablated against what they pin: B went 5F, D went 3F.
  4. Accepted deviations.
  5. Out-of-scope dispositions.
  6. Landing clauses.
    • (a) The CONTRACT_REVIEW_TIER PASS on the landing head: met (6172375603).
    • (b) Every check green or skipped by design: waits on Test Core (3/6) and (4/6).
    • (c) mergeable_state clean at enqueue.

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 14, 2026 15:56
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 14, 2026 15:56
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 14, 2026
Merged via the queue into main with commit 222293f Oct 14, 2026
41 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-23416-runtime-owner-overlay-refusal branch October 14, 2026 16:34
marchtian pushed a commit that referenced this pull request Oct 14, 2026
…issue-23429-metadata-family

One conflict: packages/rest/src/meta-delete-drop-storage-other-rows.test.ts
— main parameterised the seed (seedStoredRow(packageId)); S2 drops the
retired organization_id key from it. Took main's shape without the key.

Auto-merged files both sides touch (main = S1/S3/#23489, kept as merged):
organization-ownership.ts keeps S2's template-lookup column guard (S2's own
hunk); organization-ownership-plan.ts and -inventory.ts are main's (S1)
byte-for-byte; protocol.ts keeps #23489's packagelessReadAddress beside S2's
legacy-row filtering; rest-server.ts keeps S2's deletion of the form doors'
organization read; generated translations and the engine-double ledger are
regenerated after the merge.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WXLB6sUdwQZQ9NQS3mj3uZ
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