Skip to content

fix(metadata-protocol)!: the adopt-orphans door asks the save door's owner judges (another package owns the object; a permission set naming another owner) before the __c judge - #28808

Merged
objectstack-fleet[bot] merged 5 commits into
mainfrom
claude/project-thread-xqgh8b
Nov 1, 2026
Merged

objectstack-fleet[bot] merged 5 commits into
mainfrom
claude/project-thread-xqgh8b

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Nov 1, 2026 •

Copy link
Copy Markdown
Contributor

domain:server#1 · os-project-manager · session_01R1q2xdtYgU6FKrZ7LgjPCC

Requested by Leehom · project thread

Fixes #28776

Clause-②: no (narrowing)
Metadata-protocol: narrowed
Skills-docs: none — no file under skills/** teaches the adopt-orphans door (0 hits for adopt-orphans and reassignOrphanedMetadata).

维护者速读

  • 改了什么:「收养孤儿元数据」(POST /api/v1/packages/:id/adopt-orphans) 现在也问保存门的两个「归属」判官,顺序和保存门一致(先归属,再 metadata-protocol: the adopt-orphans door rebinds a package-less x__c field into a plain package without the __c judge (#28564 follow-up) #28735 加的 __c)。两种情况整次收养被拒、一行都不动:一个不属于任何包的对象,名字已被另一个包占有(422 OBJECT_OVERLAY_PACKAGE_MISMATCH);一个不属于任何包的权限集,正文 packageId 写的是别的包(400 VALIDATION_ERROR)。普通孤儿对象照常收养。
  • 为什么改:保存门和发布门都问这两个判官,收养门不问。对象那一种被收养后,存储里成了两行、注册表仍归原主人,正是 D9.9 要拒绝的形态。
  • 风险与回滚:实测 CRM、showcase 新启动 0 行 sys_metadata,测试夹具里 0 个会被拒的可收养行。不新增错误码,不动 spec。命名空间前缀判官不在本 PR:它会拒掉几乎所有真实孤儿对象,按席位裁决交给你定(拒绝、收养时改名,还是不判)。回滚即 revert。
  • 先看:packages/metadata-protocol/src/protocol.ts 的 reassignOrphanedMetadata(__c 块之前新加的归属判官循环,注意跳过 sys_metadata 哨兵主人那一句);packages/metadata-protocol/src/protocol.adopt-orphans-owner-judges.test.ts。
  • 两分钟验证:pnpm --filter @objectstack/metadata-protocol exec vitest run src/protocol.adopt-orphans-owner-judges.test.ts,预期 Tests 6 passed (6)(main 上 3 条红,3 条对照两边都绿)。
  • 需要你做的:无,席位落地(合同审查后)。前缀问题另行裁决。

What changes

Why Metadata-protocol: narrowed. An adoption that admitted these two rows is now refused: one more write door applies two requirements the save door already enforces. That is the same reading as #28735.

Tests

New packages/metadata-protocol/src/protocol.adopt-orphans-owner-judges.test.ts, 6 tests. The engine double stores rows and answers registry.getObjectOwner from a table the test sets; the registry write-through is spied out.

  • D9.9 refused: probe_clean (sentinel-owned) beside probe_taken (owned by app.probe-other), adopted into app.probe-target: OBJECT_OVERLAY_PACKAGE_MISMATCH / 422, message naming both packages. engine.update and the write-through are never called, and both rows stay package-less.
  • Order: probe_taken carrying x__c answers the owner refusal (422), not the __c one (400).
  • CONTROL, the sentinel pin: a sentinel-owned orphan and an unowned one are admitted and both move (reassignedCount: 2).
  • CONTROL: an orphan the target itself owns is admitted.
  • D3 refused: a package-less set whose body says packageId: 'app.probe-other', beside a clean object: VALIDATION_ERROR / 400, issues[0].path = 'packageId', message "Drop packageId from permission set …". Nothing moves.
  • CONTROL: a set naming the target, and one naming no owner, are admitted and move.
Red on base, green on head; ablations
  • Base (protocol.ts from 8f294e04, the new file at head): Tests 3 failed | 3 passed (6). The three refusal tests are red: the two D9.9 tests and the D3 test. The three controls are green both ways. Restore git checkout HEAD -- protocol.ts, git diff HEAD empty.
  • Head: Tests 6 passed (6); with protocol.extension-field-suffix.test.ts and protocol.reassign-orphans-durability.test.ts: 35/35.
  • Ablations via node scripts/ablation-replace.mjs from committed HEAD (the suite reads src):
    • object arm off (condition && (globalThis as any).ABLATE_28776_OBJ === 1): blob bc2fc3da1611 → 47279aca0276, 2 failed (the two D9.9 tests), restored blob == HEAD, diff empty.
    • permission arm off (void ownerRefusal;): → 38214b741154, 1 failed (the D3 test), restored clean.
    • sentinel exclusion off (if (layer.kind === 'mismatch')): → a96b1a46d286, 3 failed. The red is the sentinel pin, plus the two refusal tests whose sentinel-owned probe_clean is now refused first with the wrong owner. Restored clean.

Acceptance notes

Verification

All readings at head df5ce323 (base 8f294e04; the fix 01a8dcb8, changeset 27886745, and df5ce323, which routes the test double's findOne through assertEngineFindOnePredicate and adds the file to scripts/engine-double-contract.pinned.json via --write, the gate's own prescription). Full pnpm turbo run build --concurrency=2 at 27886745: 118/118; metadata-protocol rebuilt at df5ce323.

Package tests, typecheck, lint
  • pnpm --filter @objectstack/metadata-protocol test: Test Files 296 passed | 4 skipped (300), Tests 22058 passed | 26 skipped (22084) (lock VERDICT command-exit 0).
  • pnpm --filter @objectstack/metadata-protocol typecheck: exit 0 (tests included; check:type-check-debt OK, 0 packages hiding tests).
  • eslint --no-inline-config on protocol.ts and the new test file: exit 0.
Full dogfood battery (the `(narrowing)` arm)
  • Head df5ce323: pnpm --filter @objectstack/dogfood test under the lock, VERDICT command-exit 0, Test Files 378 passed | 3 skipped (381), Tests 3105 passed | 28 skipped (3133). No failure.
Derived gates (`dispatch-gates.mjs --commands`, 69 families)
  • All 69 exit 0 at df5ce323; dispatch-gates --ran: 69 derived, 69 run, 0 NOT-MEASURED, 0 UNRUN.
  • check:type-check-debt: OK — 126/129 workspace packages type-checked, --re-measure: OK.
  • The first pass at 27886745 read check:engine-double-contract exit 1: the new double's findOne did not route through the predicate, and the ledger did not know the file. Both were fixed in df5ce323 and the gate is green there. That first pass and its dogfood run were stopped and re-run whole on df5ce323.
  • Repo-wide pnpm lint and the CI-only families are CI's; see the PR's check-runs.

Generated by Claude Code

…before the __c judge

reassignOrphanedMetadata refuses the adoption whole, before the first
storage update, when an active orphan object's name is owned in the
registry by another package (ADR-0029 D9.9, OBJECT_OVERLAY_PACKAGE_MISMATCH
/ 422; the sys_metadata sentinel owner, the orphan's own registration, is
skipped) or an active orphan permission set's body packageId disagrees with
the target (ADR-0086 D3, VALIDATION_ERROR / 400). Asked in the save door's
order, before the __c judge. The namespace-prefix judge is not asked.

Co-Authored-By: Claude <noreply@anthropic.com>
… refusals

Co-Authored-By: Claude <noreply@anthropic.com>
…ne routes through the engine predicate; ledger learns the file

Co-Authored-By: Claude <noreply@anthropic.com>
@github-actions

github-actions Bot commented Nov 1, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

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

20 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: node scripts/docs-audit/affected-docs.mjs --json 3e05ed1ec2ecce09b19a23bd51a6f14afb40caf3.

⛔ 9 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails.

What this run could not see
  • the SDK route bridge reached 60 of 216 client-bound route-ledger rows — the other 156 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 156: 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; 101 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 — 10 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 3e05ed1ec2ecce09b19a23bd51a6f14afb40caf3 → packageMentionDocs.

Which tree this was computed on

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

node scripts/docs-audit/affected-docs.mjs --json 3e05ed1ec2ecce09b19a23bd51a6f14afb40caf3

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

…refused whole, nothing rebound, the owner stays

Co-Authored-By: Claude <noreply@anthropic.com>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#1 · os-project-manager · session_01R1q2xdtYgU6FKrZ7LgjPCC

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 7e37e4c238a4b6c748ff0ae1466b01e2d80680a4
Local-runs: none
Scope: packages/metadata-protocol/src/protocol.ts, packages/metadata-protocol/src/protocol.adopt-orphans-owner-judges.test.ts, packages/objectql/src/protocol-adopt-orphans-object-registry.test.ts, scripts/engine-double-contract.pinned.json + packages/metadata-protocol/src/protocol-object-owner.ts (classifyObjectContribution, releaseAdoptedObjectOwner), packages/spec/src/security/permission-set-owner-binding.ts (judgePermissionSetOwnerBinding), packages/spec/src/api/package-lifecycle.zod.ts (ReassignOrphanedMetadataResponseSchema), packages/runtime/src/domains/packages.ts (the adopt-orphans route), packages/qa/dogfood/test/package-first-authoring.dogfood.test.ts (the D5 adopt pin), docs/adr/0029 D9.9, docs/adr/0086 D3, docs/adr/0146 D10 and D11, docs/adr/0070 D5

Read at 2026-11-01T22:37Z: card #28776 (body and all 4 comments: the claim 6562862818, the step-1 ruling 6563043440, the two os-dev-reports 6563905031 and 6564870922), PR #28808 (body, 5-file list, the net diff origin/main...7e37e4c2; merge-base c9a9981d, mergeable clean), and the 48 check-runs on the head. The changeset .changeset/28776-adopt-orphans-owner-judges.md is a prose face: read for ②, not reviewed. The pinned ledger is generated output (check:engine-double-contract --write): not reviewed. Scope widened over the floor to the cited code and the spec faces named above.

Check-runs on 7e37e4c2 (read 2026-11-01T22:37Z): 48 total, all completed, none in progress. 34 success, 14 skipped (the opt-in harnesses, the UI build/lint/typecheck/test matrix, Build Docs, Temporal Conformance, the dogfood shard matrix, the schedule-gate skip: none applies to a diff with no ui/, docs or spec file). Green and material to this diff: Test Core 1/8 to 8/8 (7/8 at 22:22Z, the df5ce323 red cleared), Dogfood Regression Gate, Lint & Repo Gates with every scoped lint family, Lint · PM dispatch-gates, Check Changeset, Type Check · workspace, debt ledger, source and consumer gates, PR-body guards, Governed Surface Queue Guard, Live E2E UI. The conclusions are the gate verdicts; nothing was run here.

① Derived judgments

The diff moves one method, reassignOrphanedMetadata (POST /api/v1/packages/:id/adopt-orphans), and no public surface. Each accept-set change it implies, judged in the tree:

  1. An ACTIVE orphan object row whose name a package other than the target owns in the registry is refused whole, OBJECT_OVERLAY_PACKAGE_MISMATCH / 422. Right. classifyObjectContribution (protocol-object-owner.ts) answers mismatch for binding T against owner O when T is not O, on both the packaged-owner and the runtime-owner branch; the adopt loop calls it with request.targetPackageId before the first engine.update, so the refusal precedes every write and applyRegistryWriteThrough. The envelope is overlayPackageMismatchError(name, T, O), the save door's own (status 422, code, packageId, ownerPackageId, the "Cannot layer object" sentence). On main the same row was rebound in storage, releaseAdoptedObjectOwner answered false (owner not the sentinel), and the registry kept O: the P == Q row ADR-0029 D9.9 says is "refused at the producer, loudly" and that loadMetaFromDb counts as an error at the next boot. The route relays .status and .code (errorFromThrown reads .status first), so the wire answer is 422, as the objectql kernel pin reads it.
  2. The 'sys_metadata' sentinel owner is excluded from that arm. Right, and load-bearing. An orphan object's own registration is under the sentinel (objectql/metadata-protocol: a save naming a package of a runtime object the registry holds under the sys_metadata sentinel answers 200, but the registry keeps the package-less definition, so the saved row never goes live #23514, metadata-protocol: an adopt-orphans into a package rebinds a runtime object's row in storage, but the registry keeps the sys_metadata owner and the old definition until a restart (ADR-0070 D5) #23646), so classifyObjectContribution(name, T) answers mismatch with owner sys_metadata for every plain orphan; judged unchanged, the door would refuse every object adoption and ADR-0070 D5's convergence door would be dead (the step-1 measurement showed exactly that on a real CRM boot). The sentinel is "no package" (ADR-0070 TL;DR 1; overlayPackageMismatchError itself routes a sentinel owner to a different refusal). The exclusion is pinned by the sentinel CONTROL (a sentinel-owned and an unowned orphan both move, reassignedCount: 2) and ablated (3 red with the exclusion off). Nothing else can answer mismatch with the sentinel: only the runtime-owner branch does, and only when getObjectOwner answers it.
  3. An ACTIVE orphan permission row whose body packageId disagrees with the target is refused whole, VALIDATION_ERROR / 400, issues[0].path = 'packageId'. Right. permissionSetOwnerBindingRefusal(type, body, name, T, 'save') is the save door's envelope around the one judge (judgePermissionSetOwnerBinding, packages/spec/src/security), called with the binding the row lands in, which for an adoption is the target, exactly as the save door passes saveWriteBinding and the promotion passes promoteInto. ADR-0086 D3's verdict table: declared 'Q' against bound 'P' is refused, "never silently overridden". On main the adoption rebound the row into T while its body still said X: the silent override D3 exists to refuse. An absent key passes; a body naming the target passes (CONTROL pinned). An empty-string packageId is refused against any target, the judge's own fourth row, consistent with the save door.
  4. Order: owner judges, then the __c judge. Right against the save door: there the D9.9 object judge (protocol.ts, the classifyObjectContribution block) and the permission owner judge both precede objectBodyExtensionFieldSuffixRefusal; here the one owner loop precedes the judgeExtensionFieldSuffix block. Pinned: a row both judges refuse answers 422, not 400. One difference, noted and not a defect: the save door judges one item per request, so there is no cross-type order to copy; the adopt door judges rows in read order and throws on the first, so a multi-row adoption reports one owner refusal at a time, while the __c judge reports every field in one issues[]. The ruling asked for "each judge's existing envelope", which is this; a batched report would be a new envelope.
  5. A draft row is not judged (row.state !== 'active' skips it); an unparseable permission body is skipped (continue). Right, and the metadata-protocol: an orphan adoption registers a package-less DRAFT view as live — reassignOrphanedMetadata's view write-through has no state gate, so the registry serves a draft the boot never registers (found by #23646) #23690 / metadata-protocol: the adopt-orphans door rebinds a package-less x__c field into a plain package without the __c judge (#28564 follow-up) #28735 shape: a draft never reaches the runtime and is judged by both owner judges at its promotion (the publish door at the promoteInto block); an unparseable row is rebound as stored, as the __c judge and duplicatePackage leave it. Neither widens anything.
  6. No public surface moves. ReassignOrphanedMetadataResponseSchema (packages/spec) is unchanged; no export, no spec file, no new error code: both codes are already the save door's, registered to this package. The route is untouched. The dogfood D5 pin (package-first-authoring.dogfood.test.ts: a package-less DRAFT object adopted into app.dogfood_adopt) stays green by construction, a draft is not judged.
  7. The rewritten objectql control (protocol-adopt-orphans-object-registry.test.ts, the metadata-protocol: an adopt-orphans into a package rebinds a runtime object's row in storage, but the registry keeps the sys_metadata owner and the old definition until a restart (ADR-0070 D5) #23646 kernel suite). Judged on its own: it still pins something real, not a flipped expectation. The old CONTROL pinned "rebound in storage, the registry keeps R". The new case boots a real kernel (ObjectQLPlugin, the real SchemaRegistry), saves R's row through the real save door so getObjectOwner answers R by the real path, inserts the package-less residue, and pins three things: the wire shape (422, the code, the message naming R), that storage is NOT rebound (bindings [R, none], so the refusal precedes the update on the real engine, not only on the double), and the metadata-protocol: an adopt-orphans into a package rebinds a runtime object's row in storage, but the registry keeps the sys_metadata owner and the old definition until a restart (ADR-0070 D5) #23646 half unchanged (no registerObject / unregisterObject / object registerItem, owner R, the served definition still R's label and package). That is the end-to-end pin the metadata-protocol unit suite cannot give (its registry is a table the test sets). The header's bullet list names the change. The other five cases of the file are untouched.
  8. scripts/engine-double-contract.pinned.json: two pinned: 1 rows for the new test's findOne and update, the gate's own --write output after the double was routed through assertEngineFindOnePredicate / assertEngineUpdateDispatch. Generated; Lint · CI, release & engine gates green on the head.

② Semver level

@objectstack/metadata-protocol (18.2.0): minor, BREAKING banner, Clause-②: no (narrowing) and Metadata-protocol: narrowed on both the PR body and the changeset, one ADR-0087 marker not-required (no-migration-prescription) with its reasons, and a migration paragraph. Matches what the diff publishes: a requirement tightened at one more write door is narrowed, a kind takes at least minor on the package the diff moves, (narrowing) is BREAKING and carries its disposition (AGENTS.md Post-Task Checklist 3). The marker's category holds in the tree: no key, value, export or code is removed or renamed and there is nothing for objectstack migrate meta to rewrite; the remedy is an author's choice (bind, delete or rename the object; drop or fix the set's packageId). The level and the two lines are byte-for-byte the shape of the #28735 precedent (c363084b53, the same door). Check Changeset and check:adr-0087-registration (in Lint & Repo Gates) green. Clause-②: line: no (narrowing), correct: nothing an author can write is widened; the measured count at the base is 0.

③ Boundary flags

  • open_questions[0] (round 1): the permission arm speaks the spec judge's 'save' door words ("this save writes it into package T"); A keep, or B a spec card for an adopt verb. Answered A. PermissionSetOwnerBindingDoor is a closed union in packages/spec (out of this PR's file surface by the claim), the protocol wrapper narrows it further to 'save' | 'restore' | 'publish', and the save words carry the right remedy for an adoption: drop the key, or put the set where it declares, then send it again, nothing was written. "This save" names the act loosely for a door with a measured count of 0. Not escalated as a card: a spec verb would be a cosmetic widening of a closed union for a sentence nobody has read on the wire. If a seat wants the exact word, it is a future spec card, not a condition of this PR.
  • Round 2's disclosed deviation (a parse-error splice left for minutes while the gate loop ran; check-comment-mask-corpus read it UNPARSEABLE; fixed in the amended, unpushed commit; 8 overlapped gates re-run). Answered: nothing broken reached the head; Lint · scoped · slot lookup & corpus and every lint family are green on 7e37e4c2.
  • Round 1's miss (only the metadata-protocol suite run; Test Core 7/8 red on df5ce323). Answered by the head: 8/8 shards green, and the moved pin is judged in ① item 7.
  • out_of_scope_findings[0]: the namespace-prefix judge at this door. Escalated, not a gap of this PR: the card's ruling 6563043440 sends refuse / rename-on-adopt / unjudged to the maintainer, and the method header says the judge is deliberately not asked. Carrier: the maintainer; the seat holds it.
  • out_of_scope_findings[1]: releaseAdoptedObjectOwner's "another package owns it" row (answers false) is no longer reached in the ordinary run. Accepted as left: the loop awaits between the judge and each update, so a registry change in between still meets that row, and it fails in the cheap direction (storage rebound, registry untouched, the next boot counts it). Not dead code to delete in this PR.
  • The round-2 acceptance_notes_addition (the moved metadata-protocol: an adopt-orphans into a package rebinds a runtime object's row in storage, but the registry keeps the sys_metadata owner and the old definition until a restart (ADR-0070 D5) #23646 pin) is for the seat to add to the PR body; the body's Verification section still reads at df5ce323. Body residue, judged below as should-fix for the seat, not of the tree.
  • No dev flag is left unanswered. The dev reports mcp_calls: 0 and the relay writes it names; the card's assignee and labels untouched.

④ Design judgment

The narrowing, read against the published spec on main and the cited code, with the design tried for breaks:

Does it take a declared capability away at the consumer? No. The published surface for this door is ReassignOrphanedMetadataResponseSchema and its docblock: "bulk-rebind package-less (orphaned) metadata into this base (ADR-0070 D5)". D5 promises "Existing package_id = null rows are not broken" and a bulk-assign of legacy orphans; it declares nothing about a row the registry cannot hold under the target. The two rows this PR refuses were never deliverable by that promise: the D9.9 row produced "two rows for one object" in storage with the registry still naming the other owner (the shape ADR-0029 D9.9 declares not representable, refused at the producer and counted as an error at boot), and the D3 row produced a set bound to T whose own body says X (the silent override D3 refuses at every door that files a set). So the narrowing makes the spec's own refusal real at one more door, rather than narrowing a delivered capability at the consumer. The door still adopts every row the spec can serve: the sentinel CONTROL, the target-owned CONTROL, the two permission CONTROLs, the dogfood D5 draft, and the #23646 kernel adoptions all still move.

Does it dead-end D5 / D11? Tried and no. The break would be the sentinel: every plain orphan object is registered under sys_metadata, so an unexcluded judge refuses all of them (the step-1 control proved it). The exclusion is explicit, pinned and ablated. ADR-0146 D11 shrinks this door's input (package-less rows of packaged items migrate into .custom as declared overrides) but keeps D5's treatment for a package-less row no package ships, which is exactly the row this door still adopts; the judges touch only rows D11 would not migrate either (an object another package owns is not "an item no package ships"). The prefix judge, the one that would dead-end the door, is not asked, by ruling.

Is the judge asked at the right binding and time? Yes. The save door's D9.9 block judges "the package the write lands in" and the permission judge "the resolved binding of the row the write lands in"; an adoption lands every row in the target, so request.targetPackageId is that binding for both. Both are asked after the orphan read and before the first engine.update (pinned: update and applyRegistryWriteThrough never called on refusal), so a refusal costs no write, and the response schema's success/reassignedCount are never answered for a partial run; the loop's own per-row refusal channel (#12981) is untouched for the storage-level failures it exists for.

Could a refused adoption be worked around through another door? Tried: the save door refuses the same object row naming T (D9.9) and the same set body naming T (D3); the publish door refuses a draft of either at promotion; duplicatePackage rewrites a set's packageId to the target before its own save. No door admits what this one now refuses. Conversely nothing this door admits is refused elsewhere: a sentinel-owned orphan adopted into T is then registered under T by the write-through, which is the #23646 answer.

The rewritten control (brief's question): it pins the real-kernel refusal and the registry invariant together; a flipped expectation would have kept storedBindings at [Q, R]. See ① item 7.

Findings:

Implemented-by: claude/project-thread-xqgh8b
Reviewed-by: session_01R1q2xdtYgU6FKrZ7LgjPCC

Rendered by the seat's subagent at 2026-11-01T22:37Z, from the card, the PR, the net diff and the head's check-runs only; no local run.

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#1 · os-project-manager · session_01R1q2xdtYgU6FKrZ7LgjPCC

席位核对 · ACCEPT · domain:server seat 1 · os-project-manager · session_01R1q2xdtYgU6FKrZ7LgjPCC · 2026-11-01T22:41Z

Head 7e37e4c2, report r2 6564870922 (on #28776), claim 6562862818. Clause-②: no (narrowing), Metadata-protocol: narrowed: the contract review at CONTRACT_REVIEW_TIER (narrowing face) PASSED on this exact head, record 6564962157, with no must-fix.

item reading
Shape Draft, base main, Fixes #28776. 5 files: the owner-judge loop in reassignOrphanedMetadata (protocol.ts), the new protocol.adopt-orphans-owner-judges.test.ts, the #23646 control in protocol-adopt-orphans-object-registry.test.ts moved to the refusal, the engine-double ledger, and a minor changeset with the BREAKING banner. No export, no packages/spec/**, no new error code, no gate.
Rule The adopt door asks the save door's two owner judges (D9.9 object owner, D3 permission-set packageId) against the target, in the save door's order and before #28735's __c judge, with the sys_metadata sentinel owner excluded. The first refusal refuses the adoption whole: no row moves. The namespace-prefix judge stays out, per the seat's ruling, on Leehom's open decision card.
Should-fix The review's one should-fix (PR body only) is done: the round-2 Acceptance-notes line, with the 7e37e4c2 verification numbers, was pasted into the PR body before this ACCEPT.
Evidence CI green on head 7e37e4c2 (filter=latest: 37 success, 15 skipped, 0 failed); df5ce323's Test Core 7/8 red is cleared. The r2 report shows the metadata-protocol, objectql and runtime suites green, 70/70 derived gates and dogfood green.
Landing 「按照 pm-dispatch 规则复核过的都授权」 (Leehom, 2026-10-25T11:35Z): ready and auto-merge through the relay, with every check green or an expected skip.

Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review November 1, 2026 22:42
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge November 1, 2026 22:42
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Nov 1, 2026
Merged via the queue into main with commit 2a4c003 Nov 1, 2026
53 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/project-thread-xqgh8b branch November 1, 2026 23:05
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

Development

Successfully merging this pull request may close these issues.

metadata-protocol: the adopt-orphans door asks neither the namespace-prefix nor the owner judge (measure reach, then rule)

1 participant