Skip to content
13 changes: 7 additions & 6 deletions .changeset/32791-stored-row-one-holder-bare-action-read.md
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
---
'@objectstack/metadata': patch
---

fix(metadata): a stored package-less row the boot pass files under the host's package is one holder of an action name, not two, when a read naming no package is judged over the loaders (ADR-0150 D4)
fix(metadata): on a host that declares its package, a stored package-less row is one holder of an action name, not two, when a read naming no package is judged over the loaders (ADR-0150 D4)

Clause-②: no — the fix removes a refusal ADR-0150 D4 already denies ("A package-less row that bears a package-held name is a customization, never a second item. … it never arms beside the item it shadows (E1) and never makes the name ambiguous."), and the refusal it removes is #32560's, which has not shipped: the refusal and its removal first ship in the same release, so no released accept set widens. On a host that declares its package, after the boot priming pass and before any write that carries no package, `MetadataManager.get('action', NAME)` and `getDiagnosed` of a name one package holds beside a package-less stored row saved last go from `409 RESOURCE_CONFLICT` naming the host's package to the package-less row, that package's customization (or the environment's own item where the package binds the name to another object); beside two packages the `409` no longer names the host's package.
Metadata-protocol: none — no key, value or metadata type an author writes changes; what moves is how many holders a read naming no package counts for one stored row.

On a host that declares its package (`MetadataPlugin`'s `packageId`), the boot priming pass stamps a package-less `sys_metadata` row with the host's package before it registers it, so the registry holds that row in the host's slot. A read naming no package of an action name is judged over the registry's slots and the loaders' stored slots together (#32560), and the loaders still store that row package-less: the one row was counted twice, once as the host's package and once as the environment, and the refusal named a holder that does not exist.
On a host that declares its package (`MetadataPlugin`'s `packageId`), the boot priming pass stamped a package-less `sys_metadata` row with the host's package before it registered it, so the registry held that row in the host's slot. A read naming no package of an action name is judged over the registry's slots and the loaders' stored slots together (#32560), and the loaders still store that row package-less: the one row was counted twice, once as the host's package and once as the environment, and the refusal named a holder that does not exist.

The judged view now files such a row (an item a stored-row loader, `datasource:`, handed out for no package, held in the registry slot of the package it is stamped with, whose registration replaced no item registered in memory or by code, while a loader still stores it) the way the store files a write that carries no package: in the slot of the name's one package holder, as its customization, or else in the bare slot, as the environment's own item. It keeps the write order of the registry slot it came from, and that slot is not counted beside it.
The boot priming pass now leaves such a row unstamped (#32812, whose note has the whole change), so the registry files it the way the store files a write that carries no package: in the slot of the name's one package holder, as its customization, or else in the bare slot, as the environment's own item, and it is counted once.

Moved, all `action`, every read naming no package (`get` and `getDiagnosed` agree); FROM is `main` with #32676:

Expand All @@ -19,12 +19,13 @@
| another package | ALPHA's row on an object, a package-less row on the same object | `409` host, ALPHA, environment | the package-less row |
| another package | ALPHA's row on an object, an object-less package-less row; or ALPHA's object-less row, a package-less row on an object | `409` host, ALPHA | the package-less row (the environment's own item) |
| another package | ALPHA's rows on two objects (either order), a package-less row on one of them; or ALPHA's object-less row and its row on an object (either order), an object-less package-less row | `409` host, ALPHA, environment | the package-less row (written last) |
| another package | a package-less row; ALPHA's item in memory, before or after the pass | `409` host, ALPHA, environment | the package-less row |
| another package | a package-less row; ALPHA's item in memory before the pass | `409` host, ALPHA, environment | the package-less row |
| another package | a package-less row; ALPHA's item in memory after the pass | `409` host, ALPHA, environment | ALPHA's item |
| another package | BETA's row and a package-less row, ALPHA's item in memory before the pass; or a package-less row, and ALPHA's and BETA's items in memory (both before the pass in either order, or ALPHA's before and BETA's after) | `409` ALPHA, host, BETA, environment | `409` ALPHA, BETA, environment |
| ALPHA | a package-less row; another package's item in memory before the pass | `409` that package, ALPHA, environment | the package-less row |

Unchanged: a host that declares no package; every two-package stored composition; the package's row saved last; every read that names a package; every other type stored per `(package, name)` (their bare read reads the registry alone); a host's own item registered in memory after the pass, which is still a holder; an unstamped row the registry filed in its one holder's slot (that holder's customization); a stamped row whose registration replaced an item the host's package held in memory before the pass, which leaves that package a holder (beside a second package's row or item the read is still refused, naming both and the environment; beside none it answers the row), and a repeated priming pass keeps the overwrite mark: the pass run again on the same manager hands out a new row object, which carries the mark of the row it replaces, so these compositions answer the same after it; and the row removed from the table behind the manager, after which the host's slot is a holder again. The candidates a refusal names are a set: their order follows the judged view and is not promised.
Unchanged: a host that declares no package; every two-package stored composition; the package's row saved last; every other type stored per `(package, name)` (their bare read reads the registry alone); a host's own item registered in memory after the pass, which is still a holder; a row the registry files in its one holder's slot (that holder's customization); a row registered over an item the host's package held in memory before the pass, which leaves that package a holder (beside a second package's row or item the read is still refused, naming both and the environment; beside none it answers the row), and these compositions answer the same after the pass is run again on the same manager. The candidates a refusal names are a set: their order follows the judged view and is not promised.

Scope: this holds in the boot state. After a write that carries no package (`register` or `registerInMemory` of the name), the registry files the written body in the host's slot and the bare read names the host's package again, as on `main` (#32812). A removal naming no package is judged without this filing and is still refused naming the host's package, so in these compositions the read and the removal do not count the same holders until #32813.
Scope: with #32812 this holds after a write that carries no package (`register` or `registerInMemory` of the name) as well as in the boot state, and a removal naming no package counts the same holders as the read. A read naming a package, and the row removed from the table behind the manager, answer as #32812's note says.

No first-party composition wires a `DatabaseLoader` under a host that declares its package, and none registers an `action` in memory, so no answer changes in this repository.
31 changes: 31 additions & 0 deletions .changeset/32812-boot-pass-leaves-stored-row-unstamped.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
---
'@objectstack/metadata': patch
---

fix(metadata)!: the boot priming pass leaves a package-less stored row of a type stored per `(package, name)` unstamped, so the host's package is never counted as a holder of its name (ADR-0150 D4)

Clause-②: yes (narrowing) — one face widens against the last release and the others narrow, all on a host that declares its package and wires a `DatabaseLoader`; the widening is named under the BREAKING banner below. Adds one method to the exported `MetadataManager`, `sourceLoaderProtocolOf(item)` (the protocol of the loader `loadMany` read an item from), which the pass asks. Narrows: on a host that declares its package (`MetadataPlugin`'s `packageId`) and wires a `DatabaseLoader` (or another loader of the `datasource:` protocol), a package-less stored row of `api`, `agent`, `skill`, `action`, `email_template`, `permission`, `flow`, `picklist`, `mapping` or `dataset` is no longer served as the host package's item after the boot priming pass: its body no longer carries `_packageId` set to the host's package or `_provenance: 'package'`, the metadata list filtered by the host's package (`?package=` naming it) no longer lists it, and `publishPackage`, `revertPackage` and `unregisterPackage` naming the host's package no longer take it as one of that package's items (where the host's package itself holds the name, the row is that package's customization, ADR-0150 D4, and goes with its slot as for any holder). The `409` this lifts (the bare read naming the host's package, which holds nothing, after a write that carries no package) is #32560's, which has not shipped.
Metadata-protocol: none — no key, value or metadata type an author writes changes; what moves is which package the boot pass gives a stored row that names none.

<!-- adr-0087: not-required (no-migration-prescription) No key of any schema is added, removed, renamed or re-shaped, and no export or type of the package root changes. What moves is the package the boot priming pass stamps on a stored row that names none, at every boot; nothing at rest is rewritten (the pass writes no row back for these rows; the column bind of a legacy row whose body carries a package is unchanged), so there is nothing for objectstack migrate meta to rewrite and no tombstone. -->

**BREAKING** for a host that declares its package and stores rows that name no package for one of these ten types. Such a row is the environment's own item, or the customization of the one package that holds its name (ADR-0150 D4); it was never the host package's, but the boot pass stamped it so. To keep an item in the host's package, save it naming that package (`?package=` on the write, or the package stamped on the item `register` is given). A row whose stored body an earlier release's boot pass wrote the host's stamp into is still read as the host package's (its column is bound from that body), and is not moved. Every other type, and the host's own file items, are stamped as before.

The one widening, a behaviour change for the same reason (ADR-0150 D4, and the answer a host that declares no package already gives): a package-less stored row of these types is no longer hidden when the host's own package is disabled, as on a host that declares no package.

Among the readers of the stamp, these move with it (the list is not complete; every one of them reads the row as a host that declares no package does): such a row with a `protection` block is locked from `'artifact'` rather than `'package'` (`_lock` kept), an MCP skill or agent face built from it is named by its own name rather than the host's namespace, and the realtime events of the boot pass carry no package for it. A loader of stored rows is told by its `datasource:` protocol, as #32791's filing told it, so a loader that declares the protocol under a name other than `DatabaseLoader`'s is treated the same. No first-party package, app or example declares a package and wires a `DatabaseLoader`.

Moved answers, all on such a host, each now the answer the same composition gives on a host that declares no package:

- One package's row and a package-less row saved last (`action`): after `register` with no package, or `registerInMemory` of a package-less item, the bare `get` and `getDiagnosed` go from `409 RESOURCE_CONFLICT` naming the host's package, the holder and the environment to the written item.
- The removal naming no package, the same composition: right after the pass and after such a write, it goes from a `409` naming the host's package to removing the package-less row, for every one of the ten types (`email_template`: the holder's slot), so it names the holders the bare read names.
- The same composition, the nine unbundled types: the registry holds the package-less row in the name's bare slot, not the host's, and a read naming the holder answers its customization (the package-less row) where it answered the holder's own row. `list` names the row once, package-less, where it named it twice (its customization and, again, the host's), so a consumer that counts the holders `list` names counts one holder where it counted two.
- A read naming a package that holds nothing (the host's own, on a host declaring the holder's package; any third package, on a host declaring another): it answers the package-less row (or the item written over it), as the bare read does, where it answered nothing. The other way, the read naming the host's package goes from the package-less row to nothing where a package's item registered in memory sits beside that row, and for `email_template` beside one package's row: the host's package holds nothing there.
- A package-less row and one package's `action` item registered in memory after the pass: the bare read answers that package's item, where it answered the package-less row (the by-host difference #32752 tracked).
- The package-less row deleted from the table behind the manager: the bare read answers the registry's copy, where it was refused naming the host's package and the holder.
- `email_template`, one package's row and a package-less row: the pass files the package-less row in the holder's slot (its customization) and no longer in the host's, so the bare read after the pass is no longer refused naming the host's package.
- A package-less `action` row alone, removed with no package: the registry's stamped copy no longer outlives the row (it answered reads and `list` until a restart).

This replaces #32791's read-time filing of the stamped row (`MetadataManager`'s record of stored package-less rows, its overwrite mark and the judged view's filing), which has nothing left to file for the boot pass and is removed; every #32791 pin marked CONTROL passes unchanged. #32791's own changeset, which ships in the same release, is corrected with this change: the filing it described is replaced by the unstamped row, a read naming a package is no longer among its unchanged answers (above), its "after the pass" cell (a package-less row, a package's item registered in memory after the pass) answers that package's item, and its scope now covers a write that carries no package and the removal. One caller the filing also covered moves back: a caller of the public `MetadataManager.loadMany` that stamps a package-less row it is handed with a package itself and registers it is now refused naming that package beside the name's holder, as after any write (no first-party caller does so). Candidate order in a refusal is not promised.

Skills-docs: none — `skills/**` and `content/docs/**` do not teach that the boot pass gives a stored row the host's package; the migration step ships in this note and is carried into the release's upgrade notes when they are written.
2 changes: 1 addition & 1 deletion .changeset/32986-host-unregister-keeps-environment-rows.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
---
'@objectstack/metadata': patch
---
Expand All @@ -7,7 +7,7 @@
Clause-②: no — no released answer moves: `unregisterPackage` reads the `_packageId` stamp since #32198, which has not shipped, and with this change it leaves the environment's own rows the boot pass marked, as the released version does. The boot pass's stamp, and every reader of it, answer as before.
Metadata-protocol: none

On a host that declares its package and stores metadata in a database, the boot pass stamps each stored row that names no package with the host's package. For a type not stored per `(package, name)` — `object`, `view`, `app`, `hook`, `dashboard` and the rest — that stamp stays, so ObjectQL still registers such an object as the host package's contribution. But `MetadataManager.unregisterPackage(host)` took the stamp as membership and deleted those rows, from memory and from the database: the environment's own items were lost when the host package was unregistered.
On a host that declares its package and stores metadata in a database, the boot pass stamps each stored row of a type not stored per `(package, name)` — `object`, `view`, `app`, `hook`, `dashboard` and the rest — that names no package with the host's package (a row of a type stored per `(package, name)` is left unstamped, #32812). That stamp stays, so ObjectQL still registers such an object as the host package's contribution. But `MetadataManager.unregisterPackage(host)` took the stamp as membership and deleted those rows, from memory and from the database: the environment's own items were lost when the host package was unregistered.

`unregisterPackage` now leaves an item that a stored-row loader handed on naming no package and that names the package only by the stamp. The host's own items (a row whose stored body carries the host's stamp or names it under the legacy `package` key, an item registered for the host in memory or from files) are removed as before. Types stored per `(package, name)` are unchanged here.

Expand Down
Loading
Loading