Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
131 commits
Select commit Hold shift + click to select a range
8de88ee
feat(objectql, plugin-email): email template names held per namespace…
claude Nov 10, 2026
cef0ef9
test(objectql, plugin-email): pin the email template narrowing, the p…
claude Nov 10, 2026
c88476f
test(dogfood): the flow-family boot pins read the email template's na…
claude Nov 10, 2026
4312637
fix(metadata-protocol): the exact qualifier reads a bundle addition a…
claude Nov 10, 2026
8bbcc56
test(core, cli): the resolver and the stored-references report read t…
claude Nov 10, 2026
e37cebe
docs(changeset): a code package's bundle addition is still judged aga…
claude Nov 10, 2026
f1189c7
test(plugin-email): the platform-requested list pin loads core at mod…
claude Nov 10, 2026
3ccfb7a
Merge remote-tracking branch 'origin/main' into claude/project-thread…
claude Nov 10, 2026
3465aaa
fix(metadata-protocol): the exact qualifier keeps a pair's item on it…
claude Nov 10, 2026
a52f89e
docs(changeset): the email template change declares the ?package= nar…
claude Nov 10, 2026
a4326c9
Merge remote-tracking branch 'origin/main' into claude/project-thread…
claude Nov 10, 2026
50e1ef9
fix(plugin-email, service-automation, service-messaging): an email te…
claude Nov 10, 2026
a4ca293
fix(metadata-protocol, cli): a pair member row two holders could own …
claude Nov 10, 2026
1484ab5
test(plugin-email): read the last protocol request without Array.prot…
claude Nov 10, 2026
1fabc5d
feat(metadata): the in-memory store keeps one slot per (package, name…
claude Nov 10, 2026
f6bbafc
Merge remote-tracking branch 'origin/main' into claude/project-thread…
claude Nov 10, 2026
4c176c5
Merge remote-tracking branch 'origin/main' into claude/project-thread…
claude Nov 10, 2026
249a6a4
fix(metadata-protocol, cli)!: a package-less row of a name two packag…
claude Nov 10, 2026
9102aca
Merge remote-tracking branch 'origin/main' into claude/project-thread…
claude Nov 10, 2026
62606b5
fix(objectql): by-name doors judge a name's holders over the served s…
claude Nov 10, 2026
318f158
test: pin the served holder set at every by-name reader (#32183)
claude Nov 10, 2026
130c8fc
chore(changeset): declare the served-holder narrowing (#32183)
claude Nov 10, 2026
be1f3c3
docs(changeset): spell the before-after labels so the ADR-0087 gate r…
claude Nov 10, 2026
dc50b88
fix(metadata-protocol): a name only disabled packages hold reads as t…
claude Nov 10, 2026
3644f12
docs(changeset): name metadata-protocol and the kept layered and publ…
claude Nov 10, 2026
340f7cb
test(runtime): pay @objectstack/metadata's first load at collection i…
claude Nov 10, 2026
b41515d
Merge origin/main into the #32183 branch
claude Nov 10, 2026
14bbef3
fix(service-automation, metadata-protocol, objectql, cli): contract r…
claude Nov 10, 2026
376b80e
Merge remote-tracking branch 'origin/main' into claude/project-thread…
claude Nov 10, 2026
432ae53
fix(metadata-protocol): bare reads of a name a disabled package also …
claude Nov 10, 2026
240ff90
fix(service-automation, plugin-approvals): the R7–R11 backfills ask e…
claude Nov 10, 2026
44c2d17
test, fix: both registration orders in every two-holder pin, the serv…
claude Nov 10, 2026
7e8bf7a
test(runtime): the bare action door with one of two holders disabled,…
claude Nov 10, 2026
1559964
docs(changeset): declare the fix round's moves
claude Nov 10, 2026
ecb64cc
Merge origin/main into the branch
claude Nov 10, 2026
9ea252e
Merge remote-tracking branch 'origin/main' into claude/project-thread…
claude Nov 10, 2026
1b655c3
fix(metadata-protocol, rest, runtime, metadata): bare /layers and /pu…
claude Nov 10, 2026
c79a4e6
test(metadata-protocol, rest, runtime): the getItem-reading doubles r…
claude Nov 10, 2026
c5d0e37
Merge remote-tracking branch 'origin/main' into claude/project-thread…
claude Nov 10, 2026
d808486
fix(cli): the stored-references hint names no tracker id in its print…
claude Nov 11, 2026
ee4bcab
Merge remote-tracking branch 'origin/main' into claude/project-thread…
claude Nov 11, 2026
7136e33
fix(metadata-protocol): a row hydrated into the registry with no pack…
claude Nov 11, 2026
4668d9e
Merge origin/main into claude/project-thread-3giljf
claude Nov 11, 2026
460df4a
test(objectql): the package-less row is written as the store writes i…
claude Nov 11, 2026
248d471
fix(metadata): a bundled type's unscoped loader reads keep every memb…
claude Nov 11, 2026
26e2548
Merge origin/main into claude/project-thread-3giljf
claude Nov 11, 2026
61dfabf
docs(changeset): declare the loader half — rollback and delete by nam…
claude Nov 11, 2026
f9f589d
fix(metadata): MetadataManager's plural reads keep a bundled type's m…
claude Nov 11, 2026
f779531
fix(metadata): a bundled type's save writes its own member's row, and…
claude Nov 11, 2026
12ee9e5
docs(changeset): the manager's plural reads per member, and the membe…
claude Nov 11, 2026
cfb8f30
Merge origin/main into claude/project-thread-3giljf
claude Nov 11, 2026
0f93f8f
fix(metadata): the boot priming pass never writes an item back into t…
claude Nov 11, 2026
74a7e4c
Merge origin/main into claude/project-thread-3giljf
claude Nov 11, 2026
e8c7447
docs(metadata): MetadataUnregisterOptions keeps its own doc comment (…
claude Nov 11, 2026
fefaa51
test(metadata): the R5-A control seeds the database loader from a rea…
claude Nov 11, 2026
2990c4c
docs(metadata): the boot write-back note is its own changeset line; o…
claude Nov 11, 2026
74d2e76
Merge origin/main into claude/project-thread-3giljf
claude Nov 11, 2026
7594637
test(metadata): database-loader.test.ts pays the metadata-fs load at …
claude Nov 11, 2026
6ed3bea
docs(metadata): keep database-loader.ts out of round 7's diff
claude Nov 11, 2026
17cfbf8
fix(metadata): the boot pass binds a legacy row's empty package colum…
claude Nov 11, 2026
7056f70
Merge origin/main into the branch
claude Nov 12, 2026
9295100
test: the merged doors judge the served holders — the dataset caller …
claude Nov 12, 2026
16de44c
test(service-automation): the credential save door's stamp follows th…
claude Nov 12, 2026
67304b7
docs(changeset): declare the moves of the doors main brought in
claude Nov 12, 2026
1483899
test: the served-holder loops reach `permission`'s two-holder state
claude Nov 12, 2026
27abd45
Merge branch 'main' into claude/project-thread-3giljf
objectstack-fleet[bot] Nov 12, 2026
9aa8377
Merge origin/claude/project-thread-3giljf into the branch
claude Nov 12, 2026
1e6c958
fix(metadata-protocol): an exact address of a disabled package, or of…
claude Nov 12, 2026
77fbca5
fix(cli): the stored-reference report reads every holder of a pair me…
claude Nov 12, 2026
09c4d69
fix(metadata-protocol): an App's pair is read at its enabled module, …
claude Nov 12, 2026
73a7077
docs(changeset): declare the exact-qualifier moves
claude Nov 12, 2026
413de17
test(cli): read a report candidate's package through the holder union
claude Nov 12, 2026
c426364
Merge origin/main into the branch
claude Nov 12, 2026
5941621
fix(plugin-email): a send made from a disabled holder reads its own a…
claude Nov 12, 2026
aca8bb9
test(objectql): a disabled holder beside a package-less row, and a pa…
claude Nov 12, 2026
6655b74
docs(changeset): declare the send door's moves and the package-less r…
claude Nov 12, 2026
bb96704
test(runtime): stored rows judged over the served set, red on the #32…
claude Nov 12, 2026
4730a00
test(rest): stored rows judged over the served set, red on the #32183…
claude Nov 12, 2026
bb37073
fix(metadata-protocol): a pair address reads the package it names, ne…
claude Nov 12, 2026
db9bb73
fix(metadata-protocol): a name a disabled package holds through a sto…
claude Nov 12, 2026
e62c368
fix(metadata-protocol): gate the stored-row judgment on an installed …
claude Nov 12, 2026
fcfe167
test(rest): /published of a stored-rows-only name serves the enabled …
claude Nov 12, 2026
e2ae0d1
docs(changeset): declare the stored-row served-set moves (#32354)
claude Nov 12, 2026
aac6a6d
test(runtime): a stored row at an exact address wears the envelope of…
claude Nov 12, 2026
ab58f3f
docs(changeset): declare the pair address's moves (#32501)
claude Nov 12, 2026
a70fe28
docs(changeset): name the App pair's widening (#32501)
claude Nov 12, 2026
2533689
test: point the /layers sentence-only refusal pins at #32622 (#32354)
claude Nov 12, 2026
1b42005
test: record the exact-address envelope site; a row of a package that…
claude Nov 12, 2026
9c60c83
docs(changeset): the disabled shipper's widening (#32501)
claude Nov 12, 2026
c5eaecd
docs(changeset): Metadata-protocol none — the moves are read answers,…
claude Nov 12, 2026
d36e7da
fix(plugin-email): the send door counts every holder, a disabled pack…
claude Nov 12, 2026
a4b57cf
fix(metadata-protocol): an exact address of a name only disabled pack…
claude Nov 12, 2026
292dce5
docs(changeset): the send door is not moved; declare the exact-addres…
claude Nov 12, 2026
bb1ab33
Merge the blocker head 292dce52 (#32183) into #32501
claude Nov 12, 2026
88d3dd1
Merge blocker head 292dce52 (claude/project-thread-phk4r9) into #32354
claude Nov 12, 2026
cb73bf6
wip: carry #32354 local work (88d3dd12 patch); dropped when its branc…
claude Nov 12, 2026
ef9fceb
test(metadata-protocol): two enabled row holders of a name no package…
claude Nov 12, 2026
32584dc
fix(metadata-protocol): a name two enabled packages hold only as stor…
claude Nov 12, 2026
5692474
docs(changeset): declare #32607's moved answers
claude Nov 12, 2026
639c095
fix(metadata-protocol): only the item face refuses two enabled row ho…
claude Nov 12, 2026
e9e2227
test(objectql): flip #32183's pair-address arm to the row now that #3…
claude Nov 12, 2026
dd73f06
fix(rest): the layered chain's owner step leaves two row holders' ref…
claude Nov 12, 2026
8ec8d78
docs(changeset): declare the all-disabled member answers and narrow t…
claude Nov 12, 2026
15f3656
docs(changeset): the REST layered chain's owner step, and @objectstac…
claude Nov 12, 2026
4429ad3
Merge remote-tracking branch 'origin/claude/project-thread-phk4r9' in…
claude Nov 12, 2026
fd41963
Merge blocker head 8ec8d78e (claude/project-thread-phk4r9) into #32354
claude Nov 12, 2026
f073c31
Merge claude/project-thread-phk4r9 (8ec8d78e, changeset-only) into cl…
claude Nov 12, 2026
061fd07
Merge origin/main at 5f84ef0ec into the #32183 branch (after #32513)
claude Nov 12, 2026
0975e76
fix(objectql): the picklist arrival judge reads every holder, as the …
claude Nov 12, 2026
e6cf10f
fix(runtime, service-automation): the flow doors of a name two packag…
claude Nov 12, 2026
ae55db3
test(objectql, cli): the package-less row of a narrowed type is judge…
claude Nov 12, 2026
4e32f00
docs(changeset): declare the landing duties (arrival judge, flow door…
claude Nov 12, 2026
e6473e4
fix(core): a permission set is judged over every holder, a disabled p…
claude Nov 12, 2026
6052c6c
fix(service-automation): the dispatch key's coordinates read every ho…
claude Nov 12, 2026
018eb5f
docs(changeset): the permission-set grant reads every holder (the fix…
claude Nov 12, 2026
fa4547c
test(runtime): the flow-door parity pin types its reads as the doors …
claude Nov 12, 2026
d864492
Merge origin/main at f7a0fe0ea into the #32183 branch
claude Nov 12, 2026
32f3800
Merge origin/main at af90f307c into the #32183 branch
claude Nov 12, 2026
649ae47
Merge origin/main at 776474e8b into the #32183 branch
claude Nov 12, 2026
28b37fb
fix(metadata-protocol): the bare action read's disabled-customization…
claude Nov 12, 2026
6e575e3
Merge the blocker's landed head 28b37fbb7 (#32183) into #32501
claude Nov 12, 2026
040e2d6
Merge origin/main at 2f80f2897 into #32501
claude Nov 12, 2026
1d8ea4a
test, docs(changeset): re-measure the pair address moves on main afte…
claude Nov 12, 2026
cd1143c
Merge #32501 head 1d8ea4ad7 (claude/project-thread-3jrz5i) into #32354
claude Nov 12, 2026
0e25e64
Merge #32354 head cd1143c19 (claude/project-thread-pv4nip) into #32607
claude Nov 12, 2026
d721d9a
fix(runtime): the dispatcher's layered chain leaves two row holders' …
claude Nov 12, 2026
3b9a447
Merge origin/main at 89d7bef033 into #32607
claude Nov 13, 2026
abd91aa
fix(metadata-protocol): a name an enabled package ships at any member…
claude Nov 13, 2026
5d3a6c7
docs(changeset): the refusal's arm requires that no package ships the…
claude Nov 13, 2026
2eb2f7c
fix(metadata-protocol): round 2 of #32607 — one read of the item's ac…
claude Nov 14, 2026
f20d249
Merge remote-tracking branch 'origin/main' into claude/project-thread…
claude Nov 14, 2026
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
27 changes: 27 additions & 0 deletions .changeset/32607-two-enabled-row-holders.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
---
'@objectstack/metadata-protocol': minor
'@objectstack/rest': patch
'@objectstack/runtime': patch
---

fix(metadata-protocol)!: a name two enabled packages hold only as stored rows is refused by the plain read, never served by save order (ADR-0150 D3, #32607)

Clause-②: yes (narrowing)
Metadata-protocol: narrowed
Skills-docs: none — no skill and no doc teaches what the read naming no package answers for a name only stored rows of several packages hold (`skills/**` and `content/docs/**` grepped: the `ambiguous-in-environment` mentions are about flow, queue and permission-set references, not `/meta` reads); the refusal's own sentence names the `?package=` remedy.

<!-- adr-0087: registered two-row-holders-bare-read-refused -->

**BREAKING** answer narrowing at the `/meta` bare item reads: a name that two or more enabled packages hold only as stored rows, and that no package ships in code, answers 409 `RESOURCE_CONFLICT` naming each of them, where the plain read and `/published` served one of the rows. `/layers` already refused it.

The row-aware served-holder judgment that a disabled holder brings in (#32354) now also judges, on the item read, a name with no disabled holder. The holders are judged at one grain, the packages the item's active stored rows are bound to at the member the read addresses, as the read itself binds them, and never united with the registry's holders of the name (with no shipper the registry knows a row-only name through one bare slot, the row written or hydrated last). Two or more of them are refused as the by-name read refuses two package holders (`ambiguous-in-environment`). One holder reads as before. It applies to a type the registry reads by package, when no enabled package ships the name in code at any member, and when no package-less row or environment item of the name is stored.

**Every moved answer, before → after.** Both transports (`RestServer` and the runtime `HttpDispatcher`), both registration orders, both save orders, and on the runtime dispatcher before and after a reboot. First-party count 0: no first-party app saves one name into two packages' stored rows.

Narrowing:

- Bare `GET /meta/:type/:name`: 200 with the row saved first → 409 `RESOURCE_CONFLICT` naming each holder, with `details.candidates`.
- Bare `GET /meta/:type/:name/published`: 200 with a row (on `RestServer` the row saved first; on the runtime dispatcher the same package in every save order) → 409 `RESOURCE_CONFLICT` naming each holder.
- The execution face (`getMetaItemForExecution`) of the same name: the row saved first → the same refusal.

**Not moved.** Bare `GET /meta/:type/:name/layers` keeps its 409 `RESOURCE_CONFLICT`, the package-less writes' own refusal naming the same holders with its `?package=` prescription (#28337), on both transports: each transport's layered chain (`RestServer`'s and the runtime `HttpDispatcher`'s) reads the plain read first to find a disabled owner, and now leaves that read's refusal of row holders, and only that refusal (the protocol marks it), to the package-less refusal step, rethrowing every other refusal as before (two code holders, with or without a stored row each, and two row holders beside a disabled one, keep the plain read's refusal). `?package=` naming either holder serves that holder's row. A name one package holds as a stored row is served as before. A row the reads decline (a shipped flow name's undeclared row, a sealed managed name's row) and a row of a customization pair of another package the rows or the registry name make no holder here, as they make none beside a disabled holder (#32354), so they never turn one holder into a refusal: a package's row beside its pair's undeclared row is one holder in either save order, and no door names the pair. A dependent's added member over a base that holds the name only as a stored row (`email_template`: the base's `en-US` row, the dependent's `fr-FR` row) is the dependent's alone at that member (#28802): the plain read, `/layers` and the execution face at `fr-FR` keep serving the dependent's row. A name an enabled package ships in code at any member (for a bundled type such as `email_template`, a base shipping `en-US` beside another package's `fr-FR` row: the package-less `fr-FR` read keeps its answer), or one with a package-less row or an environment item, is judged as before. The paired customization addresses (`?package=HOLDER.custom`), which after a reboot follow the registry's one bare slot, are unchanged here (#32662 carries them).
10 changes: 10 additions & 0 deletions .changeset/32607-two-row-holders-semantic-entry.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
'@objectstack/spec': patch
---

docs(spec): the protocol upgrade guide names the bare item read #32607 narrows: a name two or more enabled packages hold only as stored rows, which no package ships in code, is refused naming each holder instead of served by save order

Clause-②: no
Metadata-protocol: none

The semantic entry `two-row-holders-bare-read-refused` (`packages/spec/src/migrations/entries/semantic/18.two-row-holders-bare-read-refused.ts`) registers the narrowing `.changeset/32607-two-enabled-row-holders.md` declares (ADR-0087, `adr-0087: registered`). There is no automatic rewrite, because which package's row a caller meant is a judgement about that caller. The remedy it names is the exact read (`?package=`), which serves each holder's row. It adds no key and no export.
133 changes: 119 additions & 14 deletions packages/metadata-protocol/src/protocol-meta-item-face.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ import { resolveBundleMemberSelector } from './protocol-bundle-members.js';
import { installedManifestsOfEngine } from './installed-manifests.js';
import { pairsOf } from '@objectstack/metadata-core';
import { disabledExactAddressHolder } from './protocol-exact-qualifier.js';
import type { PrereadActiveRows } from './protocol-meta-item-reads.js';
import {
isActionWithheldByDisabledPackage,
isItemOfDisabledPackage,
Expand Down Expand Up @@ -131,11 +132,12 @@ export function servedHolderAddressOf(registry: any, type: string, name: string)
* was the last one written or hydrated; the rows themselves name every
* package that holds the name ({@link packagelessAddress}'s candidates).
*
* Asked only when a package is disabled, of a type the registry reads by
* package, when an enabled package ships no code item of the name (otherwise
* the shipped item is the name's, ADR-0048 §3.3, and a row of another package
* never stands in for it: {@link servedHolderAddressOf} and
* `packagelessContentPackage` judge it), and when no package-less row and no
* Asked only when a package is disabled ([#32607] or when the item face
* asks, below), of a type the registry reads by package, when an enabled
* package ships no code item of the name (otherwise the shipped item is the
* name's, ADR-0048 §3.3, and a row of another package never stands in for
* it: {@link servedHolderAddressOf} and `packagelessContentPackage` judge
* it), and when no package-less row and no
* environment item is stored (a customization of no one holder, judged as
* before). Then the holders are the registry's served holders and the
* packages the active rows are bound to, less every disabled package:
Expand All @@ -145,7 +147,17 @@ export function servedHolderAddressOf(registry: any, type: string, name: string)
* - two or more → `{ kind: 'refused' }`, the by-name read's own refusal
* (`ambiguous-in-environment`), naming only the served holders (epic ruling
* on #32354, Q2): a disabled package is never named;
* - none, or no disabled package among them → `undefined`, the read as it was.
* - none → `undefined`, the read as it was.
*
* [#32607] With no disabled package among the holders, and only when
* `options.refuseWithoutDisabled` asks (the item face, {@link
* readMetaItemFace}), two or more are refused the same way, naming each: the
* plain read served the row saved first while `/layers` refused naming both.
* That arm is {@link storedRowHoldersRefusal}, which judges the holders at
* the rows' grain alone.
* Otherwise, and for one holder, `undefined` (the read already reads there).
* The layered reads keep their own refusal for that case, the one the
* package-less writes give (#28337).
*
* [#32354 contract review S4] A row the reads decline
* ({@link ObjectStackProtocolImplementation.declinesStoredRow}: a shipped flow
Expand All @@ -170,14 +182,16 @@ export function servedHolderAddressOf(registry: any, type: string, name: string)
export async function servedRowHolderOf(
host: ServedRowHost,
type: string, name: string, member?: string,
options: { readonly refuseWithoutDisabled?: boolean } = {},
): Promise<{ kind: 'served'; packageId: string } | { kind: 'refused'; error: PackageScopedNameRefusedError } | undefined> {
const registry: any = host.engine?.registry;
if (typeof registry?.readsByPackage !== 'function' || registry.readsByPackage(type) !== true) return undefined;
if (typeof registry.packageScopedHoldersOf !== 'function' || typeof registry.isPackageDisabled !== 'function') return undefined;
const disabled = (id: string): boolean => registry.isPackageDisabled(id) === true;
// No disabled package at all: no read moves, and no row is read for it.
// The predicate `isPackageDisabled` reads, asked of each installed record.
if (typeof registry.getAllPackages === 'function'
// No disabled package at all, and no refusal asked of that case: no read
// moves, and no row is read for it. The predicate `isPackageDisabled`
// reads, asked of each installed record.
if (options.refuseWithoutDisabled !== true && typeof registry.getAllPackages === 'function'
&& !(registry.getAllPackages() as ReadonlyArray<{ enabled?: unknown; status?: unknown } | undefined>)
.some((pkg) => pkg?.enabled === false || pkg?.status === 'disabled')) {
return undefined;
Expand All @@ -199,7 +213,8 @@ export async function servedRowHolderOf(
const rows = await host.packagelessAddress(type, name, member, 'active', activeRows);
if (rows.unbound) return undefined;
const every: readonly string[] = registry.packageScopedHoldersOf(type, name, null, { includeDisabledPackages: true }) ?? [];
if (![...every, ...rows.candidates].some(disabled)) return undefined;
const anyDisabled = [...every, ...rows.candidates].some(disabled);
if (!anyDisabled) return options.refuseWithoutDisabled === true ? storedRowHoldersRefusal(host, type, name, member, every, activeRows, rows.candidates) : undefined;
const installed = installedManifestsOfEngine(host.engine);
const pairRow = (row: { package_id?: unknown }): boolean =>
typeof row.package_id === 'string' && every.some((holder) => pairsOf(holder, installed).includes(row.package_id as string));
Expand All @@ -220,6 +235,73 @@ export async function servedRowHolderOf(
return { kind: 'refused', error: new PackageScopedNameRefusedError({ ...base, message: packageScopedRefusalMessage(base) }) };
}

/**
* [#32607] The marker {@link storedRowHoldersRefusal} sets on its refusal, a
* registered symbol (so every copy of this package reads the same key, and
* no envelope serializes it). The layered chains' owner step in `RestServer`
* and the runtime dispatcher reads it by the same `Symbol.for` key (neither
* depends on this package at run time): it hands over to the package-less
* refusal step only the refusal this judgment made, and rethrows every other
* (contract review S1).
*/
const STORED_ROW_HOLDERS_REFUSAL: unique symbol = Symbol.for('objectstack.metadata-protocol.storedRowHoldersRefusal') as never;

/**
* [#32607] The item face's judgment of a name with no disabled holder
* ({@link servedRowHolderOf} with `refuseWithoutDisabled`): two or more
* packages holding it only as stored rows are refused naming each, where the
* plain read served the row saved first. `undefined` leaves the read as it
* was.
*
* Only a name no enabled package ships, at any member: a shipped name keeps
* the read's own content judgment, which counts the shipper
* (`packagelessContentPackage` over the name's shipped artifacts), so there is
* no save order to guess there (a bundled type's base shipping one locale
* beside another package's row of another).
*
* [contract review M1, M2] The holders are judged at ONE grain: the packages
* the item's active rows are bound to at the member, as the read itself
* binds them ({@link packagelessAddress}), and never united with the
* registry's holders of the name. With no shipper the registry knows a
* row-only name through one bare slot, the row written or hydrated last, at
* the name's grain, so the union followed save order (a customization pair
* named as a holder when its row was the slot's entry) and counted a holder
* of another member (a base's `en-US` row against a dependent's added
* `fr-FR`). Excluded, as beside a disabled holder: a row the reads decline
* ({@link ObjectStackProtocolImplementation.declinesStoredRow}) and a row of a
* customization pair of another package the rows or the registry name
* (`pairsOf`), which is that package's customization, never a holder of its
* own. An unbound package-less row among them leaves the read as it was.
*/
async function storedRowHoldersRefusal(
host: ServedRowHost,
type: string, name: string, member: string | undefined,
every: readonly string[],
activeRows: Awaited<ReturnType<ServedRowHost['readItemActiveRows']>>,
bound: readonly string[],
): Promise<{ kind: 'refused'; error: PackageScopedNameRefusedError } | undefined> {
const registry: any = host.engine?.registry;
if ([...shippingPackageIds(host.shippedArtifactsOf(type, name))].some((id) => registry.isPackageDisabled(id) !== true)) return undefined;
const installed = installedManifestsOfEngine(host.engine);
const known = [...new Set([...every, ...bound])];
const pairOfAnother = (row: { package_id?: unknown }): boolean => typeof row.package_id === 'string'
&& known.some((holder) => holder !== row.package_id && pairsOf(holder, installed).includes(row.package_id as string));
const holding = await host.packagelessAddress(type, name, member, 'active', activeRows.filter((row) =>
!host.declinesStoredRow(type, name, row) && !pairOfAnother(row)));
if (holding.unbound || holding.candidates.length < 2) return undefined;
const base = {
status: 'refused' as const,
reason: 'ambiguous-in-environment' as const,
type: type as PackageScopedResolvableType,
name,
callerPackageId: null,
candidates: [...holding.candidates].sort().map((packageId) => ({ kind: 'package' as const, packageId })),
};
const error = new PackageScopedNameRefusedError({ ...base, message: packageScopedRefusalMessage(base) });
Object.defineProperty(error, STORED_ROW_HOLDERS_REFUSAL, { value: true });
return { kind: 'refused', error };
}

/**
* [#32183, #32354] The judgment of the address a read naming no package takes
* for a name a disabled package also holds: the registry's served holder
Expand All @@ -228,14 +310,18 @@ export async function servedRowHolderOf(
* holders; `undefined` leaves the read as it was. Answered, never thrown: the
* plain read throws the refusal ({@link servedAddressOf}); the layered read
* only re-addresses, and leaves the refusal to its door (contract review S1).
* `options` as on {@link servedRowHolderOf}: only the item face asks it, so
* [#32607] two enabled row holders of a name nobody ships are refused there
* alone.
*/
export async function servedAddressJudgment(
host: ServedRowHost,
type: string, name: string, member?: string,
options: { readonly refuseWithoutDisabled?: boolean } = {},
): Promise<{ kind: 'served'; packageId: string } | { kind: 'refused'; error: PackageScopedNameRefusedError } | undefined> {
const registryAddress = servedHolderAddressOf(host.engine?.registry, type, name);
if (registryAddress !== undefined) return { kind: 'served', packageId: registryAddress };
return servedRowHolderOf(host, type, name, member);
return servedRowHolderOf(host, type, name, member, options);
}

/**
Expand All @@ -246,8 +332,9 @@ export async function servedAddressJudgment(
export async function servedAddressOf(
host: ServedRowHost,
type: string, name: string, member?: string,
options: { readonly refuseWithoutDisabled?: boolean } = {},
): Promise<string | undefined> {
const judged = await servedAddressJudgment(host, type, name, member);
const judged = await servedAddressJudgment(host, type, name, member, options);
if (judged?.kind === 'refused') throw judged.error;
return judged?.packageId;
}
Expand Down Expand Up @@ -292,10 +379,28 @@ export async function readMetaItemFace(
// [contract review N1] The member selector resolved once, as the layered
// read resolves it: a malformed one is the read's own 400, never a 409
// judged at a member nobody named.
// [#32607] …and refuses two enabled row holders of a name nobody ships.
// The rows that judgment reads are read on the face's key and handed to
// the read below, which takes them instead of reading them again
// (#24371: the item's active rows, one read).
let activeRows: PrereadActiveRows | undefined;
const host = this;
const judgeHost: ServedRowHost = {
get engine() { return host.engine; },
packagelessAddress: (...a) => this.packagelessAddress(...a),
shippedArtifactsOf: (...a) => this.shippedArtifactsOf(...a),
declinesStoredRow: (...a) => this.declinesStoredRow(...a),
rethrowUnlessMetadataStoreUnprovisioned: (...a) => this.rethrowUnlessMetadataStoreUnprovisioned(...a),
readItemActiveRows: async (type, name) => {
const rows = await this.readItemActiveRows(type, name, true);
activeRows = { type, name, rows };
return rows;
},
};
const servedAddress = request.packageId
? undefined
: await servedAddressOf(this, request.type, request.name, resolveBundleMemberSelector(request.type, request.member));
const answer = await this.readMetaItem(servedAddress === undefined ? request : { ...request, packageId: servedAddress }, { face });
: await servedAddressOf(judgeHost, request.type, request.name, resolveBundleMemberSelector(request.type, request.member), { refuseWithoutDisabled: true });
const answer = await this.readMetaItem(servedAddress === undefined ? request : { ...request, packageId: servedAddress }, { face, ...(activeRows !== undefined ? { activeRows } : {}) });
// [#28877] The served read of an `action` also withholds one that launches
// into a disabled package, as the served list does.
const withheldAction = request.type === 'action' && face === 'served'
Expand Down
Loading
Loading