Skip to content

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

@hotlong

Found during the post-landing verification of #14439 (PR #14513) on main 7085f9053, booting examples/app-multi-package with objectstack 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-packagecom.example.multi.core (type app, owns crm_account + app multi_crm) and com.example.multi.orders (type module, owns crm_order, lookup → crm_account), composed with composeStacks([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/objectcrm_order appears twice, crm_account once. The two crm_order rows 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 says com.example.multi.core, the item door and the list rows say com.example.multi.orders. GET /api/v1/packages itself is right (core.objects = [crm_account], orders.objects = [crm_order], both writable: false).

Both boots of the same DB answered identically (rows do not accumulate; this is in-memory attribution, no sys_metadata rows exist for either object).

What Studio shows (both the vendored console at the pinned objectui d8ec8d6d and objectui main 7dedec6f)

The Data pillar for com.example.multi.core lists Order and Account (rail text Objects Order Account Read-only); the pillar for com.example.multi.orders lists 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)

  1. MetadataPlugin._parseAndRegisterArtifact iterates the flattened top level and stamps every item with the artifact's manifest.idpackages/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 is com.example.multi.core, so the metadata service's crm_order is stamped core. This is the copy the layers door serves.
  2. The ObjectQL load path registers packages[] per package in topological order — packages/objectql/src/plugin.ts:453 (ql.registerApp(manifest, scope)) — so the registry's owner of crm_order is com.example.multi.orders (registry.ts:2695 stamps _packageId from getObjectOwner). This is the copy the item door and one of the list rows serve.
  3. 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 to crm_order (an overlay contribution — objectContributionKind, owner ≠ packageId), which is why ?package=com.example.multi.core returns crm_order while the row still reports the owner as orders.
  4. The list merge keys slots by ${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.

getMetaItems for the list is runtime/src/domains/meta.tsprotocol.getMetaItems({ type, packageId }).

Expected

One owner per object across every door (com.example.multi.orders for crm_order), one row per object on GET /api/v1/meta/object, ?package=<id> returning exactly the objects GET /api/v1/packages says that package owns, and Studio's per-package Data rail matching that. #14512's option B (teach the metadata artifact door to read packages[] 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 from packages[]) instead of the artifact's identity when packages[] is present. Single-package artifacts are unaffected either way (manifest.id is the owner there — D7).

Repro

pnpm exec turbo run build --filter='./examples/app-multi-package...'
cd examples/app-multi-package && ./node_modules/.bin/objectstack dev --seed-admin -p 4310 -d file:/tmp/mp/data.db
# sign in: POST /api/v1/auth/sign-in/email {"email":"admin@objectos.ai","password":"admin123"} → set-auth-token → Bearer
curl -H "Authorization: Bearer $T" 'http://localhost:4310/api/v1/meta/object' | jq '.items | map(select(.name|startswith("crm_"))) | map({name,_packageId,_packageVersion})'
curl -H "Authorization: Bearer $T" 'http://localhost:4310/api/v1/meta/object?package=com.example.multi.core' | jq '.items|map(.name)'
curl -H "Authorization: Bearer $T" 'http://localhost:4310/api/v1/meta/object/crm_order/layers' | jq '{code:.code._packageId, packageId}'

Related: #14512 (the format decision), #14439 / #14513 (the producer), #14122 (epic), ADR-0130 D4/D5 and Consequences §1.3a.

Activity

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

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions