diff --git a/.changeset/29393-overlay-write-entry-both-addresses.md b/.changeset/29393-overlay-write-entry-both-addresses.md
new file mode 100644
index 00000000000..cf08f5e7105
--- /dev/null
+++ b/.changeset/29393-overlay-write-entry-both-addresses.md
@@ -0,0 +1,11 @@
+---
+'@objectstack/spec': patch
+---
+
+fix(spec): the `packageless-overlay-write-closed` upgrade entry names both closed overlay write addresses, the write naming no package and the write naming the read-only package itself
+
+Clause-②: no (migration entry prose; no accept set moves)
+
+Metadata-protocol: none
+
+The ADR-0087 semantic entry `packageless-overlay-write-closed`, unreleased and shipping in this release, named only the write naming no package in its `surface` and `reason`. The same release also refuses a save, publish or rollback that names the installed read-only package itself (`?package=`) onto a view, dashboard, report, translation or email template it ships, with the same fix the entry's `replacement` already prescribes: write the customization into the base's paired `.custom`, declaring `overrides`. The entry's `surface` now names both addresses, and the stored rows either one left (package-less, or bound to the read-only package); its `reason` says what each address stored, and which writes naming the package stay open (a delete, and every write the pair could not take). Its `replacement` now says that `os migrate packageless-overlays` moves a row bound to the read-only package too, and gives such a row its own `foreign_declaration` remedy, the one the command prints: delete it naming the package, then save its body into the pair, declaring `overrides` (a save naming the package, which would remove the key in place, is closed). The generated protocol upgrade guide shows both in one entry. Text only: no schema, export or runtime behaviour changes.
diff --git a/packages/spec/src/migrations/entries/semantic/18.packageless-overlay-write-closed.ts b/packages/spec/src/migrations/entries/semantic/18.packageless-overlay-write-closed.ts
index 15262eb0186..f33a735b4c6 100644
--- a/packages/spec/src/migrations/entries/semantic/18.packageless-overlay-write-closed.ts
+++ b/packages/spec/src/migrations/entries/semantic/18.packageless-overlay-write-closed.ts
@@ -2,30 +2,43 @@
import type { SemanticMigration } from '../../types.js';
-// ADR-0146 D11 — a runtime narrowing at the metadata write doors with no
-// authorable key: a write naming no package onto an item an installed package
-// ships is refused where `os migrate packageless-overlays` can move its row
-// as a declared override (a row it moves as the pair's own member stays open).
-// Registered because an upgrading deployment can carry the package-less Setup
-// rows those writes made, and the operator step that moves them into the
-// base's `.custom` pair ships in this same release, so it runs AFTER the
-// upgrade (the release-timing ruling on the card).
+// ADR-0146 D1, D11 — a runtime narrowing at the metadata write doors with no
+// authorable key, at the two addresses that bypassed a read-only package's
+// paired `.custom`: a write naming no package onto an item an installed
+// package ships is refused where `os migrate packageless-overlays` can move
+// its row as a declared override (a row it moves as the pair's own member
+// stays open), and a save, publish or rollback naming the read-only package
+// itself onto an item it ships is refused where the pair can take the
+// declared override (a delete naming the package stays open).
+// Registered because an upgrading deployment can carry the rows those writes
+// made, package-less or bound to the read-only package itself, and the
+// operator step that moves both kinds into the base's `.custom` pair ships in
+// this same release, so it runs AFTER the upgrade (the release-timing ruling
+// on the card).
export const entry: SemanticMigration = {
id: 'packageless-overlay-write-closed',
surface:
- 'a metadata write that names no package (PUT, DELETE, publish or rollback under '
- + '/api/v1/meta// with no ?package=, and the metadata item page, in Studio\'s All '
- + 'metadata area where Setup\'s metadata URLs now land, when its URL holds no ?package=) onto a '
- + 'view, dashboard, report, translation or email template that one installed read-only package '
- + 'ships, where `os migrate packageless-overlays` would move the row into that package\'s '
- + 'paired `.custom` as a declared override (a row it moves as the pair\'s own member, an '
- + 'email template locale the base does not ship, keeps the write); and the package-less '
- + 'sys_metadata rows Setup\'s form stored that way',
+ 'a metadata write onto a view, dashboard, report, translation or email template that an '
+ + 'installed read-only package ships, at either of the two addresses that bypassed that '
+ + 'package\'s paired `.custom`: (1) naming no package (PUT, DELETE, publish or rollback '
+ + 'under /api/v1/meta// with no ?package=, and the metadata item page, in Studio\'s '
+ + 'All metadata area where Setup\'s metadata URLs now land, when its URL holds no ?package=), '
+ + 'where one installed read-only package ships the item and `os migrate packageless-overlays` '
+ + 'would move the row into the pair as a declared override (a row it moves as the pair\'s own '
+ + 'member, an email template locale the base does not ship, keeps the write); (2) naming the '
+ + 'read-only package itself (a save in draft or publish mode or a per-item publish under '
+ + '/api/v1/meta//?package=, POST /api/v1/packages//publish-drafts '
+ + 'holding such a draft, and a rollback of a row bound to the package), where the pair can be '
+ + 'ensured, the base records a version and the override write gate admits the declared body '
+ + 'into the pair (a delete naming the package keeps the write); and the sys_metadata rows '
+ + 'those writes stored, package-less or bound to the read-only package itself',
replacement:
'after upgrading, run `os migrate packageless-overlays` (preview) on each deployment with '
+ 'Setup customizations, with the flags and environment the deployment boots with (`--preset` '
+ 'and `--dev` mean what they mean to `os serve`), then `os migrate packageless-overlays '
- + '--apply`: it moves each `movable` row into the base\'s paired `.custom`, declaring '
+ + '--apply`: it moves each `movable` row (package-less, or bound to the read-only package '
+ + 'itself, which the preview names "bound to" that package and `--json` marks `boundToBase`) '
+ + 'into the base\'s paired `.custom`, declaring '
+ '`overrides: { package: , basedOn: }`, except a row of an email '
+ 'template locale the base does not ship (the preview lists it as the pair\'s own member; '
+ '`--json` carries it as `addedMember`), which it moves as the pair\'s own member at that '
@@ -35,32 +48,45 @@ export const entry: SemanticMigration = {
+ 'in the body (an added member\'s with none), and `DELETE /api/v1/meta//?package='
+ '.custom` to restore the package original (an added member\'s, with '
+ '`&member=`, removes it). An added '
- + 'member\'s package-less write stays open, before and after its move. Every row the preview '
- + 'lists and does not move keeps its package-less write: `multi_shipped` (several packages ship '
- + 'the name): delete it naming no package, then save it into the pair of the package it '
- + 'customizes if it is still wanted (the pair\'s override is then served at that base\'s '
- + 'address, `?package=`); `no_pair` (the base\'s id forms no pair): nothing to do; '
+ + 'member\'s package-less write stays open, before and after its move. Every package-less row '
+ + 'the preview lists and does not move keeps its package-less write, and every bound row it '
+ + 'lists and does not move keeps its delete naming the package: `multi_shipped` (several '
+ + 'packages ship the name): delete it naming no package, then save it into the pair of the '
+ + 'package it customizes if it is still wanted (the pair\'s override is then served at that '
+ + 'base\'s address, `?package=`); `no_pair` (the base\'s id forms no pair): nothing to do; '
+ '`foreign_declaration` (its `overrides` names another package): remove the key with a save '
- + 'naming no package, then re-run: the move declares the right override; `shadowed` (the pair '
+ + 'naming no package, then re-run: the move declares the right override (a row bound to the '
+ + 'read-only package cannot take that save, as a save naming the package is closed: delete it '
+ + 'naming the package, then save its body into the pair, declaring `overrides: { package: '
+ + ', basedOn }`); `shadowed` (the pair '
+ 'already holds the item; the pair\'s row is served when it declares the override, otherwise '
+ 'this row still is): nothing to do to keep the pair\'s declared override, or restore the '
+ 'package original (the DELETE above) and re-run to move this one; a `failed` move: fix what '
+ 'the error names and re-run; when the error names the pair (it cannot be ensured, or it '
+ 'refuses the row) or, for a declared override, the base\'s missing version, the row may also '
- + 'be left, as it keeps its package-less write',
+ + 'be left, as it keeps its package-less write (a bound row, its delete naming the package, '
+ + 'and the save naming it where the pair could not take the override)',
reason:
'A customization of an installed package lives in its paired `.custom` package, where it is '
+ 'a declared override that is exported with the pair and judged against the base\'s version '
- + '(ADR-0146 D1, D2, D11). The package-less write made a row of no package instead, which no '
- + 'export carries. Rows already stored that way are still served unchanged, so nothing breaks '
- + 'at the upgrade. Between the upgrade and `--apply`, every customization is served, and the '
+ + '(ADR-0146 D1, D2, D11). Two writes bypassed the pair: the package-less write made a row of '
+ + 'no package, which no export carries, and the write naming the read-only package itself made '
+ + 'a row bound to that package, declaring nothing, served in place of the package\'s own '
+ + 'document. Rows already stored either way are still served unchanged, so nothing breaks at '
+ + 'the upgrade. Between the upgrade and `--apply`, every customization is served, and the '
+ 'package-less save, publish, delete or rollback of a movable one the command moves as a '
+ 'declared override is refused 403 NOT_OVERRIDABLE naming the pair and this command; an '
+ 'email template locale the base does not ship, which the command moves as the pair\'s own '
+ 'member, keeps its package-less write. The close reaches only what the command moves as a '
+ 'declared override (its own classification and its own move steps), so no customization is '
- + 'left without a write door. No conversion applies: the rows are stored data, not authored '
- + 'source, and the move goes through the save door. ADR-0146, ADR-0087.',
+ + 'left without a write door. The save, publish or rollback naming the read-only package '
+ + 'itself is refused 403 NOT_OVERRIDABLE naming the pair and the `overrides` declaration '
+ + 'wherever the pair can take the declared override (it can be ensured, the base records a '
+ + 'version and the override write gate admits the declared body); a delete naming the package '
+ + 'stays open, as the way to remove such a row, and so does every write the pair could not '
+ + 'take, an email template locale the base does not ship among them. No conversion applies: '
+ + 'the rows are stored data, not authored source, and the move goes through the save door. '
+ + 'ADR-0146, ADR-0087.',
acceptanceCriteria:
'On the deployment\'s database and configuration, with its boot flags and environment, the '
+ 'preview of `os migrate packageless-overlays` lists no movable row. Each customization is then '