Repository navigation
Multi-package artifact: the metadata service attributes every top-level object to the artifact's manifest.id while the registry owns it per package — crm_order is served twice on GET /api/v1/meta/object, listed under com.example.multi.core, and Studio's Data pillar for the App package shows the module's object #14599
Description
Activity
- addedbugSomething isn't workingSomething isn't workingpriority:p1High: required for production / M2High: required for production / M2
on Sep 2, 2026 Claiming this card.
- session:
session_01UHvF5hyiZjnCyExFnfQB8m - branch:
claude/issue-14599-metadata-door-reads-packages(pushed, empty, before any edit) - scope: the READER half only —
packages/metadata/src/plugin.ts_parseAndRegisterArtifactlearns to iteratepackages[]when present (ADR-0130 D4 read-both). The producer (composeStacks/os build) keeps emitting the flattened top level; that is A multi-package artifact serializes its metadata twice — the flattened top level and everypackages[i]body carry the same definitions #14512's decision, not this card's.
Generated by Claude Code
Generated by Claude Code
- session:
os-dev-report
{ "issue": 14599, "status": "done", "branch": "claude/issue-14599-metadata-door-reads-packages", "pr": "https://github.com/objectstack-ai/objectstack/pull/14643", "premise_still_valid": true, "summary": "MetadataPlugin._parseAndRegisterArtifact now reads both ADR-0130 D4 shapes: `packages` present -> each assembled package body's collections are registered stamped with THAT body's id/version; `packages` absent -> the single-`manifest` branch runs exactly as before (D7). Ordering and the entry gate are reused from the ObjectQL load path's own `resolveArtifactPackageOrder` (D5), so the two readers of one `packages[]` cannot disagree about order or about which artifacts load. SCOPE DEVIATION, deliberate and flagged: reuse was physically impossible from where that function lived — `@objectstack/objectql` depends on `@objectstack/metadata` — so `artifact-packages.ts` moved to `@objectstack/core`, which already owns `resolvePluginOrder` and is already a dependency of both readers (no package edge added); objectql re-exports all three names, so its published surface is unchanged, and runtime's three dispatcher-error-vocabulary `file:` anchors were repointed. A residual sweep keeps any top-level definition no package body repeats (`packages` composes by `concat`) — registered once under the artifact's own id and warned about — so nothing a booted instance sees today disappears. Producer untouched: composeStacks / os build keep emitting the flattened top level (#14512's decision).", "tests": "All at HEAD 423074c99, clean tree. MEASUREMENT FIRST: every live ARTIFACT_FIELD_TO_TYPE key (25) is a member of AssembledPackageBodySchema, so iterating bodies loses no collection; the only two absent (`workflows`, `ragPipelines`) are declared by NEITHER schema and are already-ledgered dead map entries. ACCEPTANCE on a real `objectstack dev --seed-admin` boot of examples/app-multi-package (all 6 criteria met, exact JSON quoted in the PR body): (1) GET /api/v1/meta/object -> exactly [{crm_account, com.example.multi.core}, {crm_order, com.example.multi.orders}]; (2) ?package=core -> [crm_account], ?package=orders -> [crm_order], matching GET /api/v1/packages' manifest.objects; (3) /meta/object/crm_order/layers -> code._packageId and packageId both com.example.multi.orders, and the item door asked under ?package=core agrees; (4) /meta/app -> multi_crm stamped com.example.multi.core (the fixture carries no other package-owned collections; /meta/view is count 0 on both legs); (5) GET /api/v1/packages unchanged, both rows writable:false, core.objects=[crm_account], orders.objects=[crm_order]; (6) single-package control on a real app-todo boot, fixed vs ablated -> `diff` EMPTY. UNIT PIN packages/metadata/src/plugin-artifact-packages-attribution.test.ts (new, 7 cases, fixture built by the REAL composeStacks producer) 7 passed; includes the D7 control as a literal manager.register sequence. DOGFOOD packages/qa/dogfood/test/multi-package-artifact.dogfood.test.ts extended +3 cases over real HTTP with the artifact door mounted via artifactSource (bootStack registers no MetadataPlugin, so the pre-existing block could not reach this defect) -> 8 passed. ABLATION, direction predicted RED before running, prediction exact: mutation `git checkout 2a2653619 -- packages/metadata/src/plugin.ts`; PROVED ON DISK — blob ac95906e -> 0b22c5f2, markers _registerArtifactBodyCollections 3->0, resolveArtifactPackageOrder 3->0, carriesPackages 4->0, deleted text `packageId: manifestPackageId` back at 3 hits; REBUILT BOTH LEGS (dogfood resolves metadata through dist) with ablation-dist-preflight.mjs — mutate leg `--absent` -> 'marker absent from all 24 built files', restore leg -> 'marker present in 6 built files'; OBSERVED unit 4 failed | 3 passed (the 4 predicted red; crm_order stamped com.example.multi.core instead of .orders), dogfood 3 failed | 5 passed (crm_order row duplicated), and a real ablated boot reproduced the card's exact 3-row JSON; RESTORED BY BYTES `git checkout HEAD -- <absolute path>` -> git hash-object == HEAD blob ac95906ed193bcaf1adf2eb112fa01ea91a8b622, whole-tree `git status --porcelain` empty, `git diff HEAD` empty, both pins re-run green (7/7, 8/8). SUITES: @objectstack/metadata 44 files/684 tests, @objectstack/core 47/1156, @objectstack/objectql 262/4518, @objectstack/runtime 209/3079 — all passed; objectql+runtime `typecheck` green (objectql check:test-typecheck included). GATES: union re-derived from the actual diff — `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands` -> 43 commands, all run, exit codes captured before any pipe: 40 exit 0 (incl. check:dispatcher-error-vocabulary, check-system-context-census — anchors did not move, no --fix needed —, check:stack-collection-maps, check:test-source-alias, check:undeclared-dep-imports, check:type-source-resolution, check:published-files, check:empty-changeset, check:changeset-no-major), plus pnpm check:nul-bytes OK (7980 files). 3 exit 3 = the scripts' own PREREQUISITE-NOT-MET, recorded NOT MEASURED and not findings: check-test-completeness and check-half-states need a CI-supplied input; check:dual-build-cjs-loads needs a whole-repo pnpm build. CI status at report time: in_progress (draft PR just opened).", "mcp_calls": "6 — issue_read get, issue_read get_comments, add_issue_comment (claim), create_pull_request, pull_request_read (body read-back), add_issue_comment (this report). Card body + #14512's body and all 11 timeline items were read through the zero-quota public-repo payload channel; the repo-scoped REST probe answered 403 and `gh` is absent in this container.", "open_questions": [], "out_of_scope_findings": [] }Notes for the PM, outside the machine-read block:
-
One deliberate scope deviation, flagged rather than taken silently. The dispatch said "keep the change to the metadata plugin" and "reuse
resolveArtifactPackageOrder, do not re-derive an order". Those two conflict on a fact the dispatch did not have:@objectstack/objectqldepends on@objectstack/metadata, so the door could not import that function from where it lived. Re-deriving was excluded twice over — by the direction, and by the module's own header (⛔ "Do not add a second sort here, in any form") — and the function is also the entry gate, so a second read would have made the two readers disagree about which artifacts are loadable, not just about order. Resolution: move it to@objectstack/core, which already ownsresolvePluginOrderand is already a dependency of both readers, so no package edge is added; objectql re-exports all three names, so its published surface is unchanged. Diff spans core / objectql / metadata / runtime; changesets graded@objectstack/coreminor, the other three patch. -
The dogfood harness could not reach this door, and that was measured, not assumed.
bootStackregistersAppPluginand noMetadataPlugin, so the existing multi-package dogfood block measures the ObjectQL registry alone — a pin written on/meta/*through the plain harness would have been green on both ablation legs. The new block mounts the real artifact door viaartifactSourceover a build-shaped artifact (the shapeshowcase-object-extension-meta-read.dogfood.test.tsestablished), and the ablation confirms it is not a phantom: 3 red on the mutate leg. -
One behaviour beyond the dispatch's per-key rule, and why. The dispatch framed "keep reading the top level" per KEY; the measurement says every live key has a package-body counterpart, so per that rule the top level would be skipped entirely. The residual risk is per-ITEM, not per-key:
packagescomposes byconcat, so an artifact built from one stack that already carriedpackagesand one that did not has top-level collections outside every body, and dropping them would be a silent regression. The sweep registers those once under the artifact's own id and logs it; for a normally composed artifact it registers nothing and logs nothing (pinned both ways). -
A dead-map-entry observation, deliberately NOT filed as a new issue.
workflowsandragPipelinesinARTIFACT_FIELD_TO_TYPEare declared by neitherObjectStackDefinitionSchemanorAssembledPackageBodySchema— a definition carrying either is refusedunrecognized_keysby the strict parse, so they can never match, exactly like the retiredthemes/roles/policies. This is already ledgered:scripts/check-stack-collection-maps.mjscarries an explicit waiver for these two keys naming Six independent enumerations of the stack-collection set, none answerable tostack.zod.ts— one is mis-aimed and two still list retired kinds #6242 row 4. Filing it would be a duplicate, so it is recorded here instead. -
Base has moved since the branch point (
2a2653619->ca48cf377); the merge state readsblockedon a fresh draft, and CI convergence is yours per the standing split.
Generated by Claude Code
Generated by Claude Code
-
Triage — lane repair only. ⛔ No re-grading:
bug/priority:p1/pm:dispatchedare unchanged and this card's dispatch is not disturbed.Added the missing
domain:engine. This card waspm:dispatchedatp1with nodomain:*at all, which is the worst shape in the sweep: a dispatched, top-priority card belonging to no lane. Nothing in the state machine would have surfaced it, and the seat that holds it had no label saying so.Named the fix surface rather than inferring from the title, per the anchoring rule. The
manifest.idattribution the card describes lives in:packages/objectql/src/artifact-packages.ts,registry.ts,plugin.tspackages/metadata/src/(the loader/registry side)
Both
packages/objectqlandpackages/metadata*aredomain:engineunder the merged lane (maintainer 2026-08-19; the formerdomain:metadatais retired from circulation). So the routing is the same whichever side the repair lands on, which is why I could close this gap without disturbing the in-flight work.⚠️ One boundary worth stating for whoever holds it: the card's last symptom — "Studio's Data pillar for the App package shows the module's object" — is a client rendering of the same server defect. If any part of the fix turns out to be objectui-side, that half goes torepo:objectuias its own card; ⛔ do not cross the repo boundary from this one.If the holding seat reads the anchor differently,
pm:retriageplus a dissent comment in the same stroke and I will re-route.
Generated by Claude Code
Post-landing verification on main after #14643 — Studio grouping
Re-run of the post-landing verification recorded on #14439 (14:17 UTC), now on
main655b106ce2804d2cdb9ed4386df795152535d372(= the #14643 merge commit, on top of #14513). Verification only — no product code changed, no PR. Every defect the earlier run found on the objects door and in Studio's App-package rail is gone.Setup. Fresh worktree of
origin/mainat655b106ce(git log --grep='#14643'→ that HEAD),pnpm install,pnpm exec turbo run build --filter='./examples/app-multi-package...'(57/57 tasks). Boot fromexamples/app-multi-package:./node_modules/.bin/objectstack dev --seed-admin -p 3457 -d file:<scratch>/dogfood-mp2/data.db— boot log📦 Artifact: dist/objectstack.json; the artifact carriespackages[]=[{manifest:{id:"com.example.multi.orders",…}}, {manifest:{id:"com.example.multi.core",…}}]and the flattened top level.GET /api/v1/health→200 {"success":true,"data":{"status":"ok","version":"17.2.0",…}}. Sign-inPOST /api/v1/auth/sign-in/email {"email":"admin@objectos.ai","password":"admin123"}→200,set-auth-tokenpresent (77 chars), used asAuthorization: Bearer. Two boots of the same DB (the second to mount the freshly built/_console) answered identically.Item 2 — server truth: PASS
GET /api/v1/meta/object→200, 75 items;crm_orderonce,crm_accountonce (fields/_diagnosticsstripped):{"name":"crm_account","label":"Account","pluralLabel":"Accounts","isSystem":false,"datasource":"default","sharingModel":"private","nameField":"name","_packageId":"com.example.multi.core","_provenance":"package"} {"name":"crm_order","label":"Order","pluralLabel":"Orders","isSystem":false,"datasource":"default","sharingModel":"private","nameField":"name","_packageId":"com.example.multi.orders","_provenance":"package"}GET /api/v1/meta/object?package=com.example.multi.core→[{"name":"crm_account","_packageId":"com.example.multi.core"}]✅ (the earlier run also returnedcrm_orderhere)
GET /api/v1/meta/object?package=com.example.multi.orders→[{"name":"crm_order","_packageId":"com.example.multi.orders"}]✅GET /api/v1/meta/object/crm_order/layers— the layers door now names orders (the earlier run said core):{"type":"object","name":"crm_order","code":{"name":"crm_order","_packageId":"com.example.multi.orders","_packageVersion":"1.0.0","_provenance":"package"},"overlay":null,"effective":{"_packageId":"com.example.multi.orders"},"lock":"none","provenance":"package","packageId":"com.example.multi.orders"}GET /api/v1/meta/object/crm_order?package=com.example.multi.core(item door, asked under the wrong package) — agrees:{"item":{"name":"crm_order","_packageId":"com.example.multi.orders","_packageVersion":"1.0.0","_provenance":"package"},"packageId":"com.example.multi.orders","provenance":"package","lock":"none"}(
?package=com.example.multi.orders→ same owner;crm_account/layers→code._packageId=packageId=com.example.multi.core.)GET /api/v1/meta/app→multi_crmowned by core:{"name":"multi_crm","label":"Multi-Package CRM","_packageId":"com.example.multi.core","_provenance":"package","navigation":[{"id":"nav_accounts","objectName":"crm_account"}]}✅ (other rows:setup,account).GET /api/v1/packages→200,total: 23; the two fixture rows verbatim exceptmanifest.objects/manifest.appsreduced to names:{"manifest":{"id":"com.example.multi.core","namespace":"crm","defaultDatasource":"default","version":"1.0.0","type":"app","scope":"project","name":"Multi-Package Core","description":"The App half of a two-package release artifact (ADR-0130 D4)","objects":["crm_account"],"engines":{"protocol":"^17"},"apps":["multi_crm"]},"status":"installed","enabled":true,"installedAt":"2026-09-02T18:51:09.804Z","updatedAt":"2026-09-02T18:51:09.804Z","writable":false} {"manifest":{"id":"com.example.multi.orders","namespace":"crm","defaultDatasource":"default","version":"1.0.0","type":"module","scope":"project","name":"Multi-Package Orders","description":"The Module half of a two-package release artifact (ADR-0130 D4)","objects":["crm_order"],"dependencies":{"com.example.multi.core":"^1.0.0"},"engines":{"protocol":"^17"},"apps":null},"status":"installed","enabled":true,"installedAt":"2026-09-02T18:51:09.810Z","updatedAt":"2026-09-02T18:51:09.810Z","writable":false}GET /api/v1/packages/com.example.multi.core→{"id":"com.example.multi.core","writable":false,"objects":["crm_account"]};…/com.example.multi.orders→{"id":"com.example.multi.orders","writable":false,"objects":["crm_order"]}.
GET /api/v1/meta/package→200, 23 items; fixture rows:{"manifest":{"id":"com.example.multi.core","type":"app","namespace":"crm","version":"1.0.0","objects":["crm_account"]},"status":"installed","enabled":true,"writable":false}and{"manifest":{"id":"com.example.multi.orders","type":"module","namespace":"crm","version":"1.0.0","objects":["crm_order"]},"status":"installed","enabled":true,"writable":false}✅assertion result exactly one crm_order(owner orders) and onecrm_account(owner core) onGET /meta/object✅ ?package=core→[crm_account];?package=orders→[crm_order]✅ layers door and item door (asked under ?package=core) both namecom.example.multi.orders✅ /meta/app→multi_crmowned by core✅ /api/v1/packagesboth rowswritable:false,core.objects=[crm_account],orders.objects=[crm_order]✅ /meta/packageboth rowswritable:false✅ Item 3 — Studio: PASS (vendored console AND objectui HMR console)
(a) Vendored
/_console. Pin.objectui-sha=67dadd602a3a891666ea1513c5de677140784b6a(objectui#7229). A fresh worktree has no console dist, so it was built at the pin withpnpm objectui:build(✓ @objectstack/console dist ready (50932 KB) from objectui@67dadd602a3a;pnpm check:console-sha→✓ Console dist matches the objectui pin; no drift refusal,/_console/→ 200 after a restart by recorded PID on the same DB). The pin does not contain objectui#7331 (ebc05b4, 64 commits behind objectuimain) — PIN-stale, not BUILD-stale; as #14597 records, on this fixture the pre-#7331 heuristic and the serverwritableverdict coincide.(b) Authoritative — objectui HMR console at objectui
mainbb459ea9cb734166a8bf2d84163ed684d885e95c(git merge-base --is-ancestor ebc05b4 …→ true, so #7331 is in), detached worktree,apps/console:DEV_PROXY_TARGET=http://localhost:3457 VITE_SERVER_URL=http://localhost:3457 pnpm exec vite --port 5190 --strictPort(proxy self-checkGET /api/v1/healthvia :5190 → 200), signed in on that origin. Every row below is identical on (a) and (b).Headless Chromium 141, viewport 1440×900, screenshots taken after
networkidle+ settle; DOM text recorded only alongside its screenshot.check result evidence ( vendored-*/hmr-*)Studio front door ( /studio) lists both packages✅ both under "INSTALLED (READ-ONLY · BROWSABLE)", each card Read-only; "MY PACKAGES (WRITABLE): No writable packages yet"02b-studio-front-door.pngPackage picker lists both, both read-only ✅ popover "PACKAGES (APPS)": Multi-Package Core com.example.multi.core Read-only,Multi-Package Orders com.example.multi.orders Read-only; 0 ×Writable04-core-switcher-open.pngHeader badge / rail footer read-only ✅ header button Multi-Package Core Read-only/Multi-Package Orders Read-only; rail footer lock +Read-only(noNew object);Create appdisabled on orders03-core-data.png,05-orders-data.pngApp package ( com.example.multi.core) Data rail lists ONLYAccount✅ rail = ["Account"](crm_account, 9 fields); noOrder— the earlier run'sOrder Accountleak is gone03-core-data.pngModule package ( com.example.multi.orders) Data rail lists ONLYOrder✅ rail = ["Order"](crm_order, 10 fields)05-orders-data.pngCross-package lookup crm_order.account → crm_accountrenders in the module's object view✅ Form tab shows Account LOOKUP; field inspector: TypeRelation · Lookup, Related objectcrm_account, "every crm_account record is selectable"07-orders-form-tab.png,08-orders-account-lookup-inspector.pngThe client's own
fetch('/api/v1/packages')from each origin returnedwritable:falseon both fixture rows (*-findings.json).Note on the entry route:
/_console/apps/com.objectstack.studiorenders "App not available — it may still be publishing" on this fixture (02-studio-landing.png, same on both consoles). That is the console's correct answer here, not a defect: the fixture installs nocom.objectstack.studiopackage (GET /api/v1/packages→ 23 rows, none Studio;GET /api/v1/meta/app→multi_crm,setup,accountonly). The builder's own front door is/_console/studio, which is what the table measured.Observations (no issue filed)
- The list door's rows carry no
_packageVersionkey at all (all 75 rows, incl.sys_*), while the layers/item doors carry"_packageVersion":"1.0.0"for the fixture objects. Door-wide projection, not multi-package-specific, so recorded here only. - Boot log:
WARN [MetadataPlugin] artifact '…/dist/objectstack.json' predates this runtime's spec (authored engines.protocol floor 17.0.0, runtime spec 17.2.0) — converted 2 site(s) forward via ADR-0087 conversion 'field-required-notnull-explicit' (first at objects[0].fields.name.storage.notNull). The fixture source spellsrequired: true; the conversion door (Artifacts built by released 17.x tooling are REFUSED by the 17.2 runtime: retired-key tombstones fire at artifact parse, and no artifact-ingestion door runs the ADR-0087 conversion that exists for exactly this #12772) is doing its job. A fixture nit at most.
Verdicts
- Item 2 (server): PASS on every asserted door — one owner per object across list,
?package=, layers and item doors; one row per object. - Item 3 (Studio): PASS — picker, read-only verdicts, per-package grouping for the App package (only
Account) and the module (onlyOrder), cross-package lookup — on the vendored console at67dadd602a3aand on the objectui HMR console atbb459ea9c([approvals][qa] Browser-verify the #7213 entry convergence end to end, and refresh the approvals checklist's entry paths #7331 included). - No new defect found; nothing filed.
Evidence:
<scratch>/dogfood-mp2/evidence/{server,studio-vendored,studio-hmr}on the verification host (curl JSON per door, boot logs,vendored-0{1,2,2b,3,4,5,6,7,8}-*.png,hmr-0{1,2,2b,3,4,5,6,7,8}-*.png,*-findings.json). Servers stopped by recorded PID; worktrees removed.
Generated by Claude Code
- The list door's rows carry no
- added a commit that references this issue
on Oct 7, 2026
Found during the post-landing verification of #14439 (PR #14513) on
main7085f9053, bootingexamples/app-multi-packagewithobjectstack dev --seed-admin. Verification-only finding — nothing fixed here. This is the "correctness edge … nothing exercises it today" that #14512 names, exercised: the two copies of one definition are attributed to different packages, and which owner a consumer sees depends on which door it went through.Fixture
examples/app-multi-package—com.example.multi.core(typeapp, ownscrm_account+ appmulti_crm) andcom.example.multi.orders(typemodule, ownscrm_order, lookup →crm_account), composed withcomposeStacks([ordersStack, coreStack], { manifest: 'preserve' }). The artifact's top-level manifest is core (the'last'pick),packages[]carries both.What the server answers (all authenticated as the seeded admin)
GET /api/v1/meta/object—crm_orderappears twice,crm_accountonce. The twocrm_orderrows are not byte-identical (one carries_packageVersion, the other does not — two producers):[{"name":"crm_order","label":"Order","_packageId":"com.example.multi.orders","_packageVersion":"1.0.0","_provenance":"package"}, {"name":"crm_order","label":"Order","_packageId":"com.example.multi.orders","_packageVersion":null,"_provenance":"package"}, {"name":"crm_account","label":"Account","_packageId":"com.example.multi.core","_packageVersion":null,"_provenance":"package"}]GET /api/v1/meta/object?package=com.example.multi.core— returns the module's object as well as its own:[{"name":"crm_order","_packageId":"com.example.multi.orders"},{"name":"crm_account","_packageId":"com.example.multi.core"}]GET /api/v1/meta/object?package=com.example.multi.orders— correct:[{"name":"crm_order","_packageId":"com.example.multi.orders"}].GET /api/v1/meta/object/crm_order/layers(same answer with?package=either value) — the metadata service's copy says the owner is core:{"type":"object","name":"crm_order","code":{"name":"crm_order","_packageId":"com.example.multi.core","_packageVersion":"1.0.0","_provenance":"package",…},"overlay":null,"effective":{…"_packageId":"com.example.multi.core"…},"lock":"none","provenance":"package","packageId":"com.example.multi.core"}GET /api/v1/meta/object/crm_order?package=com.example.multi.core— the single-item door says the owner is orders:{"item":{"name":"crm_order","_packageId":"com.example.multi.orders","_packageVersion":"1.0.0",…},"packageId":"com.example.multi.orders",…}.So the platform holds two answers to "who owns
crm_order": the layers door sayscom.example.multi.core, the item door and the list rows saycom.example.multi.orders.GET /api/v1/packagesitself is right (core.objects = [crm_account],orders.objects = [crm_order], bothwritable: false).Both boots of the same DB answered identically (rows do not accumulate; this is in-memory attribution, no
sys_metadatarows exist for either object).What Studio shows (both the vendored console at the pinned objectui
d8ec8d6dand objectuimain7dedec6f)The Data pillar for
com.example.multi.corelists Order and Account (rail textObjects Order Account Read-only); the pillar forcom.example.multi.orderslists only Order. ADR-0130 Consequences ("Studio's scope is the package, so package boundaries are the grouping Studio has never had (§1.3a)") does not hold for the App package: it shows the whole artifact. Screenshots are attached to the verification comment on #14439 (hmr-03-core-data.png,vendored-03-core-data.png).Mechanism (read, not guessed)
MetadataPlugin._parseAndRegisterArtifactiterates the flattened top level and stamps every item with the artifact'smanifest.id—packages/metadata/src/plugin.ts:929(manifestPackageId = metadata.manifest.id),:1005(applyProtection(item, { packageId: manifestPackageId, … })),:1010(manager.register(metaType, name, item, { notify: false })). For this artifact that id iscom.example.multi.core, so the metadata service'scrm_orderis stamped core. This is the copy the layers door serves.packages[]per package in topological order —packages/objectql/src/plugin.ts:453(ql.registerApp(manifest, scope)) — so the registry's owner ofcrm_orderiscom.example.multi.orders(registry.ts:2695stamps_packageIdfromgetObjectOwner). This is the copy the item door and one of the list rows serve.registry.getAllObjects(packageId)filters by contributors (registry.ts:2688), and the metadata-service copy stamped core is re-ingested into the registry as core's contribution tocrm_order(anoverlaycontribution —objectContributionKind, owner ≠ packageId), which is why?package=com.example.multi.corereturnscrm_orderwhile the row still reports the owner as orders.${packageId}${name}(packages/metadata-protocol/src/protocol.ts:1199), so the core-attributed copy and the orders-attributed copy land in two slots — the duplicate row.getMetaItemsfor the list isruntime/src/domains/meta.ts→protocol.getMetaItems({ type, packageId }).Expected
One owner per object across every door (
com.example.multi.ordersforcrm_order), one row per object onGET /api/v1/meta/object,?package=<id>returning exactly the objectsGET /api/v1/packagessays that package owns, and Studio's per-package Data rail matching that. #14512's option B (teach the metadata artifact door to readpackages[]when present, and stop emitting the flattened copy) removes the cause at the root; a narrower fix is for the top-level iteration to stamp each item with its owning package (resolvable frompackages[]) instead of the artifact's identity whenpackages[]is present. Single-package artifacts are unaffected either way (manifest.idis the owner there — D7).Repro
Related: #14512 (the format decision), #14439 / #14513 (the producer), #14122 (epic), ADR-0130 D4/D5 and Consequences §1.3a.