@@ -40,6 +40,7 @@ import { isOriginGatedType, managedItemSealedSentence, regimeCDeclaredSaveSenten
4040import { SysMetadataRepository } from './sys-metadata-repository.js' ;
4141import { OBJECT_EXTENSION_TYPE } from './protocol-object-extension-item.js' ;
4242import { customizationPairEnsureRefusal , type PairRegistry } from './protocol-customization-package.js' ;
43+ import { resolvePackageClosure } from '@objectstack/lint/runtime' ;
4344import { overlayMemberField , overlayMemberOfBody , overlayMemberValue } from './migrations/overlay-index.js' ;
4445import {
4546 customizationBasePackageId ,
@@ -246,12 +247,12 @@ export function packagelessOverlayWriteRefusal(
246247 closed : { base : string ; pair : string } ,
247248 operation : PackagelessWriteOperation ,
248249) : Error {
249- const { item, shipped, pair, declared, writeInto } = closedOverlayWriteClauses ( request , closed ) ;
250- const rule = ' A write naming no package is closed where `os migrate packageless-overlays` can move its row '
250+ const { item, shipped, pair, declared, writeInto, ceremony } = closedOverlayWriteClauses ( request , closed ) ;
251+ const rule = ` A write naming no package is closed where ${ ceremony } can move its row `
251252 + `(ADR-0146 D11); a package-less row stored before it is served until that command moves it into '${ closed . pair } '.` ;
252253 const message = operation === 'rollback'
253254 ? `${ item } ${ shipped } , and a rollback of its package-less row is closed (ADR-0146 D11): run `
254- + `\`os migrate packageless-overlays\` to move the row into ${ pair } ; to restore a version from before `
255+ + `${ ceremony } to move the row into ${ pair } ; to restore a version from before `
255256 + `the move, save that version's body there, declaring ${ declared } .`
256257 : operation === 'delete'
257258 ? `${ item } ${ shipped } : remove its customization from ${ pair } . ${ rule } `
@@ -266,6 +267,13 @@ export function packagelessOverlayWriteRefusal(
266267 * the declaration, and the fix — write it into the pair, declaring the
267268 * override. ONE spelling: a writer refused at either address reads the same
268269 * fix, and only the rule clause that follows it says which address is closed.
270+ *
271+ * Two more clauses, one spelling each: `ceremony`, the command that moves a
272+ * stored row into the pair (both sentences name it), and `boundRowServed`,
273+ * what serves a row bound to the base stored before the close until one of
274+ * the three open doors acts on it — [#29392] that command, which moves it
275+ * into the pair with its declaration, the pair's declared override, or a
276+ * delete naming the base.
269277 */
270278function closedOverlayWriteClauses (
271279 request : { type : string ; name : string } ,
@@ -278,7 +286,9 @@ function closedOverlayWriteClauses(
278286 const declared = `overrides: { package: '${ closed . base } ', basedOn }` ;
279287 const writeInto = ( operation : 'save' | 'publish' ) =>
280288 `${ item } ${ shipped } : ${ operation === 'publish' ? 'publish' : 'save' } it into ${ pair } , declaring ${ declared } .` ;
281- return { item, shipped, pair, declared, writeInto } ;
289+ const ceremony = '`os migrate packageless-overlays`' ;
290+ const boundRowServed = `is served until moved by ${ ceremony } , overridden by the pair, or deleted naming the base.` ;
291+ return { item, shipped, pair, declared, writeInto, ceremony, boundRowServed } ;
282292}
283293
284294/** `403 NOT_OVERRIDABLE`, the code and status the sealed write already answers, so neither close gains a code. */
@@ -298,11 +308,11 @@ function closedOverlayWriteError(message: string): Error {
298308 * a row bound to the base. `rollback` is every restore of a stored version:
299309 * the per-item rollback and [#29390] a commit revert's restore limb (and the
300310 * package rollback through it), one sentence for both. ⛔ Not `delete`:
301- * removing a row bound to the base writes nothing, and it is the one way to
302- * clear such a row stored before the close — `os migrate packageless-overlays`
303- * reads package-less rows only, so it never moves one . The removal serves the
304- * base's own document again, or the pair's declared override where one exists
305- * (it is served over the row).
311+ * removing a row bound to the base writes nothing, and it is one way to clear
312+ * such a row stored before the close; [#29392] `os migrate packageless-overlays`
313+ * is the other, moving it into the pair with its declaration . The removal
314+ * serves the base's own document again, or the pair's declared override where
315+ * one exists (it is served over the row).
306316 */
307317export type NamedBaseWriteOperation = Exclude < PackagelessWriteOperation , 'delete' > ;
308318
@@ -314,12 +324,14 @@ export type NamedBaseWriteOperation = Exclude<PackagelessWriteOperation, 'delete
314324 * package itself (`?package=<base>`) onto an item of one of the five overlay
315325 * types that package ships stored the customization as a row bound to the
316326 * base, with no declaration: served at the base's address in place of its
317- * document, outside the pair D1 puts every customization in, and not seen by
318- * `os migrate packageless-overlays`. It is refused with the fix the
319- * package-less close names ({@link closedOverlayWriteClauses}): write it into
320- * the pair, declaring the override. A row stored that way before the close is
321- * still served until the pair declares the override (the declared override is
322- * served over it) or a delete naming the base removes it — both open.
327+ * document, outside the pair D1 puts every customization in. It is refused
328+ * with the fix the package-less close names ({@link closedOverlayWriteClauses}):
329+ * write it into the pair, declaring the override. A row stored that way before
330+ * the close is still served until one of three open doors acts on it, all
331+ * named in the sentence ({@link closedOverlayWriteClauses}' `boundRowServed`):
332+ * [#29392] `os migrate packageless-overlays` moves it into the pair with its
333+ * declaration, the pair declares the override (served over it), or a delete
334+ * naming the base removes it.
323335 *
324336 * Fix first, under the REST door's 500-character bound (`CLIENT_MESSAGE_MAX`,
325337 * `@objectstack/types`), as the package-less sentence.
@@ -329,13 +341,12 @@ export function namedBaseOverlayWriteRefusal(
329341 closed : { base : string ; pair : string } ,
330342 operation : NamedBaseWriteOperation ,
331343) : Error {
332- const { item, shipped, pair, declared, writeInto } = closedOverlayWriteClauses ( request , closed ) ;
333- const served = 'is served until the pair declares the override, or a delete naming the base removes it.' ;
344+ const { item, shipped, pair, declared, writeInto, boundRowServed } = closedOverlayWriteClauses ( request , closed ) ;
334345 const message = operation === 'rollback'
335346 ? `${ item } ${ shipped } , and a rollback of its row bound to the read-only base is closed (ADR-0146 D1, D11): `
336- + `save the version's body into ${ pair } , declaring ${ declared } . The row ${ served } `
347+ + `save the version's body into ${ pair } , declaring ${ declared } . It ${ boundRowServed } `
337348 : `${ writeInto ( operation ) } The read-only base itself takes no customization (ADR-0146 D1, D11): `
338- + `a row bound to it from before ${ served } ` ;
349+ + `a row bound to it ${ boundRowServed } ` ;
339350 return closedOverlayWriteError ( message ) ;
340351}
341352
@@ -348,8 +359,31 @@ export interface OverlayPairFix {
348359 readonly base : string ;
349360 readonly pair : string ;
350361 readonly basedOn : string ;
362+ /**
363+ * The closure the pair WILL have, present only while the pair is not
364+ * installed: the ensure door declares the pair's one dependency, the base
365+ * (`manifest.dependencies: { [base]: range }`), so its closure is the base
366+ * and what the base reaches. The gate's own read
367+ * (`resolveWritePackageScope`) answers nothing for a package the registry
368+ * cannot produce, and its `not_concerned` on a declared body is no verdict
369+ * about the write the fix takes.
370+ */
371+ readonly pendingPairScope ?: WrittenPackageScope ;
372+ }
373+
374+ /** A written package and the closure of its declared dependencies, as `resolveWritePackageScope` answers it. */
375+ export interface WrittenPackageScope {
376+ readonly packageId : string ;
377+ readonly dependencies : readonly string [ ] ;
351378}
352379
380+ /**
381+ * The override write gate as a close asks it: {@link overrideWriteVerdict},
382+ * with the written package's closure when the caller holds it (a pair the
383+ * ensure step would create, {@link OverlayPairFix.pendingPairScope}).
384+ */
385+ export type OverrideWriteVerdictAsk = ( request : OverrideWriteSubject , writtenScope ?: WrittenPackageScope ) => OverrideWriteVerdict ;
386+
353387/**
354388 * [ADR-0146 D1, D11] The registry half of whether the fix both overlay-write
355389 * closes name — write it into `base`'s pair, declaring the override — can be
@@ -382,7 +416,11 @@ export function overlayPairFixOf(
382416 if ( customizationPairEnsureRefusal ( host . engine , registry , base , pair ) !== undefined ) return null ; // the ensure door would refuse
383417 const basedOn = registry . getPackage ( base ) ?. manifest ?. version ;
384418 if ( typeof basedOn !== 'string' || basedOn === '' ) return null ; // no `basedOn` to declare
385- return { base, pair, basedOn } ;
419+ if ( registry . getPackage ( pair ) ) return { base, pair, basedOn } ;
420+ // The pair the ensure door would create: one dependency, the base.
421+ const baseClosure = resolvePackageClosure ( base , ( id ) => registry . getPackage ( id ) as { manifest ?: unknown } | undefined ) ;
422+ const dependencies = [ base , ...( baseClosure ?. dependencies ?? [ ] ) . filter ( ( id ) => id !== pair ) ] ;
423+ return { base, pair, basedOn, pendingPairScope : { packageId : pair , dependencies } } ;
386424}
387425
388426/**
@@ -392,9 +430,14 @@ export function overlayPairFixOf(
392430 * identity at `member`, declaring `overrides: { package: base, basedOn }`. An
393431 * `email_template` member at a locale the base does not ship is refused
394432 * there, so its write stays open.
433+ *
434+ * The gate is asked about the pair the fix writes into, installed or not: a
435+ * pair not yet ensured (a fresh install) is judged with the closure the
436+ * ensure door gives it ({@link OverlayPairFix.pendingPairScope}), so the
437+ * answer is the one the write meets once the pair exists.
395438 */
396439export function overlayPairFixAdmits (
397- host : { readonly overrideWriteVerdict : ( request : OverrideWriteSubject ) => OverrideWriteVerdict } ,
440+ host : { readonly overrideWriteVerdict : OverrideWriteVerdictAsk } ,
398441 fix : OverlayPairFix ,
399442 subject : {
400443 /** The singular type. */
@@ -411,7 +454,7 @@ export function overlayPairFixAdmits(
411454 ...( body ?? { name, ...( disc && member !== null ? { [ disc ] : member } : { } ) } ) ,
412455 overrides : { package : fix . base , basedOn : fix . basedOn } ,
413456 } ;
414- const verdict = host . overrideWriteVerdict ( { type, name, packageId : fix . pair , item : declaredBody } ) ;
457+ const verdict = host . overrideWriteVerdict ( { type, name, packageId : fix . pair , item : declaredBody } , fix . pendingPairScope ) ;
415458 return verdict . verdict !== 'refuse' ;
416459}
417460
@@ -421,7 +464,7 @@ export interface NamedBaseCloseHost {
421464 /** The read-only code packages that ship a name of an overlay type ([] for any other type). */
422465 readonly overlayBaseShippers : ( type : string , name : string ) => string [ ] ;
423466 /** The override write gate, as the declared write into the pair meets it at the save door. */
424- readonly overrideWriteVerdict : ( request : OverrideWriteSubject ) => OverrideWriteVerdict ;
467+ readonly overrideWriteVerdict : OverrideWriteVerdictAsk ;
425468}
426469
427470/** The write naming a package that {@link namedBaseCloseSubject} judges. */
@@ -848,17 +891,25 @@ export function overrideWriteRefusalError(
848891 * member ({@link overrideWriteMemberOf}), and an artifact of another member is
849892 * no shipper: a registry that ignores `member` can answer one, as
850893 * `mergeArtifactProtection` guards on the read side.
894+ *
895+ * `writtenScope`, when given for the written package, is its closure in
896+ * place of the registry's read: a close asking about a pair the ensure step
897+ * would create ({@link OverlayPairFix.pendingPairScope}). Every door's own
898+ * write passes none.
851899 */
852900export function overrideWriteVerdict (
853901 this : Pick < PackagedBaseRefusalHost , 'isWritablePackage' | 'resolveWritePackageScope' | 'shippedArtifactsOf' > ,
854902 request : OverrideWriteSubject ,
903+ writtenScope ?: WrittenPackageScope ,
855904) : OverrideWriteVerdict {
856905 const packageId = typeof request . packageId === 'string' && request . packageId . length > 0
857906 ? request . packageId
858907 : undefined ;
859908 const declaration = declarationOf ( request . item ) ;
860909 if ( packageId === undefined && ! declaration . present ) return NOT_CONCERNED ;
861- const scope = packageId === undefined ? undefined : this . resolveWritePackageScope ( packageId ) ;
910+ const scope = packageId === undefined
911+ ? undefined
912+ : writtenScope ?. packageId === packageId ? writtenScope : this . resolveWritePackageScope ( packageId ) ;
862913 // A named package the registry cannot produce is not this gate's to
863914 // judge: both doors ask `uninstalledNamedBaseRefusal`, which refuses a
864915 // write naming an uninstalled base (`WRITABLE_PACKAGE_REQUIRED`) — the
0 commit comments