Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 75 additions & 0 deletions .changeset/adr0130-metadata-door-reads-packages.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
---
"@objectstack/metadata": patch
"@objectstack/core": minor
"@objectstack/objectql": patch
"@objectstack/runtime": patch
---

fix(metadata): register a `packages[]` artifact per package at the metadata door so every object has one owner across every door (#14599)

A release artifact carrying `packages[]` (ADR-0130 D4) was read at the metadata
door as if it carried one package: `MetadataPlugin._parseAndRegisterArtifact`
iterated the **flattened top level** and stamped every item with the artifact's
own `manifest.id`. For an artifact composed with `composeStacks(…, { manifest:
'preserve' })` that id is one arbitrary member's — `selectManifest`'s `'last'`
pick — so a two-package artifact registered the **module's** object under the
**App** package's identity, while the ObjectQL load path, reading the same
artifact's `packages[]`, owned it under the module's.

The platform then held two answers to "who owns this object", and which one a
consumer saw depended on the door it went through. Measured on a real boot of
`examples/app-multi-package`:

- `GET /api/v1/meta/object` served `crm_order` **twice** — the list merge keys
slots by `${packageId}${name}`, so the two differently-attributed copies
landed in two slots;
- `GET /api/v1/meta/object?package=<the App package>` returned the **module's**
object, because the App-stamped copy was re-ingested into the registry as that
package's contribution;
- the layers door named the App package while the item door and
`GET /api/v1/packages` named the module;
- Studio's Data pillar for the App package listed the module's object — ADR-0130
Consequences §1.3a ("Studio's scope is the package") did not hold.

**The door now reads both shapes, and attributes every item to the body it was
found in.** `packages` present → each assembled package body's collections are
registered stamped with **that body's** id; `packages` absent → the single
`manifest` branch runs exactly as before (D7). The owner is read off the body an
item was found in — never reverse-derived by matching a top-level item's name
against a name-to-package index, which would be the second metadata-identity
resolution path #14512's triage rejected by name.

**Ordering and the entry gate are reused, not re-derived (D5).** The door calls
the same `resolveArtifactPackageOrder` the ObjectQL load path calls, so the two
readers of one `packages[]` cannot disagree about the registration order **or**
about which artifacts are loadable at all.

⚠️ **`resolveArtifactPackageOrder` / `artifactPackageId` moved to
`@objectstack/core`** — hence the `minor` there. They were in
`@objectstack/objectql`, which **depends on** `@objectstack/metadata`, so the
metadata door could not import them from where they lived; `@objectstack/core`
already owns `resolvePluginOrder` and is already a dependency of both readers,
so hosting them there adds **no edge** to the package graph. `@objectstack/objectql`
re-exports both under their existing names — its published surface is unchanged,
which is why it is graded `patch`. `@objectstack/runtime` is `patch` for the
dispatcher error vocabulary's `file:` anchors, repointed at the new path.

**Single-package artifacts are byte-for-byte unaffected (D7)**, measured rather
than asserted: the whole `manager.register` sequence for a single-`manifest`
artifact — every call, in order, with the id and version each item was stamped
with — is pinned as a literal in
`packages/metadata/src/plugin-artifact-packages-attribution.test.ts` and was
recorded identically on both legs of the ablation. A real boot of
`examples/app-todo` answers every door identically before and after.

**Nothing a booted instance can see today disappears.** Every live
`ARTIFACT_FIELD_TO_TYPE` key is a member of `AssembledPackageBodySchema`
(measured, not assumed), so iterating bodies loses no collection; and because
`packages` composes by `concat`, an artifact whose top level carries a
definition no package body repeats keeps it — registered once, attributed to the
artifact's own identity, and logged, because it means the artifact's two halves
disagree about what it ships.

⛔ The **producer** half is untouched: `composeStacks` and `os build` keep
emitting the flattened top level alongside `packages[]`. Whether they should is
#14512's decision, not this door's.
Original file line numberDiff line numberDiff line change
Expand Up@@ -9,6 +9,26 @@
* turns an artifact — either shape — into the ordered list of manifests the
* load path registers.
*
* ## Why this lives in `@objectstack/core` and not next to one of its readers
*
* There are TWO readers of `packages[]`, not one, and they sit in packages that
* cannot import each other: `ObjectQLPlugin` (`@objectstack/objectql`, which
* calls `registerApp` per package) and `MetadataPlugin`
* (`@objectstack/metadata`, whose artifact door registers each package body's
* collections stamped with that body's id). `@objectstack/objectql` depends on
* `@objectstack/metadata`, so the metadata door physically cannot import this
* module from where it started life.
*
* The alternative — a second read of `packages[]` inside the metadata door —
* would have split more than the sort: this function is also the GATE (it
* parses each entry against `ArtifactPackageSchema` and refuses a duplicate
* package id), so two readers would have disagreed about which artifacts are
* loadable, not just about what order to load them in. `@objectstack/core`
* already owns `resolvePluginOrder` and is already a dependency of both
* readers, so hosting it here adds NO package edge to the graph. ⛔ Do not
* re-home this next to either reader; the next reader will have the same
* problem.
*
* ## Both shapes are read (D4), and the fallback is the compatibility mechanism
*
* - `packages` present → iterate it.
Expand DownExpand Up@@ -102,7 +122,7 @@
* missing-dependency semantics are re-adjudicated here.
*/

import { resolvePluginOrder, type OrderablePlugin } from '@objectstack/core';
import { resolvePluginOrder, type OrderablePlugin } from './plugin-order.js';
import { ArtifactPackageSchema } from '@objectstack/spec';

/**
Expand Down
7 changes: 7 additions & 0 deletions packages/core/src/index.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -10,6 +10,13 @@
export * from './kernel-base.js';
export * from './kernel.js';
export * from './plugin-order.js';
// ADR-0130 D4/D5 — the ONE reader of a release artifact's `packages[]`, and the
// ONE place it is ordered. It lives here rather than beside a reader because it
// has two of them in packages that cannot import each other (`@objectstack/
// objectql`'s load path and `@objectstack/metadata`'s artifact door), and
// because the ordering it performs is `resolvePluginOrder` directly above.
// `@objectstack/objectql` re-exports it, so its published surface is unchanged.
export * from './artifact-packages.js';
export * from './lite-kernel.js';
export * from './types.js';
export * from './logger.js';
Expand Down
Loading
Loading
, '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
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 75 additions & 0 deletions .changeset/adr0130-metadata-door-reads-packages.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
---
"@objectstack/metadata": patch
"@objectstack/core": minor
"@objectstack/objectql": patch
"@objectstack/runtime": patch
---

fix(metadata): register a `packages[]` artifact per package at the metadata door so every object has one owner across every door (#14599)

A release artifact carrying `packages[]` (ADR-0130 D4) was read at the metadata
door as if it carried one package: `MetadataPlugin._parseAndRegisterArtifact`
iterated the **flattened top level** and stamped every item with the artifact's
own `manifest.id`. For an artifact composed with `composeStacks(…, { manifest:
'preserve' })` that id is one arbitrary member's — `selectManifest`'s `'last'`
pick — so a two-package artifact registered the **module's** object under the
**App** package's identity, while the ObjectQL load path, reading the same
artifact's `packages[]`, owned it under the module's.

The platform then held two answers to "who owns this object", and which one a
consumer saw depended on the door it went through. Measured on a real boot of
`examples/app-multi-package`:

- `GET /api/v1/meta/object` served `crm_order` **twice** — the list merge keys
slots by `${packageId}${name}`, so the two differently-attributed copies
landed in two slots;
- `GET /api/v1/meta/object?package=<the App package>` returned the **module's**
object, because the App-stamped copy was re-ingested into the registry as that
package's contribution;
- the layers door named the App package while the item door and
`GET /api/v1/packages` named the module;
- Studio's Data pillar for the App package listed the module's object — ADR-0130
Consequences §1.3a ("Studio's scope is the package") did not hold.

**The door now reads both shapes, and attributes every item to the body it was
found in.** `packages` present → each assembled package body's collections are
registered stamped with **that body's** id; `packages` absent → the single
`manifest` branch runs exactly as before (D7). The owner is read off the body an
item was found in — never reverse-derived by matching a top-level item's name
against a name-to-package index, which would be the second metadata-identity
resolution path #14512's triage rejected by name.

**Ordering and the entry gate are reused, not re-derived (D5).** The door calls
the same `resolveArtifactPackageOrder` the ObjectQL load path calls, so the two
readers of one `packages[]` cannot disagree about the registration order **or**
about which artifacts are loadable at all.

⚠️ **`resolveArtifactPackageOrder` / `artifactPackageId` moved to
`@objectstack/core`** — hence the `minor` there. They were in
`@objectstack/objectql`, which **depends on** `@objectstack/metadata`, so the
metadata door could not import them from where they lived; `@objectstack/core`
already owns `resolvePluginOrder` and is already a dependency of both readers,
so hosting them there adds **no edge** to the package graph. `@objectstack/objectql`
re-exports both under their existing names — its published surface is unchanged,
which is why it is graded `patch`. `@objectstack/runtime` is `patch` for the
dispatcher error vocabulary's `file:` anchors, repointed at the new path.

**Single-package artifacts are byte-for-byte unaffected (D7)**, measured rather
than asserted: the whole `manager.register` sequence for a single-`manifest`
artifact — every call, in order, with the id and version each item was stamped
with — is pinned as a literal in
`packages/metadata/src/plugin-artifact-packages-attribution.test.ts` and was
recorded identically on both legs of the ablation. A real boot of
`examples/app-todo` answers every door identically before and after.

**Nothing a booted instance can see today disappears.** Every live
`ARTIFACT_FIELD_TO_TYPE` key is a member of `AssembledPackageBodySchema`
(measured, not assumed), so iterating bodies loses no collection; and because
`packages` composes by `concat`, an artifact whose top level carries a
definition no package body repeats keeps it — registered once, attributed to the
artifact's own identity, and logged, because it means the artifact's two halves
disagree about what it ships.

⛔ The **producer** half is untouched: `composeStacks` and `os build` keep
emitting the flattened top level alongside `packages[]`. Whether they should is
#14512's decision, not this door's.
Original file line numberDiff line numberDiff line change
Expand Up@@ -9,6 +9,26 @@
* turns an artifact — either shape — into the ordered list of manifests the
* load path registers.
*
* ## Why this lives in `@objectstack/core` and not next to one of its readers
*
* There are TWO readers of `packages[]`, not one, and they sit in packages that
* cannot import each other: `ObjectQLPlugin` (`@objectstack/objectql`, which
* calls `registerApp` per package) and `MetadataPlugin`
* (`@objectstack/metadata`, whose artifact door registers each package body's
* collections stamped with that body's id). `@objectstack/objectql` depends on
* `@objectstack/metadata`, so the metadata door physically cannot import this
* module from where it started life.
*
* The alternative — a second read of `packages[]` inside the metadata door —
* would have split more than the sort: this function is also the GATE (it
* parses each entry against `ArtifactPackageSchema` and refuses a duplicate
* package id), so two readers would have disagreed about which artifacts are
* loadable, not just about what order to load them in. `@objectstack/core`
* already owns `resolvePluginOrder` and is already a dependency of both
* readers, so hosting it here adds NO package edge to the graph. ⛔ Do not
* re-home this next to either reader; the next reader will have the same
* problem.
*
* ## Both shapes are read (D4), and the fallback is the compatibility mechanism
*
* - `packages` present → iterate it.
Expand DownExpand Up@@ -102,7 +122,7 @@
* missing-dependency semantics are re-adjudicated here.
*/

import { resolvePluginOrder, type OrderablePlugin } from '@objectstack/core';
import { resolvePluginOrder, type OrderablePlugin } from './plugin-order.js';
import { ArtifactPackageSchema } from '@objectstack/spec';

/**
Expand Down
7 changes: 7 additions & 0 deletions packages/core/src/index.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -10,6 +10,13 @@
export * from './kernel-base.js';
export * from './kernel.js';
export * from './plugin-order.js';
// ADR-0130 D4/D5 — the ONE reader of a release artifact's `packages[]`, and the
// ONE place it is ordered. It lives here rather than beside a reader because it
// has two of them in packages that cannot import each other (`@objectstack/
// objectql`'s load path and `@objectstack/metadata`'s artifact door), and
// because the ordering it performs is `resolvePluginOrder` directly above.
// `@objectstack/objectql` re-exports it, so its published surface is unchanged.
export * from './artifact-packages.js';
export * from './lite-kernel.js';
export * from './types.js';
export * from './logger.js';
Expand Down
Loading
Loading
, '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
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 75 additions & 0 deletions .changeset/adr0130-metadata-door-reads-packages.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
---
"@objectstack/metadata": patch
"@objectstack/core": minor
"@objectstack/objectql": patch
"@objectstack/runtime": patch
---

fix(metadata): register a `packages[]` artifact per package at the metadata door so every object has one owner across every door (#14599)

A release artifact carrying `packages[]` (ADR-0130 D4) was read at the metadata
door as if it carried one package: `MetadataPlugin._parseAndRegisterArtifact`
iterated the **flattened top level** and stamped every item with the artifact's
own `manifest.id`. For an artifact composed with `composeStacks(…, { manifest:
'preserve' })` that id is one arbitrary member's — `selectManifest`'s `'last'`
pick — so a two-package artifact registered the **module's** object under the
**App** package's identity, while the ObjectQL load path, reading the same
artifact's `packages[]`, owned it under the module's.

The platform then held two answers to "who owns this object", and which one a
consumer saw depended on the door it went through. Measured on a real boot of
`examples/app-multi-package`:

- `GET /api/v1/meta/object` served `crm_order` **twice** — the list merge keys
slots by `${packageId}${name}`, so the two differently-attributed copies
landed in two slots;
- `GET /api/v1/meta/object?package=<the App package>` returned the **module's**
object, because the App-stamped copy was re-ingested into the registry as that
package's contribution;
- the layers door named the App package while the item door and
`GET /api/v1/packages` named the module;
- Studio's Data pillar for the App package listed the module's object — ADR-0130
Consequences §1.3a ("Studio's scope is the package") did not hold.

**The door now reads both shapes, and attributes every item to the body it was
found in.** `packages` present → each assembled package body's collections are
registered stamped with **that body's** id; `packages` absent → the single
`manifest` branch runs exactly as before (D7). The owner is read off the body an
item was found in — never reverse-derived by matching a top-level item's name
against a name-to-package index, which would be the second metadata-identity
resolution path #14512's triage rejected by name.

**Ordering and the entry gate are reused, not re-derived (D5).** The door calls
the same `resolveArtifactPackageOrder` the ObjectQL load path calls, so the two
readers of one `packages[]` cannot disagree about the registration order **or**
about which artifacts are loadable at all.

⚠️ **`resolveArtifactPackageOrder` / `artifactPackageId` moved to
`@objectstack/core`** — hence the `minor` there. They were in
`@objectstack/objectql`, which **depends on** `@objectstack/metadata`, so the
metadata door could not import them from where they lived; `@objectstack/core`
already owns `resolvePluginOrder` and is already a dependency of both readers,
so hosting them there adds **no edge** to the package graph. `@objectstack/objectql`
re-exports both under their existing names — its published surface is unchanged,
which is why it is graded `patch`. `@objectstack/runtime` is `patch` for the
dispatcher error vocabulary's `file:` anchors, repointed at the new path.

**Single-package artifacts are byte-for-byte unaffected (D7)**, measured rather
than asserted: the whole `manager.register` sequence for a single-`manifest`
artifact — every call, in order, with the id and version each item was stamped
with — is pinned as a literal in
`packages/metadata/src/plugin-artifact-packages-attribution.test.ts` and was
recorded identically on both legs of the ablation. A real boot of
`examples/app-todo` answers every door identically before and after.

**Nothing a booted instance can see today disappears.** Every live
`ARTIFACT_FIELD_TO_TYPE` key is a member of `AssembledPackageBodySchema`
(measured, not assumed), so iterating bodies loses no collection; and because
`packages` composes by `concat`, an artifact whose top level carries a
definition no package body repeats keeps it — registered once, attributed to the
artifact's own identity, and logged, because it means the artifact's two halves
disagree about what it ships.

⛔ The **producer** half is untouched: `composeStacks` and `os build` keep
emitting the flattened top level alongside `packages[]`. Whether they should is
#14512's decision, not this door's.
Original file line numberDiff line numberDiff line change
Expand Up@@ -9,6 +9,26 @@
* turns an artifact — either shape — into the ordered list of manifests the
* load path registers.
*
* ## Why this lives in `@objectstack/core` and not next to one of its readers
*
* There are TWO readers of `packages[]`, not one, and they sit in packages that
* cannot import each other: `ObjectQLPlugin` (`@objectstack/objectql`, which
* calls `registerApp` per package) and `MetadataPlugin`
* (`@objectstack/metadata`, whose artifact door registers each package body's
* collections stamped with that body's id). `@objectstack/objectql` depends on
* `@objectstack/metadata`, so the metadata door physically cannot import this
* module from where it started life.
*
* The alternative — a second read of `packages[]` inside the metadata door —
* would have split more than the sort: this function is also the GATE (it
* parses each entry against `ArtifactPackageSchema` and refuses a duplicate
* package id), so two readers would have disagreed about which artifacts are
* loadable, not just about what order to load them in. `@objectstack/core`
* already owns `resolvePluginOrder` and is already a dependency of both
* readers, so hosting it here adds NO package edge to the graph. ⛔ Do not
* re-home this next to either reader; the next reader will have the same
* problem.
*
* ## Both shapes are read (D4), and the fallback is the compatibility mechanism
*
* - `packages` present → iterate it.
Expand DownExpand Up@@ -102,7 +122,7 @@
* missing-dependency semantics are re-adjudicated here.
*/

import { resolvePluginOrder, type OrderablePlugin } from '@objectstack/core';
import { resolvePluginOrder, type OrderablePlugin } from './plugin-order.js';
import { ArtifactPackageSchema } from '@objectstack/spec';

/**
Expand Down
7 changes: 7 additions & 0 deletions packages/core/src/index.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -10,6 +10,13 @@
export * from './kernel-base.js';
export * from './kernel.js';
export * from './plugin-order.js';
// ADR-0130 D4/D5 — the ONE reader of a release artifact's `packages[]`, and the
// ONE place it is ordered. It lives here rather than beside a reader because it
// has two of them in packages that cannot import each other (`@objectstack/
// objectql`'s load path and `@objectstack/metadata`'s artifact door), and
// because the ordering it performs is `resolvePluginOrder` directly above.
// `@objectstack/objectql` re-exports it, so its published surface is unchanged.
export * from './artifact-packages.js';
export * from './lite-kernel.js';
export * from './types.js';
export * from './logger.js';
Expand Down
Loading
Loading
, '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
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 75 additions & 0 deletions .changeset/adr0130-metadata-door-reads-packages.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
---
"@objectstack/metadata": patch
"@objectstack/core": minor
"@objectstack/objectql": patch
"@objectstack/runtime": patch
---

fix(metadata): register a `packages[]` artifact per package at the metadata door so every object has one owner across every door (#14599)

A release artifact carrying `packages[]` (ADR-0130 D4) was read at the metadata
door as if it carried one package: `MetadataPlugin._parseAndRegisterArtifact`
iterated the **flattened top level** and stamped every item with the artifact's
own `manifest.id`. For an artifact composed with `composeStacks(…, { manifest:
'preserve' })` that id is one arbitrary member's — `selectManifest`'s `'last'`
pick — so a two-package artifact registered the **module's** object under the
**App** package's identity, while the ObjectQL load path, reading the same
artifact's `packages[]`, owned it under the module's.

The platform then held two answers to "who owns this object", and which one a
consumer saw depended on the door it went through. Measured on a real boot of
`examples/app-multi-package`:

- `GET /api/v1/meta/object` served `crm_order` **twice** — the list merge keys
slots by `${packageId}${name}`, so the two differently-attributed copies
landed in two slots;
- `GET /api/v1/meta/object?package=<the App package>` returned the **module's**
object, because the App-stamped copy was re-ingested into the registry as that
package's contribution;
- the layers door named the App package while the item door and
`GET /api/v1/packages` named the module;
- Studio's Data pillar for the App package listed the module's object — ADR-0130
Consequences §1.3a ("Studio's scope is the package") did not hold.

**The door now reads both shapes, and attributes every item to the body it was
found in.** `packages` present → each assembled package body's collections are
registered stamped with **that body's** id; `packages` absent → the single
`manifest` branch runs exactly as before (D7). The owner is read off the body an
item was found in — never reverse-derived by matching a top-level item's name
against a name-to-package index, which would be the second metadata-identity
resolution path #14512's triage rejected by name.

**Ordering and the entry gate are reused, not re-derived (D5).** The door calls
the same `resolveArtifactPackageOrder` the ObjectQL load path calls, so the two
readers of one `packages[]` cannot disagree about the registration order **or**
about which artifacts are loadable at all.

⚠️ **`resolveArtifactPackageOrder` / `artifactPackageId` moved to
`@objectstack/core`** — hence the `minor` there. They were in
`@objectstack/objectql`, which **depends on** `@objectstack/metadata`, so the
metadata door could not import them from where they lived; `@objectstack/core`
already owns `resolvePluginOrder` and is already a dependency of both readers,
so hosting them there adds **no edge** to the package graph. `@objectstack/objectql`
re-exports both under their existing names — its published surface is unchanged,
which is why it is graded `patch`. `@objectstack/runtime` is `patch` for the
dispatcher error vocabulary's `file:` anchors, repointed at the new path.

**Single-package artifacts are byte-for-byte unaffected (D7)**, measured rather
than asserted: the whole `manager.register` sequence for a single-`manifest`
artifact — every call, in order, with the id and version each item was stamped
with — is pinned as a literal in
`packages/metadata/src/plugin-artifact-packages-attribution.test.ts` and was
recorded identically on both legs of the ablation. A real boot of
`examples/app-todo` answers every door identically before and after.

**Nothing a booted instance can see today disappears.** Every live
`ARTIFACT_FIELD_TO_TYPE` key is a member of `AssembledPackageBodySchema`
(measured, not assumed), so iterating bodies loses no collection; and because
`packages` composes by `concat`, an artifact whose top level carries a
definition no package body repeats keeps it — registered once, attributed to the
artifact's own identity, and logged, because it means the artifact's two halves
disagree about what it ships.

⛔ The **producer** half is untouched: `composeStacks` and `os build` keep
emitting the flattened top level alongside `packages[]`. Whether they should is
#14512's decision, not this door's.
Original file line numberDiff line numberDiff line change
Expand Up@@ -9,6 +9,26 @@
* turns an artifact — either shape — into the ordered list of manifests the
* load path registers.
*
* ## Why this lives in `@objectstack/core` and not next to one of its readers
*
* There are TWO readers of `packages[]`, not one, and they sit in packages that
* cannot import each other: `ObjectQLPlugin` (`@objectstack/objectql`, which
* calls `registerApp` per package) and `MetadataPlugin`
* (`@objectstack/metadata`, whose artifact door registers each package body's
* collections stamped with that body's id). `@objectstack/objectql` depends on
* `@objectstack/metadata`, so the metadata door physically cannot import this
* module from where it started life.
*
* The alternative — a second read of `packages[]` inside the metadata door —
* would have split more than the sort: this function is also the GATE (it
* parses each entry against `ArtifactPackageSchema` and refuses a duplicate
* package id), so two readers would have disagreed about which artifacts are
* loadable, not just about what order to load them in. `@objectstack/core`
* already owns `resolvePluginOrder` and is already a dependency of both
* readers, so hosting it here adds NO package edge to the graph. ⛔ Do not
* re-home this next to either reader; the next reader will have the same
* problem.
*
* ## Both shapes are read (D4), and the fallback is the compatibility mechanism
*
* - `packages` present → iterate it.
Expand DownExpand Up@@ -102,7 +122,7 @@
* missing-dependency semantics are re-adjudicated here.
*/

import { resolvePluginOrder, type OrderablePlugin } from '@objectstack/core';
import { resolvePluginOrder, type OrderablePlugin } from './plugin-order.js';
import { ArtifactPackageSchema } from '@objectstack/spec';

/**
Expand Down
7 changes: 7 additions & 0 deletions packages/core/src/index.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -10,6 +10,13 @@
export * from './kernel-base.js';
export * from './kernel.js';
export * from './plugin-order.js';
// ADR-0130 D4/D5 — the ONE reader of a release artifact's `packages[]`, and the
// ONE place it is ordered. It lives here rather than beside a reader because it
// has two of them in packages that cannot import each other (`@objectstack/
// objectql`'s load path and `@objectstack/metadata`'s artifact door), and
// because the ordering it performs is `resolvePluginOrder` directly above.
// `@objectstack/objectql` re-exports it, so its published surface is unchanged.
export * from './artifact-packages.js';
export * from './lite-kernel.js';
export * from './types.js';
export * from './logger.js';
Expand Down
Loading
Loading
, '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
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 75 additions & 0 deletions .changeset/adr0130-metadata-door-reads-packages.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
---
"@objectstack/metadata": patch
"@objectstack/core": minor
"@objectstack/objectql": patch
"@objectstack/runtime": patch
---

fix(metadata): register a `packages[]` artifact per package at the metadata door so every object has one owner across every door (#14599)

A release artifact carrying `packages[]` (ADR-0130 D4) was read at the metadata
door as if it carried one package: `MetadataPlugin._parseAndRegisterArtifact`
iterated the **flattened top level** and stamped every item with the artifact's
own `manifest.id`. For an artifact composed with `composeStacks(…, { manifest:
'preserve' })` that id is one arbitrary member's — `selectManifest`'s `'last'`
pick — so a two-package artifact registered the **module's** object under the
**App** package's identity, while the ObjectQL load path, reading the same
artifact's `packages[]`, owned it under the module's.

The platform then held two answers to "who owns this object", and which one a
consumer saw depended on the door it went through. Measured on a real boot of
`examples/app-multi-package`:

- `GET /api/v1/meta/object` served `crm_order` **twice** — the list merge keys
slots by `${packageId}${name}`, so the two differently-attributed copies
landed in two slots;
- `GET /api/v1/meta/object?package=<the App package>` returned the **module's**
object, because the App-stamped copy was re-ingested into the registry as that
package's contribution;
- the layers door named the App package while the item door and
`GET /api/v1/packages` named the module;
- Studio's Data pillar for the App package listed the module's object — ADR-0130
Consequences §1.3a ("Studio's scope is the package") did not hold.

**The door now reads both shapes, and attributes every item to the body it was
found in.** `packages` present → each assembled package body's collections are
registered stamped with **that body's** id; `packages` absent → the single
`manifest` branch runs exactly as before (D7). The owner is read off the body an
item was found in — never reverse-derived by matching a top-level item's name
against a name-to-package index, which would be the second metadata-identity
resolution path #14512's triage rejected by name.

**Ordering and the entry gate are reused, not re-derived (D5).** The door calls
the same `resolveArtifactPackageOrder` the ObjectQL load path calls, so the two
readers of one `packages[]` cannot disagree about the registration order **or**
about which artifacts are loadable at all.

⚠️ **`resolveArtifactPackageOrder` / `artifactPackageId` moved to
`@objectstack/core`** — hence the `minor` there. They were in
`@objectstack/objectql`, which **depends on** `@objectstack/metadata`, so the
metadata door could not import them from where they lived; `@objectstack/core`
already owns `resolvePluginOrder` and is already a dependency of both readers,
so hosting them there adds **no edge** to the package graph. `@objectstack/objectql`
re-exports both under their existing names — its published surface is unchanged,
which is why it is graded `patch`. `@objectstack/runtime` is `patch` for the
dispatcher error vocabulary's `file:` anchors, repointed at the new path.

**Single-package artifacts are byte-for-byte unaffected (D7)**, measured rather
than asserted: the whole `manager.register` sequence for a single-`manifest`
artifact — every call, in order, with the id and version each item was stamped
with — is pinned as a literal in
`packages/metadata/src/plugin-artifact-packages-attribution.test.ts` and was
recorded identically on both legs of the ablation. A real boot of
`examples/app-todo` answers every door identically before and after.

**Nothing a booted instance can see today disappears.** Every live
`ARTIFACT_FIELD_TO_TYPE` key is a member of `AssembledPackageBodySchema`
(measured, not assumed), so iterating bodies loses no collection; and because
`packages` composes by `concat`, an artifact whose top level carries a
definition no package body repeats keeps it — registered once, attributed to the
artifact's own identity, and logged, because it means the artifact's two halves
disagree about what it ships.

⛔ The **producer** half is untouched: `composeStacks` and `os build` keep
emitting the flattened top level alongside `packages[]`. Whether they should is
#14512's decision, not this door's.
Original file line numberDiff line numberDiff line change
Expand Up@@ -9,6 +9,26 @@
* turns an artifact — either shape — into the ordered list of manifests the
* load path registers.
*
* ## Why this lives in `@objectstack/core` and not next to one of its readers
*
* There are TWO readers of `packages[]`, not one, and they sit in packages that
* cannot import each other: `ObjectQLPlugin` (`@objectstack/objectql`, which
* calls `registerApp` per package) and `MetadataPlugin`
* (`@objectstack/metadata`, whose artifact door registers each package body's
* collections stamped with that body's id). `@objectstack/objectql` depends on
* `@objectstack/metadata`, so the metadata door physically cannot import this
* module from where it started life.
*
* The alternative — a second read of `packages[]` inside the metadata door —
* would have split more than the sort: this function is also the GATE (it
* parses each entry against `ArtifactPackageSchema` and refuses a duplicate
* package id), so two readers would have disagreed about which artifacts are
* loadable, not just about what order to load them in. `@objectstack/core`
* already owns `resolvePluginOrder` and is already a dependency of both
* readers, so hosting it here adds NO package edge to the graph. ⛔ Do not
* re-home this next to either reader; the next reader will have the same
* problem.
*
* ## Both shapes are read (D4), and the fallback is the compatibility mechanism
*
* - `packages` present → iterate it.
Expand DownExpand Up@@ -102,7 +122,7 @@
* missing-dependency semantics are re-adjudicated here.
*/

import { resolvePluginOrder, type OrderablePlugin } from '@objectstack/core';
import { resolvePluginOrder, type OrderablePlugin } from './plugin-order.js';
import { ArtifactPackageSchema } from '@objectstack/spec';

/**
Expand Down
7 changes: 7 additions & 0 deletions packages/core/src/index.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -10,6 +10,13 @@
export * from './kernel-base.js';
export * from './kernel.js';
export * from './plugin-order.js';
// ADR-0130 D4/D5 — the ONE reader of a release artifact's `packages[]`, and the
// ONE place it is ordered. It lives here rather than beside a reader because it
// has two of them in packages that cannot import each other (`@objectstack/
// objectql`'s load path and `@objectstack/metadata`'s artifact door), and
// because the ordering it performs is `resolvePluginOrder` directly above.
// `@objectstack/objectql` re-exports it, so its published surface is unchanged.
export * from './artifact-packages.js';
export * from './lite-kernel.js';
export * from './types.js';
export * from './logger.js';
Expand Down
Loading
Loading
, '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
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 75 additions & 0 deletions .changeset/adr0130-metadata-door-reads-packages.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
---
"@objectstack/metadata": patch
"@objectstack/core": minor
"@objectstack/objectql": patch
"@objectstack/runtime": patch
---

fix(metadata): register a `packages[]` artifact per package at the metadata door so every object has one owner across every door (#14599)

A release artifact carrying `packages[]` (ADR-0130 D4) was read at the metadata
door as if it carried one package: `MetadataPlugin._parseAndRegisterArtifact`
iterated the **flattened top level** and stamped every item with the artifact's
own `manifest.id`. For an artifact composed with `composeStacks(…, { manifest:
'preserve' })` that id is one arbitrary member's — `selectManifest`'s `'last'`
pick — so a two-package artifact registered the **module's** object under the
**App** package's identity, while the ObjectQL load path, reading the same
artifact's `packages[]`, owned it under the module's.

The platform then held two answers to "who owns this object", and which one a
consumer saw depended on the door it went through. Measured on a real boot of
`examples/app-multi-package`:

- `GET /api/v1/meta/object` served `crm_order` **twice** — the list merge keys
slots by `${packageId}${name}`, so the two differently-attributed copies
landed in two slots;
- `GET /api/v1/meta/object?package=<the App package>` returned the **module's**
object, because the App-stamped copy was re-ingested into the registry as that
package's contribution;
- the layers door named the App package while the item door and
`GET /api/v1/packages` named the module;
- Studio's Data pillar for the App package listed the module's object — ADR-0130
Consequences §1.3a ("Studio's scope is the package") did not hold.

**The door now reads both shapes, and attributes every item to the body it was
found in.** `packages` present → each assembled package body's collections are
registered stamped with **that body's** id; `packages` absent → the single
`manifest` branch runs exactly as before (D7). The owner is read off the body an
item was found in — never reverse-derived by matching a top-level item's name
against a name-to-package index, which would be the second metadata-identity
resolution path #14512's triage rejected by name.

**Ordering and the entry gate are reused, not re-derived (D5).** The door calls
the same `resolveArtifactPackageOrder` the ObjectQL load path calls, so the two
readers of one `packages[]` cannot disagree about the registration order **or**
about which artifacts are loadable at all.

⚠️ **`resolveArtifactPackageOrder` / `artifactPackageId` moved to
`@objectstack/core`** — hence the `minor` there. They were in
`@objectstack/objectql`, which **depends on** `@objectstack/metadata`, so the
metadata door could not import them from where they lived; `@objectstack/core`
already owns `resolvePluginOrder` and is already a dependency of both readers,
so hosting them there adds **no edge** to the package graph. `@objectstack/objectql`
re-exports both under their existing names — its published surface is unchanged,
which is why it is graded `patch`. `@objectstack/runtime` is `patch` for the
dispatcher error vocabulary's `file:` anchors, repointed at the new path.

**Single-package artifacts are byte-for-byte unaffected (D7)**, measured rather
than asserted: the whole `manager.register` sequence for a single-`manifest`
artifact — every call, in order, with the id and version each item was stamped
with — is pinned as a literal in
`packages/metadata/src/plugin-artifact-packages-attribution.test.ts` and was
recorded identically on both legs of the ablation. A real boot of
`examples/app-todo` answers every door identically before and after.

**Nothing a booted instance can see today disappears.** Every live
`ARTIFACT_FIELD_TO_TYPE` key is a member of `AssembledPackageBodySchema`
(measured, not assumed), so iterating bodies loses no collection; and because
`packages` composes by `concat`, an artifact whose top level carries a
definition no package body repeats keeps it — registered once, attributed to the
artifact's own identity, and logged, because it means the artifact's two halves
disagree about what it ships.

⛔ The **producer** half is untouched: `composeStacks` and `os build` keep
emitting the flattened top level alongside `packages[]`. Whether they should is
#14512's decision, not this door's.
Original file line numberDiff line numberDiff line change
Expand Up@@ -9,6 +9,26 @@
* turns an artifact — either shape — into the ordered list of manifests the
* load path registers.
*
* ## Why this lives in `@objectstack/core` and not next to one of its readers
*
* There are TWO readers of `packages[]`, not one, and they sit in packages that
* cannot import each other: `ObjectQLPlugin` (`@objectstack/objectql`, which
* calls `registerApp` per package) and `MetadataPlugin`
* (`@objectstack/metadata`, whose artifact door registers each package body's
* collections stamped with that body's id). `@objectstack/objectql` depends on
* `@objectstack/metadata`, so the metadata door physically cannot import this
* module from where it started life.
*
* The alternative — a second read of `packages[]` inside the metadata door —
* would have split more than the sort: this function is also the GATE (it
* parses each entry against `ArtifactPackageSchema` and refuses a duplicate
* package id), so two readers would have disagreed about which artifacts are
* loadable, not just about what order to load them in. `@objectstack/core`
* already owns `resolvePluginOrder` and is already a dependency of both
* readers, so hosting it here adds NO package edge to the graph. ⛔ Do not
* re-home this next to either reader; the next reader will have the same
* problem.
*
* ## Both shapes are read (D4), and the fallback is the compatibility mechanism
*
* - `packages` present → iterate it.
Expand DownExpand Up@@ -102,7 +122,7 @@
* missing-dependency semantics are re-adjudicated here.
*/

import { resolvePluginOrder, type OrderablePlugin } from '@objectstack/core';
import { resolvePluginOrder, type OrderablePlugin } from './plugin-order.js';
import { ArtifactPackageSchema } from '@objectstack/spec';

/**
Expand Down
7 changes: 7 additions & 0 deletions packages/core/src/index.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -10,6 +10,13 @@
export * from './kernel-base.js';
export * from './kernel.js';
export * from './plugin-order.js';
// ADR-0130 D4/D5 — the ONE reader of a release artifact's `packages[]`, and the
// ONE place it is ordered. It lives here rather than beside a reader because it
// has two of them in packages that cannot import each other (`@objectstack/
// objectql`'s load path and `@objectstack/metadata`'s artifact door), and
// because the ordering it performs is `resolvePluginOrder` directly above.
// `@objectstack/objectql` re-exports it, so its published surface is unchanged.
export * from './artifact-packages.js';
export * from './lite-kernel.js';
export * from './types.js';
export * from './logger.js';
Expand Down
Loading
Loading
, '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
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 75 additions & 0 deletions .changeset/adr0130-metadata-door-reads-packages.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
---
"@objectstack/metadata": patch
"@objectstack/core": minor
"@objectstack/objectql": patch
"@objectstack/runtime": patch
---

fix(metadata): register a `packages[]` artifact per package at the metadata door so every object has one owner across every door (#14599)

A release artifact carrying `packages[]` (ADR-0130 D4) was read at the metadata
door as if it carried one package: `MetadataPlugin._parseAndRegisterArtifact`
iterated the **flattened top level** and stamped every item with the artifact's
own `manifest.id`. For an artifact composed with `composeStacks(…, { manifest:
'preserve' })` that id is one arbitrary member's — `selectManifest`'s `'last'`
pick — so a two-package artifact registered the **module's** object under the
**App** package's identity, while the ObjectQL load path, reading the same
artifact's `packages[]`, owned it under the module's.

The platform then held two answers to "who owns this object", and which one a
consumer saw depended on the door it went through. Measured on a real boot of
`examples/app-multi-package`:

- `GET /api/v1/meta/object` served `crm_order` **twice** — the list merge keys
slots by `${packageId}${name}`, so the two differently-attributed copies
landed in two slots;
- `GET /api/v1/meta/object?package=<the App package>` returned the **module's**
object, because the App-stamped copy was re-ingested into the registry as that
package's contribution;
- the layers door named the App package while the item door and
`GET /api/v1/packages` named the module;
- Studio's Data pillar for the App package listed the module's object — ADR-0130
Consequences §1.3a ("Studio's scope is the package") did not hold.

**The door now reads both shapes, and attributes every item to the body it was
found in.** `packages` present → each assembled package body's collections are
registered stamped with **that body's** id; `packages` absent → the single
`manifest` branch runs exactly as before (D7). The owner is read off the body an
item was found in — never reverse-derived by matching a top-level item's name
against a name-to-package index, which would be the second metadata-identity
resolution path #14512's triage rejected by name.

**Ordering and the entry gate are reused, not re-derived (D5).** The door calls
the same `resolveArtifactPackageOrder` the ObjectQL load path calls, so the two
readers of one `packages[]` cannot disagree about the registration order **or**
about which artifacts are loadable at all.

⚠️ **`resolveArtifactPackageOrder` / `artifactPackageId` moved to
`@objectstack/core`** — hence the `minor` there. They were in
`@objectstack/objectql`, which **depends on** `@objectstack/metadata`, so the
metadata door could not import them from where they lived; `@objectstack/core`
already owns `resolvePluginOrder` and is already a dependency of both readers,
so hosting them there adds **no edge** to the package graph. `@objectstack/objectql`
re-exports both under their existing names — its published surface is unchanged,
which is why it is graded `patch`. `@objectstack/runtime` is `patch` for the
dispatcher error vocabulary's `file:` anchors, repointed at the new path.

**Single-package artifacts are byte-for-byte unaffected (D7)**, measured rather
than asserted: the whole `manager.register` sequence for a single-`manifest`
artifact — every call, in order, with the id and version each item was stamped
with — is pinned as a literal in
`packages/metadata/src/plugin-artifact-packages-attribution.test.ts` and was
recorded identically on both legs of the ablation. A real boot of
`examples/app-todo` answers every door identically before and after.

**Nothing a booted instance can see today disappears.** Every live
`ARTIFACT_FIELD_TO_TYPE` key is a member of `AssembledPackageBodySchema`
(measured, not assumed), so iterating bodies loses no collection; and because
`packages` composes by `concat`, an artifact whose top level carries a
definition no package body repeats keeps it — registered once, attributed to the
artifact's own identity, and logged, because it means the artifact's two halves
disagree about what it ships.

⛔ The **producer** half is untouched: `composeStacks` and `os build` keep
emitting the flattened top level alongside `packages[]`. Whether they should is
#14512's decision, not this door's.
Original file line numberDiff line numberDiff line change
Expand Up@@ -9,6 +9,26 @@
* turns an artifact — either shape — into the ordered list of manifests the
* load path registers.
*
* ## Why this lives in `@objectstack/core` and not next to one of its readers
*
* There are TWO readers of `packages[]`, not one, and they sit in packages that
* cannot import each other: `ObjectQLPlugin` (`@objectstack/objectql`, which
* calls `registerApp` per package) and `MetadataPlugin`
* (`@objectstack/metadata`, whose artifact door registers each package body's
* collections stamped with that body's id). `@objectstack/objectql` depends on
* `@objectstack/metadata`, so the metadata door physically cannot import this
* module from where it started life.
*
* The alternative — a second read of `packages[]` inside the metadata door —
* would have split more than the sort: this function is also the GATE (it
* parses each entry against `ArtifactPackageSchema` and refuses a duplicate
* package id), so two readers would have disagreed about which artifacts are
* loadable, not just about what order to load them in. `@objectstack/core`
* already owns `resolvePluginOrder` and is already a dependency of both
* readers, so hosting it here adds NO package edge to the graph. ⛔ Do not
* re-home this next to either reader; the next reader will have the same
* problem.
*
* ## Both shapes are read (D4), and the fallback is the compatibility mechanism
*
* - `packages` present → iterate it.
Expand DownExpand Up@@ -102,7 +122,7 @@
* missing-dependency semantics are re-adjudicated here.
*/

import { resolvePluginOrder, type OrderablePlugin } from '@objectstack/core';
import { resolvePluginOrder, type OrderablePlugin } from './plugin-order.js';
import { ArtifactPackageSchema } from '@objectstack/spec';

/**
Expand Down
7 changes: 7 additions & 0 deletions packages/core/src/index.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -10,6 +10,13 @@
export * from './kernel-base.js';
export * from './kernel.js';
export * from './plugin-order.js';
// ADR-0130 D4/D5 — the ONE reader of a release artifact's `packages[]`, and the
// ONE place it is ordered. It lives here rather than beside a reader because it
// has two of them in packages that cannot import each other (`@objectstack/
// objectql`'s load path and `@objectstack/metadata`'s artifact door), and
// because the ordering it performs is `resolvePluginOrder` directly above.
// `@objectstack/objectql` re-exports it, so its published surface is unchanged.
export * from './artifact-packages.js';
export * from './lite-kernel.js';
export * from './types.js';
export * from './logger.js';
Expand Down
Loading
Loading
, '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
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 75 additions & 0 deletions .changeset/adr0130-metadata-door-reads-packages.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
---
"@objectstack/metadata": patch
"@objectstack/core": minor
"@objectstack/objectql": patch
"@objectstack/runtime": patch
---

fix(metadata): register a `packages[]` artifact per package at the metadata door so every object has one owner across every door (#14599)

A release artifact carrying `packages[]` (ADR-0130 D4) was read at the metadata
door as if it carried one package: `MetadataPlugin._parseAndRegisterArtifact`
iterated the **flattened top level** and stamped every item with the artifact's
own `manifest.id`. For an artifact composed with `composeStacks(…, { manifest:
'preserve' })` that id is one arbitrary member's — `selectManifest`'s `'last'`
pick — so a two-package artifact registered the **module's** object under the
**App** package's identity, while the ObjectQL load path, reading the same
artifact's `packages[]`, owned it under the module's.

The platform then held two answers to "who owns this object", and which one a
consumer saw depended on the door it went through. Measured on a real boot of
`examples/app-multi-package`:

- `GET /api/v1/meta/object` served `crm_order` **twice** — the list merge keys
slots by `${packageId}${name}`, so the two differently-attributed copies
landed in two slots;
- `GET /api/v1/meta/object?package=<the App package>` returned the **module's**
object, because the App-stamped copy was re-ingested into the registry as that
package's contribution;
- the layers door named the App package while the item door and
`GET /api/v1/packages` named the module;
- Studio's Data pillar for the App package listed the module's object — ADR-0130
Consequences §1.3a ("Studio's scope is the package") did not hold.

**The door now reads both shapes, and attributes every item to the body it was
found in.** `packages` present → each assembled package body's collections are
registered stamped with **that body's** id; `packages` absent → the single
`manifest` branch runs exactly as before (D7). The owner is read off the body an
item was found in — never reverse-derived by matching a top-level item's name
against a name-to-package index, which would be the second metadata-identity
resolution path #14512's triage rejected by name.

**Ordering and the entry gate are reused, not re-derived (D5).** The door calls
the same `resolveArtifactPackageOrder` the ObjectQL load path calls, so the two
readers of one `packages[]` cannot disagree about the registration order **or**
about which artifacts are loadable at all.

⚠️ **`resolveArtifactPackageOrder` / `artifactPackageId` moved to
`@objectstack/core`** — hence the `minor` there. They were in
`@objectstack/objectql`, which **depends on** `@objectstack/metadata`, so the
metadata door could not import them from where they lived; `@objectstack/core`
already owns `resolvePluginOrder` and is already a dependency of both readers,
so hosting them there adds **no edge** to the package graph. `@objectstack/objectql`
re-exports both under their existing names — its published surface is unchanged,
which is why it is graded `patch`. `@objectstack/runtime` is `patch` for the
dispatcher error vocabulary's `file:` anchors, repointed at the new path.

**Single-package artifacts are byte-for-byte unaffected (D7)**, measured rather
than asserted: the whole `manager.register` sequence for a single-`manifest`
artifact — every call, in order, with the id and version each item was stamped
with — is pinned as a literal in
`packages/metadata/src/plugin-artifact-packages-attribution.test.ts` and was
recorded identically on both legs of the ablation. A real boot of
`examples/app-todo` answers every door identically before and after.

**Nothing a booted instance can see today disappears.** Every live
`ARTIFACT_FIELD_TO_TYPE` key is a member of `AssembledPackageBodySchema`
(measured, not assumed), so iterating bodies loses no collection; and because
`packages` composes by `concat`, an artifact whose top level carries a
definition no package body repeats keeps it — registered once, attributed to the
artifact's own identity, and logged, because it means the artifact's two halves
disagree about what it ships.

⛔ The **producer** half is untouched: `composeStacks` and `os build` keep
emitting the flattened top level alongside `packages[]`. Whether they should is
#14512's decision, not this door's.
Original file line numberDiff line numberDiff line change
Expand Up@@ -9,6 +9,26 @@
* turns an artifact — either shape — into the ordered list of manifests the
* load path registers.
*
* ## Why this lives in `@objectstack/core` and not next to one of its readers
*
* There are TWO readers of `packages[]`, not one, and they sit in packages that
* cannot import each other: `ObjectQLPlugin` (`@objectstack/objectql`, which
* calls `registerApp` per package) and `MetadataPlugin`
* (`@objectstack/metadata`, whose artifact door registers each package body's
* collections stamped with that body's id). `@objectstack/objectql` depends on
* `@objectstack/metadata`, so the metadata door physically cannot import this
* module from where it started life.
*
* The alternative — a second read of `packages[]` inside the metadata door —
* would have split more than the sort: this function is also the GATE (it
* parses each entry against `ArtifactPackageSchema` and refuses a duplicate
* package id), so two readers would have disagreed about which artifacts are
* loadable, not just about what order to load them in. `@objectstack/core`
* already owns `resolvePluginOrder` and is already a dependency of both
* readers, so hosting it here adds NO package edge to the graph. ⛔ Do not
* re-home this next to either reader; the next reader will have the same
* problem.
*
* ## Both shapes are read (D4), and the fallback is the compatibility mechanism
*
* - `packages` present → iterate it.
Expand DownExpand Up@@ -102,7 +122,7 @@
* missing-dependency semantics are re-adjudicated here.
*/

import { resolvePluginOrder, type OrderablePlugin } from '@objectstack/core';
import { resolvePluginOrder, type OrderablePlugin } from './plugin-order.js';
import { ArtifactPackageSchema } from '@objectstack/spec';

/**
Expand Down
7 changes: 7 additions & 0 deletions packages/core/src/index.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -10,6 +10,13 @@
export * from './kernel-base.js';
export * from './kernel.js';
export * from './plugin-order.js';
// ADR-0130 D4/D5 — the ONE reader of a release artifact's `packages[]`, and the
// ONE place it is ordered. It lives here rather than beside a reader because it
// has two of them in packages that cannot import each other (`@objectstack/
// objectql`'s load path and `@objectstack/metadata`'s artifact door), and
// because the ordering it performs is `resolvePluginOrder` directly above.
// `@objectstack/objectql` re-exports it, so its published surface is unchanged.
export * from './artifact-packages.js';
export * from './lite-kernel.js';
export * from './types.js';
export * from './logger.js';
Expand Down
Loading
Loading