Skip to content

Commit cd7716e

Browse files
committed
fix(metadata-protocol): judge an overlay-write close's declared body against the pair the ensure door would create, and name the ceremony in the named-base sentence
Claude-Session: https://claude.ai/code/session_01Utp6CYeyrEJGAte4R8B7un Co-authored-by: Claude <noreply@anthropic.com>
1 parent 42b0ab5 commit cd7716e

3 files changed

Lines changed: 93 additions & 33 deletions

File tree

‎packages/metadata-protocol/src/protocol-packaged-base-refusal.ts‎

Lines changed: 75 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ import { isOriginGatedType, managedItemSealedSentence, regimeCDeclaredSaveSenten
4040
import { SysMetadataRepository } from './sys-metadata-repository.js';
4141
import { OBJECT_EXTENSION_TYPE } from './protocol-object-extension-item.js';
4242
import { customizationPairEnsureRefusal, type PairRegistry } from './protocol-customization-package.js';
43+
import { resolvePackageClosure } from '@objectstack/lint/runtime';
4344
import { overlayMemberField, overlayMemberOfBody, overlayMemberValue } from './migrations/overlay-index.js';
4445
import {
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
*/
270278
function 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
*/
307317
export 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
*/
396439
export 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
*/
852900
export 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

‎packages/metadata-protocol/src/protocol.named-base-overlay-write.test.ts‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,8 @@
2525
* draft stays a draft and no active row is written;
2626
* - the rollback of a row bound to the base: refused, the row unchanged;
2727
* - CONTROLS, open as on `main`: a delete naming the base removes such a row
28-
* (the one way to clear it: the ceremony moves package-less rows only), a
28+
* (one way to clear it; [#29392] `os migrate packageless-overlays` moves it
29+
* into the pair, and the sentence names both), a
2930
* draft discard naming it, the pair's declared save (served over a row
3031
* bound to the base), a server-stated rewrite (`source` set), a writable
3132
* package's same name, a name the base does not ship (ADR-0070 D1's own
@@ -211,6 +212,10 @@ function expectClosedSentence(message: string, type: string, name: string, verb:
211212
expect(message).toContain('ADR-0146 D1, D11');
212213
if (verb === 'rollback') expect(message).toContain('a rollback of its row bound to the read-only base is closed');
213214
else expect(message).toContain(`${verb} it into '${PAIR}'`);
215+
// [#29392] The ceremony moves a row bound to the base into the pair: the
216+
// sentence points an operator holding one to it, beside the other two doors.
217+
expect(message).toContain('is served until moved by `os migrate packageless-overlays`, overridden by the pair, '
218+
+ 'or deleted naming the base.');
214219
// The REST door cuts a refusal's text at 500 characters
215220
// (`CLIENT_MESSAGE_MAX`, `@objectstack/types`): the whole sentence fits.
216221
expect(message.length).toBeLessThan(500);

‎packages/metadata-protocol/src/protocol.ts‎

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -338,6 +338,7 @@ import type {
338338
NamedBaseWriteSubject,
339339
OverrideWriteSubject,
340340
OverrideWriteVerdict,
341+
WrittenPackageScope,
341342
PackagelessWriteOperation,
342343
} from './protocol-packaged-base-refusal.js';
343344
// [ADR-0146 D10, #28564] The `__c` extension-field reservation's one judge.
@@ -8267,8 +8268,8 @@ export class ObjectStackProtocolImplementation implements
82678268
* {@link promoteDraftForPublish}) or a rollback that would land a row bound
82688269
* to the base. D1 puts a read-only package's customizations in its paired
82698270
* `<base>.custom`; such a row is served at the base's address with no
8270-
* declaration, outside the pair, and `os migrate packageless-overlays`
8271-
* never sees it (its census reads package-less rows only). Refused with
8271+
* declaration, outside the pair, until [#29392] `os migrate
8272+
* packageless-overlays` moves it into the pair. Refused with
82728273
* the fix the package-less close names ({@link packagelessOverlayWriteRefusal},
82738274
* one spelling): write it into the pair, declaring the override.
82748275
*
@@ -8282,9 +8283,10 @@ export class ObjectStackProtocolImplementation implements
82828283
* that could not be taken.
82838284
*
82848285
* ⛔ Not a delete: removing a row bound to the base writes nothing, and it
8285-
* is the only way to clear such a row stored before the close (the
8286-
* ceremony moves package-less rows only). A row stored before it stays
8287-
* served until the pair declares the override, which is served over it.
8286+
* is one way to clear such a row stored before the close; the ceremony is
8287+
* the other, moving it into the pair with its declaration. A row stored
8288+
* before it stays served until one of those, or the pair's declared
8289+
* override, which is served over it; the sentence names all three.
82888290
*
82898291
* Authoring writes only: the server-stated rewrites (`source` set, the
82908292
* `package-duplicate` write face) name the row they rewrite. Registry
@@ -8316,15 +8318,17 @@ export class ObjectStackProtocolImplementation implements
83168318
*
83178319
* Asked by the save door ahead of {@link refusePackagedBaseOverride}, and
83188320
* by the promotion ({@link promoteDraftForPublish}, so both
8319-
* `publishMetaItem` and `publishPackageDrafts`) on the draft's body.
8321+
* `publishMetaItem` and `publishPackageDrafts`) on the draft's body. The
8322+
* two overlay-write closes ask it about the pair their fix writes into,
8323+
* with the closure a pair not yet ensured will have (`writtenScope`).
83208324
*/
8321-
private overrideWriteVerdict(request: OverrideWriteSubject): OverrideWriteVerdict {
8325+
private overrideWriteVerdict(request: OverrideWriteSubject, writtenScope?: WrittenPackageScope): OverrideWriteVerdict {
83228326
const host: ThisParameterType<typeof overrideWriteVerdict> = {
83238327
isWritablePackage: (...a) => this.isWritablePackage(...a),
83248328
resolveWritePackageScope: (...a) => this.resolveWritePackageScope(...a),
83258329
shippedArtifactsOf: (...a) => this.shippedArtifactsOf(...a),
83268330
};
8327-
return overrideWriteVerdict.call(host, request);
8331+
return overrideWriteVerdict.call(host, request, writtenScope);
83288332
}
83298333

83308334
/** Docblock and body: {@link ProtocolWriteGatesFacet.refusePackagedBaseRemoval} in `protocol-write-gates.ts` [#29985 codemod]. */

0 commit comments

Comments
 (0)