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 main7085f9053, 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 main7dedec6f)

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.idis 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.

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions

    , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
     blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
    }
    } catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
    })();
    (function(){
    try {
    var __m = "github.com";
    var __re = new RegExp('^' + "github\\.com" + '
    
    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 main7085f9053, 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 main7dedec6f)

    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.idis 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.

    Metadata

    Metadata

    Assignees

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions

      , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
      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 main7085f9053, 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 main7dedec6f)

      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.idis 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.

      Metadata

      Metadata

      Assignees

      Type

      No type

      Projects

      No projects

        Milestone

        No milestone

        Relationships

        None yet

        Development

        No branches or pull requests

        Issue actions

        , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
        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 main7085f9053, 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 main7dedec6f)

        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.idis 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.

        Metadata

        Metadata

        Assignees

        Type

        No type

        Projects

        No projects

          Milestone

          No milestone

          Relationships

          None yet

          Development

          No branches or pull requests

          Issue actions

          , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
          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 main7085f9053, 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 main7dedec6f)

          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.idis 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.

          Metadata

          Metadata

          Assignees

          Type

          No type

          Projects

          No projects

            Milestone

            No milestone

            Relationships

            None yet

            Development

            No branches or pull requests

            Issue actions

            , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
            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 main7085f9053, 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 main7dedec6f)

            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.idis 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.

            Metadata

            Metadata

            Assignees

            Type

            No type

            Projects

            No projects

              Milestone

              No milestone

              Relationships

              None yet

              Development

              No branches or pull requests

              Issue actions

              , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
              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 main7085f9053, 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 main7dedec6f)

              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.idis 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.

              Metadata

              Metadata

              Assignees

              Type

              No type

              Projects

              No projects

                Milestone

                No milestone

                Relationships

                None yet

                Development

                No branches or pull requests

                Issue actions

                , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
                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 main7085f9053, 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 main7dedec6f)

                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.idis 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.

                Metadata

                Metadata

                Assignees

                Type

                No type

                Projects

                No projects

                  Milestone

                  No milestone

                  Relationships

                  None yet

                  Development

                  No branches or pull requests

                  Issue actions