1111 * - Both item reads' protection envelope (`getMetaItem`,
1212 * `getMetaItemLayered`, through `servedLockState`) and the per-type
1313 * `locked` count of `getMetaDiagnostics` call {@link resolveItemLock}.
14- * - The lock family a served body carries (`mergeArtifactProtection`) calls
15- * it too, so a body never states a lock the envelope does not report.
14+ * - The lock family a served body carries is that resolution's answer
15+ * ({@link withItemLockFamily}, through `mergeArtifactProtection` and the
16+ * layered read's `effective`), so a body never states a lock the envelope
17+ * does not report.
1618 *
1719 * Before this module the reads took their lock from two derivations of their
1820 * own, and neither was the door's:
7678 * across the rows in scope. The served document is still the row the
7779 * address prefers, and its lock family is the binding layer's.
7880 *
79- * ⛔ Not a policy of its own. Which artifact a caller hands the `artifact`
80- * layer is still the caller's lookup.
81+ * ## [#21803] The artifact layer is every installed package that ships the name
82+ *
83+ * The `artifact` layer is not an artifact a caller picks either. Two installed
84+ * code packages may ship one `(type, name)` (ADR-0048 §3.4), and before this
85+ * rule the `_lock` gate looked the artifact up with no package (the first
86+ * package registered) while both reads looked it up with the request's
87+ * package (prefer-local). One item had two lock answers, and which one the
88+ * door gave depended on registration order: with package B shipping
89+ * `_lock: 'full'` and package A shipping no lock, a read naming A reported
90+ * `'none'` while the door refused (B registered first), and a read naming B
91+ * reported `'full'` while the door admitted (A registered first).
92+ *
93+ * Every caller now hands {@link resolveArtifactLockLayer} the item's address
94+ * and a reader of every artifact the installed packages ship under the name,
95+ * and gets the layer: those artifacts, the address's own package first. The
96+ * resolution then reads the layers once per shipping package and takes the
97+ * strictest answer:
98+ *
99+ * - **Per package, the rule above.** Each package's artifact over the
100+ * overlay layer, first binding layer wins, so a package's answer is
101+ * exactly what the door gave when that package's artifact was the one it
102+ * looked up.
103+ * - **The lock is the strictest of those answers** ({@link strictestLock}).
104+ * So the lock is never looser than the door's answer under ANY
105+ * registration order, and it no longer depends on one. Taking the
106+ * strictest artifact alone, and then the first binding layer, would not
107+ * hold that: with A shipping no lock, B shipping `'no-delete'` and the
108+ * stored row declaring `'no-overlay'`, the door refused the save when A was
109+ * registered first (A's artifact does not bind, the row does), and B's
110+ * `'no-delete'` binding as the whole artifact layer would admit it.
111+ * - **Prose** is the answer of the first package, in the layer's order,
112+ * whose answer is the strictest one: the address's own package, then the
113+ * others by package id, so it does not depend on registration order
114+ * either. When no single package's answer is the strictest (one refuses
115+ * the write, another the delete), the lock carries no prose and no layer.
116+ * - **Content stays prefer-local** (ADR-0048). The served document and its
117+ * provenance are still the address's own package's artifact.
118+ *
119+ * With one package shipping the name, or none, the answer is exactly what it
120+ * was: one per-package answer is the strictest of one.
121+ *
122+ * ## The served body carries the resolution's answer
123+ *
124+ * {@link withItemLockFamily} writes the answer's lock family onto a served
125+ * body and removes any `_lock*` key the answer does not carry. A body is often
126+ * not the document a layer binds from (the address's own package's artifact,
127+ * a row served by content precedence outside the lock's scope), so a family
128+ * left in place could state a lock the envelope does not report.
81129 */
82130import {
83131 MetadataLockSchema ,
@@ -91,8 +139,10 @@ import {
91139/**
92140 * The layers an item's lock can come from, in precedence order:
93141 *
94- * - `artifact`: the item a code package's loader registered (ADR-0010 §3.3,
95- * "an overlay cannot loosen a packaged lock");
142+ * - `artifact`: the items the code packages' loaders registered (ADR-0010
143+ * §3.3, "an overlay cannot loosen a packaged lock"), one per installed
144+ * package that ships the name, as {@link resolveArtifactLockLayer} selects
145+ * them from the item's address;
96146 * - `overlay`: the stored `sys_metadata` rows in the item's scope (ADR-0005),
97147 * as {@link resolveOverlayLockLayer} selects them from the item's address.
98148 */
@@ -101,8 +151,18 @@ export const ITEM_LOCK_LAYERS = Object.freeze(['artifact', 'overlay'] as const);
101151/** One of {@link ITEM_LOCK_LAYERS}. */
102152export type ItemLockLayer = ( typeof ITEM_LOCK_LAYERS ) [ number ] ;
103153
104- /** Per layer, the document that layer contributes, or `undefined` when it has none. */
105- export type ItemLockLayers = { readonly [ L in ItemLockLayer ] : unknown } ;
154+ /**
155+ * Per layer, what that layer contributes:
156+ *
157+ * - `artifact`: [#21803] the artifact of every installed package that ships
158+ * the item, in {@link resolveArtifactLockLayer}'s order (empty when none
159+ * does);
160+ * - `overlay`: the overlay layer's document, or `undefined` when it has none.
161+ */
162+ export type ItemLockLayers = {
163+ readonly artifact : readonly unknown [ ] ;
164+ readonly overlay : unknown ;
165+ } & { readonly [ L in ItemLockLayer ] : unknown } ;
106166
107167/** The resolution's answer. */
108168export interface ItemLock {
@@ -127,12 +187,25 @@ const UNLOCKED: ItemLock = Object.freeze({
127187} ) ;
128188
129189/**
130- * Resolve the item's lock from the documents its layers contribute.
131- * See this module's header for the rule.
190+ * Resolve the item's lock from what its layers contribute. See this module's
191+ * header for the rule: per shipping package, the first layer whose declared
192+ * `_lock` is not `'none'` binds; [#21803] the item's lock is the strictest of
193+ * those answers.
132194 */
133195export function resolveItemLock ( layers : ItemLockLayers ) : ItemLock {
196+ const shipped : readonly unknown [ ] = layers . artifact . length > 0 ? layers . artifact : [ undefined ] ;
197+ const perPackage = shipped . map ( ( artifact ) => firstBindingLayer ( { artifact, overlay : layers . overlay } ) ) ;
198+ const lock = strictestLock ( perPackage . map ( ( answer ) => answer . lock ) ) ;
199+ if ( lock === 'none' ) return UNLOCKED ;
200+ // The first package, in the layer's order, whose answer is the strictest.
201+ // None is when the strictest joins two answers that refuse different verbs.
202+ return perPackage . find ( ( answer ) => answer . lock === lock ) ?? { ...UNLOCKED , lock } ;
203+ }
204+
205+ /** The rule for ONE package's artifact over the overlay layer: the first binding layer. */
206+ function firstBindingLayer ( documents : { readonly [ L in ItemLockLayer ] : unknown } ) : ItemLock {
134207 for ( const layer of ITEM_LOCK_LAYERS ) {
135- const declared = extractProtection ( layers [ layer ] ) ;
208+ const declared = extractProtection ( documents [ layer ] ) ;
136209 if ( declared . lock !== 'none' ) {
137210 return {
138211 lock : declared . lock ,
@@ -147,25 +220,26 @@ export function resolveItemLock(layers: ItemLockLayers): ItemLock {
147220}
148221
149222/**
150- * {@link resolveItemLock }, reading each layer only when no layer above it
151- * binds. The answer is the same; what this saves is the reads below a binding
152- * layer. The write doors use it, so a packaged lock is answered without a
153- * `sys_metadata` read, and a store that cannot be read never turns a packaged
154- * lock's `ITEM_LOCKED` into a 503.
223+ * {@link resolveItemLock }, reading the `overlay` layer only when it can bind
224+ * for some shipping package: when no package ships the item, or one of them
225+ * ships an artifact that declares no lock. The answer is the same; what this
226+ * saves is the `sys_metadata` read when every shipping package's own lock
227+ * binds. The write doors use it, so such a packaged lock is answered without a
228+ * store read, and a store that cannot be read never turns its `ITEM_LOCKED`
229+ * into a 503.
155230 *
156231 * A reader's failure propagates: each caller owns its #5532 / #5706
157232 * discrimination.
158233 */
159234export async function resolveItemLockLazily ( read : {
160- readonly [ L in ItemLockLayer ] : ( ) => unknown | Promise < unknown > ;
235+ readonly artifact : ( ) => readonly unknown [ ] | Promise < readonly unknown [ ] > ;
236+ readonly overlay : ( ) => unknown | Promise < unknown > ;
161237} ) : Promise < ItemLock > {
162- const layers = Object . fromEntries ( ITEM_LOCK_LAYERS . map ( ( layer ) => [ layer , undefined ] ) ) as Record < ItemLockLayer , unknown > ;
163- for ( const layer of ITEM_LOCK_LAYERS ) {
164- layers [ layer ] = await read [ layer ] ( ) ;
165- const answer = resolveItemLock ( layers ) ;
166- if ( answer . layer !== undefined ) return answer ;
167- }
168- return UNLOCKED ;
238+ const artifact = await read . artifact ( ) ;
239+ const everyPackageBinds = artifact . length > 0
240+ && artifact . every ( ( document ) => extractProtection ( document ) . lock !== 'none' ) ;
241+ if ( everyPackageBinds ) return resolveItemLock ( { artifact, overlay : undefined } ) ;
242+ return resolveItemLock ( { artifact, overlay : await read . overlay ( ) } ) ;
169243}
170244
171245// ── [#21761] The overlay layer, selected from the item's address ─────────────
@@ -313,20 +387,60 @@ function strictestRowLockDocument(rows: readonly StoredOverlayRow[], packageId:
313387 return family ;
314388}
315389
390+ // ── [#21803] The artifact layer, selected from the item's address ────────────
391+
392+ /**
393+ * Every artifact the installed code packages registered under the addressed
394+ * item's name: one per package that ships it, whichever package that is. Each
395+ * caller reads its own registry; the protocol's reader is
396+ * `ObjectStackProtocolImplementation.shippedArtifactsOf`.
397+ */
398+ export type ShippedArtifactReader = ( ) => readonly unknown [ ] ;
399+
400+ /**
401+ * [#21803] THE artifact layer's selection. What the `artifact` layer of
402+ * {@link resolveItemLock} gets for `address`: every artifact `artifactsOf`
403+ * reads, each once, the address's own package's first, then the others by
404+ * package id, so the order (and with it the prose of the answer) does not
405+ * depend on registration order. See this module's header for the rule.
406+ *
407+ * Every caller that enforces or reports an item's lock passes its address
408+ * here: the `_lock` gate, both item reads, the list and the diagnostics tile.
409+ * `packageId` orders the layer; it never decides which packages are in it.
410+ */
411+ export function resolveArtifactLockLayer ( address : ItemAddress , artifactsOf : ShippedArtifactReader ) : readonly unknown [ ] {
412+ const shipped = [ ...new Set ( artifactsOf ( ) ) ] . filter ( ( artifact ) => artifact !== undefined && artifact !== null ) ;
413+ const packageOf = ( artifact : unknown ) : string => {
414+ const id = ( artifact as { _packageId ?: unknown } ) . _packageId ;
415+ return typeof id === 'string' ? id : '' ;
416+ } ;
417+ const rank = ( artifact : unknown ) : number =>
418+ address . packageId !== undefined && packageOf ( artifact ) === address . packageId ? 0 : 1 ;
419+ // `sort` is stable: one package's two entries keep the reader's order.
420+ return shipped . sort ( ( a , b ) => rank ( a ) - rank ( b ) || packageOf ( a ) . localeCompare ( packageOf ( b ) ) ) ;
421+ }
422+
316423/**
317- * [#21761] `document` with the lock family of the `overlay` layer
318- * ({@link resolveOverlayLockLayer}) when that layer binds, so a served body
319- * states the lock the envelope reports even when the rows in scope that bind
320- * are not the row the body was served from. A key that layer's document does
321- * not declare is removed. Returns `document` itself when the overlay layer does
322- * not bind or nothing changes (the binding row is the served row), and a copy
323- * otherwise. The `artifact` layer's family is `mergeArtifactProtection`'s to
324- * put, as before.
424+ * [#21761, #21803] `document` carrying the lock family of `itemLock`, the
425+ * one item-lock resolution's answer: `_lock` and the prose the answer carries,
426+ * and no `_lock*` key it does not. So a served body states exactly the lock the
427+ * envelope reports, whichever document it was served from: a row the address
428+ * prefers for content while other rows in scope bind ([#21761]), the address's
429+ * own package's artifact while another package's lock binds ([#21803]), or a
430+ * row served by content precedence from a scope the lock is not read from (a
431+ * family the answer does not carry is removed). Returns `document` itself when
432+ * nothing changes, and a copy otherwise.
325433 */
326- export function withOverlayLockFamily ( document : unknown , layers : ItemLockLayers ) : unknown {
434+ export function withItemLockFamily ( document : unknown , itemLock : ItemLock ) : unknown {
327435 if ( ! document || typeof document !== 'object' || Array . isArray ( document ) ) return document ;
328- if ( resolveItemLock ( layers ) . layer !== 'overlay' ) return document ;
329- const family = layers . overlay as Record < string , unknown > ;
436+ const family : Record < string , unknown > = itemLock . lock === 'none'
437+ ? { }
438+ : {
439+ _lock : itemLock . lock ,
440+ _lockReason : itemLock . lockReason ,
441+ _lockDocsUrl : itemLock . lockDocsUrl ,
442+ _lockSource : itemLock . lockSource ,
443+ } ;
330444 const current = document as Record < string , unknown > ;
331445 if ( LOCK_FAMILY_KEYS . every ( ( key ) => current [ key ] === family [ key ] ) ) return document ;
332446 const out : Record < string , unknown > = { ...current } ;
0 commit comments