|
| 1 | +--- |
| 2 | +"@objectstack/metadata": patch |
| 3 | +"@objectstack/core": minor |
| 4 | +"@objectstack/objectql": patch |
| 5 | +"@objectstack/runtime": patch |
| 6 | +--- |
| 7 | + |
| 8 | +fix(metadata): register a `packages[]` artifact per package at the metadata door so every object has one owner across every door (#14599) |
| 9 | + |
| 10 | +A release artifact carrying `packages[]` (ADR-0130 D4) was read at the metadata |
| 11 | +door as if it carried one package: `MetadataPlugin._parseAndRegisterArtifact` |
| 12 | +iterated the **flattened top level** and stamped every item with the artifact's |
| 13 | +own `manifest.id`. For an artifact composed with `composeStacks(…, { manifest: |
| 14 | +'preserve' })` that id is one arbitrary member's — `selectManifest`'s `'last'` |
| 15 | +pick — so a two-package artifact registered the **module's** object under the |
| 16 | +**App** package's identity, while the ObjectQL load path, reading the same |
| 17 | +artifact's `packages[]`, owned it under the module's. |
| 18 | + |
| 19 | +The platform then held two answers to "who owns this object", and which one a |
| 20 | +consumer saw depended on the door it went through. Measured on a real boot of |
| 21 | +`examples/app-multi-package`: |
| 22 | + |
| 23 | +- `GET /api/v1/meta/object` served `crm_order` **twice** — the list merge keys |
| 24 | + slots by `${packageId}${name}`, so the two differently-attributed copies |
| 25 | + landed in two slots; |
| 26 | +- `GET /api/v1/meta/object?package=<the App package>` returned the **module's** |
| 27 | + object, because the App-stamped copy was re-ingested into the registry as that |
| 28 | + package's contribution; |
| 29 | +- the layers door named the App package while the item door and |
| 30 | + `GET /api/v1/packages` named the module; |
| 31 | +- Studio's Data pillar for the App package listed the module's object — ADR-0130 |
| 32 | + Consequences §1.3a ("Studio's scope is the package") did not hold. |
| 33 | + |
| 34 | +**The door now reads both shapes, and attributes every item to the body it was |
| 35 | +found in.** `packages` present → each assembled package body's collections are |
| 36 | +registered stamped with **that body's** id; `packages` absent → the single |
| 37 | +`manifest` branch runs exactly as before (D7). The owner is read off the body an |
| 38 | +item was found in — never reverse-derived by matching a top-level item's name |
| 39 | +against a name-to-package index, which would be the second metadata-identity |
| 40 | +resolution path #14512's triage rejected by name. |
| 41 | + |
| 42 | +**Ordering and the entry gate are reused, not re-derived (D5).** The door calls |
| 43 | +the same `resolveArtifactPackageOrder` the ObjectQL load path calls, so the two |
| 44 | +readers of one `packages[]` cannot disagree about the registration order **or** |
| 45 | +about which artifacts are loadable at all. |
| 46 | + |
| 47 | +⚠️ **`resolveArtifactPackageOrder` / `artifactPackageId` moved to |
| 48 | +`@objectstack/core`** — hence the `minor` there. They were in |
| 49 | +`@objectstack/objectql`, which **depends on** `@objectstack/metadata`, so the |
| 50 | +metadata door could not import them from where they lived; `@objectstack/core` |
| 51 | +already owns `resolvePluginOrder` and is already a dependency of both readers, |
| 52 | +so hosting them there adds **no edge** to the package graph. `@objectstack/objectql` |
| 53 | +re-exports both under their existing names — its published surface is unchanged, |
| 54 | +which is why it is graded `patch`. `@objectstack/runtime` is `patch` for the |
| 55 | +dispatcher error vocabulary's `file:` anchors, repointed at the new path. |
| 56 | + |
| 57 | +**Single-package artifacts are byte-for-byte unaffected (D7)**, measured rather |
| 58 | +than asserted: the whole `manager.register` sequence for a single-`manifest` |
| 59 | +artifact — every call, in order, with the id and version each item was stamped |
| 60 | +with — is pinned as a literal in |
| 61 | +`packages/metadata/src/plugin-artifact-packages-attribution.test.ts` and was |
| 62 | +recorded identically on both legs of the ablation. A real boot of |
| 63 | +`examples/app-todo` answers every door identically before and after. |
| 64 | + |
| 65 | +**Nothing a booted instance can see today disappears.** Every live |
| 66 | +`ARTIFACT_FIELD_TO_TYPE` key is a member of `AssembledPackageBodySchema` |
| 67 | +(measured, not assumed), so iterating bodies loses no collection; and because |
| 68 | +`packages` composes by `concat`, an artifact whose top level carries a |
| 69 | +definition no package body repeats keeps it — registered once, attributed to the |
| 70 | +artifact's own identity, and logged, because it means the artifact's two halves |
| 71 | +disagree about what it ships. |
| 72 | + |
| 73 | +⛔ The **producer** half is untouched: `composeStacks` and `os build` keep |
| 74 | +emitting the flattened top level alongside `packages[]`. Whether they should is |
| 75 | +#14512's decision, not this door's. |
0 commit comments