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
31 changes: 31 additions & 0 deletions .changeset/23630-promote-doors-d99.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
---
'@objectstack/metadata-protocol': minor
---

fix(metadata-protocol)!: a draft of an object promoted into a second package after another package's runtime-authored row came to own the object is refused before any write, as its save is (ADR-0029 D9.9)

Clause-②: no (narrowing)

<!-- adr-0087: not-required (no-migration-prescription) No metadata moves: no spec key, authorable spelling, object definition or stored shape is added, removed, renamed or re-shaped, no stored row is rewritten, converted or deleted by this change, and no exported declaration is added, removed or narrowed, so there is nothing for `objectstack migrate meta` to rewrite. What narrows is the accept set of the two draft promotion doors for one shape: promoting an `object` draft into a package other than the one whose runtime-authored row now owns the object is refused, where it was written and then discarded by the registry. The remedy is in the request a caller sends (re-save the draft bound to the owner's package, discard it, or give the object its own name), never a rewrite of a package source or a stored row; the draft stays stored. The other categories are closed on facts: the package publishes (not unpublished); no ADR-0087 id is named or touched (not registered or already-registered); and nothing exported is removed or narrowed (not runtime-interface-only, type-surface-only or runtime-export-retired). -->

**BREAKING** (an accept-set narrowing), shipped as `minor` under the launch-window convention for breaking changes.

An object has exactly one registry entry, so it has exactly one owning package (ADR-0029 D3), and `sys_metadata` must not describe a shape the registry cannot hold (D9.9). The save door refuses an `object` row bound to a package that does not own the object. A draft save is judged against the owner of its own moment, so package `Q`'s draft of an object that no row held yet was stored, correctly. When package `P`'s runtime-authored row of that object was saved after it, `P` owned the object. Promoting `Q`'s draft then wrote `Q`'s active row beside `P`'s and answered success, while the registry refused it into a warning and kept serving `P`'s schema. An uninstall of `P` then refused `409 RESOURCE_CONFLICT` until a row was removed by hand.

Both promotion doors now ask the save door's question of the row the promotion writes, and refuse it before any write, with the save door's code and prescription.

What changes for you, where package `P`'s runtime-authored row owns object `NAME`, `Q` is any other package, and `Q`'s draft of `NAME` was saved before `P`'s row existed:

- FROM `POST /api/v1/meta/object/NAME/publish?package=Q` (`publishMetaItem({ type: 'object', name: NAME, packageId: Q })`) answering `200` "Published draft …", with a `Q`-bound active row stored while the registry kept `P`'s schema, TO `422 OBJECT_OVERLAY_PACKAGE_MISMATCH` with nothing written and `Q`'s draft still stored: "Cannot layer object 'NAME': the overlay is bound to package 'Q', but the object is owned by package 'P'. …", the save door's text for the same row.
- FROM `POST /api/v1/packages/Q/publish-drafts` (`publishPackageDrafts({ packageId: Q })`) answering `200` with `outcome: 'published'` and `failed: []`, TO `200` with `success: false`, `outcome: 'refused'` and `publishedCount: 0`. `NAME` is in `failed[]` with `code: 'OBJECT_OVERLAY_PACKAGE_MISMATCH'` and that text. Every other draft of `Q` is in `failed[]` with `code: 'BATCH_ABORTED'`. Nothing is promoted (the batch is all-or-nothing, ADR-0067 D2).

**Remedy, in one line.** Re-save the draft bound to the owner (`PUT /api/v1/meta/object/NAME?package=P&mode=draft`) and publish it there, or give `Q`'s object its own name, and discard `Q`'s draft with `DELETE /api/v1/meta/object/NAME?package=Q&state=draft`.

**Unchanged.**

- A promotion of `Q`'s draft of an object no other package's row holds, and the owner `P`'s promotion of its own draft.
- A publish naming no package that promotes a package-unbound draft into the owner's package.
- A code-packaged owner. An object a code package ships is sealed, so a promotion of another package's draft of it still answers `403 NOT_OVERRIDABLE` (the batch door: that code in `failed[]`), as the save door does, and never `OBJECT_OVERLAY_PACKAGE_MISMATCH`.
- A package-less owner (the `sys_metadata` sentinel). Its rule is decided separately.
- Every type except `object`. Two packages may still hold one name.
- Rows stored before ADR-0029 D9.9 stay as they are. Nothing is rewritten on upgrade.
36 changes: 36 additions & 0 deletions packages/metadata-protocol/src/protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24474,6 +24474,42 @@ export class ObjectStackProtocolImplementation implements
{ type: singularType, name: request.name, org: 'env' } as Parameters<typeof repo.get>[0],
{ state: 'draft', packageId: draftKey, ...memberOption(request.member) },
);
// [ADR-0029 D9.9, #23630] …and the save door's representability
// refusal, asked of the row this promotion writes: an `object` row
// bound to a package that does not own the object. A draft save is
// judged against the owner of its own moment, so package `Q`'s draft
// of an object no row held yet was stored, correctly; when package
// `P`'s runtime-authored row of that object was saved after it, `P`
// owned the object, and promoting `Q`'s draft wrote `Q`'s active row
// beside `P`'s and answered success, while the registry write-through
// refused it into a `console.warn` and kept serving `P` — the
// disagreement {@link overlayPackageMismatchError} exists to prevent.
//
// Judged on `promoteInto`, the package the active row is written
// under (the stated binding, or for an unbound draft adopted into the
// item's package, that package), exactly as the save door judges the
// address it resolved; with the save door's judge and envelope, and
// before `repo.promoteDraft` writes, so a batch publish aborts on it
// as on the refusals below (ADR-0067 D2). Both doors reach it: the
// per-item publish and `publishPackageDrafts` promote through here.
//
// ⛔ Not asked of SEALED managed content ({@link isSealedManagedItem}):
// an object a code package ships. The save door answers that row's
// package door first ({@link refusePackagedBaseOverride},
// `NOT_OVERRIDABLE` / 403, ahead of its D9.9 call), and the promotion
// reaches the same refusal at the store (`repo.promoteDraft` →
// `assertAllowed`). Asking `classifyObjectContribution` there would
// answer its packaged `mismatch` arm instead — a second dialect for
// one row, whose prescription ("bind to the owner") the sealed rule
// refuses. One row, one refusal: the save door's.
if (draftForGate && singularType === 'object' && !this.isSealedManagedItem(singularType, request.name)) {
const layer = this.classifyObjectContribution(request.name, promoteInto);
if (layer.kind === 'mismatch') {
throw ObjectStackProtocolImplementation.overlayPackageMismatchError(
request.name, layer.packageId, layer.ownerPackageId,
);
}
}
// [#21470] …and the divergent `name` refusal, on the same body and for
// the same reason: a draft stored before `saveMetaItem` judged every
// type can carry a `name` that is not its row's, and promoting it
Expand Down
Loading
Loading