Skip to content

Commit 0837fdc

Browse files
fix(metadata-protocol): an unshipped email-template locale keeps its write before the pair is ensured; the named-base refusal names os migrate packageless-overlays (#30322)
<!-- claude-projects-attribution: {"github_login":"os-bill"} --> _Requested by **Bill** · [project thread](https://claude.ai/code/project/chan_016iMXWSZDzqWRWvWzdNo8wg?thread=cmsg_016iMXWSZDzqWRWvWzdNo8wgBNPpAVVeFjdqPfxDbdEPKj)_ domain:server#5 · os-bill · session_01Utp6CYVV6fcAQrNTDQRvPb ## 维护者速读 - **改了什么:** 给系统自带的邮件模板加一个基础包没有的语种(比如 showcase 只带英文,管理员加中文),在新装环境里现在能直接保存;指名基础包的写入(`?package=BASE`)同理。基础包已有的语种照旧被拒,并指向 `.custom` 配对包。 - **为什么改:** 代码注释写明这种写入应保持开放,但新环境里配对包还没建出来,判断「能否写进配对包」时拿到「不归我管」,被当成「能」,于是先拒一次;照报错去配对包写,又因基础包没有这个语种被拒第二次,翻译加不进去。拒绝句另补上 `os migrate packageless-overlays`,告诉运维这条命令能把老的基础包绑定行挪进配对包(#29479)。 - **风险与回滚:** 不加参数、不改错误码和状态;只改配对包尚未安装时的判断依据(按 ensure 门将要给它的依赖闭包)。指名基础包的拒绝句措辞变了(为塞进 500 字上限而收短),dogfood 钉住的 rollback 片段原样保留。回滚即撤 PR。 - **先看:** `packages/metadata-protocol/src/protocol-packaged-base-refusal.ts`(`overlayPairFixOf` 新增的 `pendingPairScope`、`overlayPairFixAdmits` 的调用、`closedOverlayWriteClauses` 的 `boundRowServed`),`packages/metadata-protocol/src/protocol.named-base-overlay-write.test.ts`(新 describe「a fresh install, the pair not yet ensured」)。 - **两分钟验证:** `pnpm --filter @objectstack/metadata-protocol exec vitest run src/protocol.named-base-overlay-write.test.ts -t "fresh install"`:三条全绿,两条无包和指名基础包的 fr-FR 写入成功,en-US 对照仍是 403 `NOT_OVERRIDABLE`。 - **需要你做的:** 无,席位落地。 Fixes #30168 Fixes #29479 Clause-②: no — the fix restores the write `packagelessCloseSubject`'s docblock documents as open ("An `email_template` member at a locale the base does not ship is refused there (#28777) and stays open"); a pair that is installed was already answered that way, and nothing outside that documented set is admitted. Metadata-protocol: semantic — an author's package-less (or named-base) save of an unshipped email-template locale, refused `403 NOT_OVERRIDABLE` on an install whose pair was not yet ensured, now succeeds. Skills-docs: none — `skills/objectstack-platform/SKILL.md` line 128 (PR #29924) already states the package-less write of an unshipped locale is kept ("an email-template row at a locale the base does not ship moves as the pair's own member and keeps that write"); this PR makes the runtime match it. ## What changes **#30168 — the close read "no such package" as "admitted".** Both overlay-write closes (`packagelessCloseSubject`, and `namedBaseCloseSubject` for `?package=BASE`) close a write only where the fix they name can be taken, and ask the override write gate about the declared body written into the pair (`overlayPairFixAdmits`). The gate reads the written package's closure from the registry (`resolveWritePackageScope`). On a fresh install the pair `BASE.custom` is not installed yet, so that read answers `undefined` and `overrideWriteVerdict` answers `not_concerned`, which `overlayPairFixAdmits` treats as "not refused". The close then closed a write whose fix is refused once the pair exists (`declared_package_ships_no_such_item`): the card's double refusal. Every existing pin booted with the pair installed, so none saw it. - `overlayPairFixOf` now carries `pendingPairScope` when the pair is not installed: the closure the ensure door gives it (one dependency, the base, `manifest.dependencies: { [base]: range }`), so the base plus what the base reaches, read with `resolvePackageClosure`, the gate's own walk. - `overrideWriteVerdict` (module function and the class method) takes an optional `writtenScope` used in place of the registry read for that package. Only `overlayPairFixAdmits` passes one; every door's own write passes none, so no door's verdict moves. - `OverrideWriteVerdictAsk` types the gate as both closes' hosts ask it (`NamedBaseCloseHost`, `PackagelessCloseHost`). **The first act's measurement (step 1 both ways).** On a fresh showcase boot (`pnpm dev -- --fresh`, this branch), `PUT /api/v1/meta/email_template/showcase_task_done_email`: - with `?member=zh-CN`: `400 VALIDATION_ERROR` "The "member" query parameter is not supported by this endpoint … Supported parameters: force, mode, package, surface." The save door is body-addressed (`META_ITEM_SAVE_PARAMS`), so this spelling never reaches the close. - without it, with `locale: 'zh-CN'` in the body: the close reads the body's member (`overlayMemberOfBody` → `zh-CN`), the right one. The defect was the gate half above, not the member reading; the docblock is right and the code was wrong. **#29479 — the named-base sentence names the ceremony.** Since PR #29477, `os migrate packageless-overlays` moves a row stored bound to the read-only base into the pair with its declaration; the refusal did not say so. `closedOverlayWriteClauses` gains two clauses, one spelling each: `ceremony` (the command, now used by both sentences, byte-identical in the package-less one) and `boundRowServed`, used by the named-base save, publish and rollback sentences: "… is served until moved by `os migrate packageless-overlays`, overridden by the pair, or deleted naming the base." The previous tail ("served until the pair declares the override, or a delete naming the base removes it") was shortened to fit: with the ceremony added in the long form, the file's 30-character-base / 40-character-name pin measured 525 (save) and 511 (rollback) characters against the REST door's 500 (`CLIENT_MESSAGE_MAX`). The phrase the dogfood suite pins ("a rollback of its row bound to the read-only base is closed") is unchanged. Docblocks that said the ceremony never moves a base-bound row (`NamedBaseWriteOperation`, `namedBaseOverlayWriteRefusal` in both files) are corrected. ## Tests `packages/metadata-protocol/src/protocol.named-base-overlay-write.test.ts`: `makeProtocol({ pairInstalled: false })` boots the base with no pair, and a new top-level describe pins: - the package-less save of `crm_welcome` at `fr-FR` (the base ships the canonical member only) is accepted and stored package-less; - the save naming the base at `fr-FR` is accepted, as it is with the pair installed; - CONTROL: the canonical member is still closed onto the pair at both addresses, with the pair not yet ensured, and nothing is written. `expectClosedSentence` now also asserts the `boundRowServed` clause on every named-base sentence; the 500-character pin covers the new length. <details><summary>Red first, on origin/main 04f36b6</summary> ``` × the package-less save is accepted and served as the environment's own member × the save naming the base is accepted, as it is with the pair installed Error: 'email_template/crm_welcome' ships in 'com.acme.crm': save it into 'com.acme.crm.custom' (?package=com.acme.crm.custom), declaring overrides: { package: 'com.acme.crm', basedOn }. A write naming no package is closed where `os migrate packageless-overlays` can move its row (ADR-0146 D11); … Tests 2 failed | 32 passed (34) ``` The CONTROL case was green on main too. </details> <details><summary>Ablation (one, the behaviour the new pins hold)</summary> `node scripts/ablation-replace.mjs --file packages/metadata-protocol/src/protocol-packaged-base-refusal.ts --anchor "item: declaredBody }, fix.pendingPairScope);" --replacement "item: declaredBody } /* ABLATED pendingPairScope */);" -- … vitest run src/protocol.named-base-overlay-write.test.ts -t 'fresh install'` on `b2fc8526`. The suite imports the source (`./protocol.js`), so no `dist/` leg applies. ``` ablation-replace: ok mutation landed: anchor 1 -> 0, blob 6d46188 -> 56db37f9ff9d × the package-less save is accepted and served as the environment's own member × the save naming the base is accepted, as it is with the pair installed Tests 2 failed | 1 passed | 34 skipped (37) ablation-replace: ok restored: blob == HEAD (6d46188) and `git diff HEAD` is empty ``` </details> ## Signature notes (for the next PR on these functions) `classifyPackagelessRow` is unchanged. `overlayPairFixAdmits` keeps its parameters and result; its host's `overrideWriteVerdict` is typed `OverrideWriteVerdictAsk` (one optional parameter, `writtenScope`), and it passes `fix.pendingPairScope`. `OverlayPairFix` gains the optional `pendingPairScope`, present only while the pair is not installed. ## Acceptance notes - Live readings (fresh showcase boot, this branch): package-less zh-CN save `200`, served at `?member=zh-CN` as the environment's own member while en-US stays the package's; `?package=com.example.showcase` fr-FR save `200`; both en-US controls `403 NOT_OVERRIDABLE` naming the pair, the named-base one with the new clause. - `content/docs/concepts/metadata-lifecycle.mdx`'s D11 callout says a package-less write onto an item one read-only package ships is refused, without the unshipped-locale exception. This PR does not make it false (the exception was documented in code and skills, not there); left as is. ## Verification All on head `b2fc8526` (after `git merge origin/main`, `1a06be80`, then a rebuild: `turbo run build --filter='!@objectstack/docs'`, 119/119). - `pnpm --filter @objectstack/metadata-protocol test`: Test Files 312 passed, 4 skipped; Tests 22382 passed, 26 skipped. - `pnpm --filter @objectstack/metadata-protocol typecheck`: exit 0. - `packages/objectql`: `environment-overlay-email-locale-restart.test.ts`, `protocol-packageless-overlay-migration.test.ts`, `environment-overlay-five-types.test.ts`: 3 files, 106 tests passed (they read the rebuilt `dist/`). - `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack`: 60 derived families, each run, all `exit 0`; `--ran` reconciliation: "60 derived famil(ies) accounted for — 60 run, 0 NOT-MEASURED". - `pnpm lint`: exit 0. - Not run locally, left to CI: the dogfood battery (no `!`, no `(narrowing)` arm), `test:repo`, the UI lanes. --- _Generated by [Claude Code](https://claude.ai/code/session_01Utp6CYeyrEJGAte4R8B7un)_ 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01Utp6CYeyrEJGAte4R8B7un <!-- signed: domain:server#5 · os-bill · session_01Utp6CYVV6fcAQrNTDQRvPb --> --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent cfb602c commit 0837fdc

5 files changed

Lines changed: 159 additions & 40 deletions

File tree

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
---
2+
'@objectstack/metadata-protocol': minor
3+
---
4+
5+
fix(metadata-protocol): an email template at a locale the base does not ship keeps its package-less write on a fresh install; the named-base refusal names `os migrate packageless-overlays`
6+
7+
Clause-②: no
8+
Metadata-protocol: semantic
9+
10+
- **An added locale is writable before the pair exists.** Both overlay-write closes (the package-less write, and the write naming the read-only base with `?package=<base>`) stay open where the fix they would name cannot be taken: an `email_template` member at a locale the base does not ship is refused as a declared override, so its write stays open (ADR-0146 D11). The close asked the override write gate about the pair; on a fresh install the pair is not ensured yet, the gate's closure read answers nothing for a package that does not exist, and the close read that "not concerned" as "admitted". So `PUT /api/v1/meta/email_template/<name>` with a zh-CN body beside a base that ships en-US only answered `403 NOT_OVERRIDABLE`, naming a pair write that was refused too (`declared_package_ships_no_such_item`). The close now judges a pair that is not installed with the closure the ensure door gives it (the base and what the base reaches), so the write is accepted, as it is once the pair exists. A member the base ships is closed exactly as before.
11+
- **The named-base refusal names the ceremony.** `os migrate packageless-overlays` moves a row stored bound to the read-only base into the pair with its declaration. The sentence for a save, publish or rollback naming the base now says such a row "is served until moved by `os migrate packageless-overlays`, overridden by the pair, or deleted naming the base". One spelling, every door; the code and status (`403 NOT_OVERRIDABLE`) are unchanged.

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

Lines changed: 75 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ import {
4646
import { SysMetadataRepository } from './sys-metadata-repository.js';
4747
import { OBJECT_EXTENSION_TYPE } from './protocol-object-extension-item.js';
4848
import { customizationPairEnsureRefusal, type PairRegistry } from './protocol-customization-package.js';
49+
import { resolvePackageClosure } from '@objectstack/lint/runtime';
4950
import { overlayMemberField, overlayMemberOfBody, overlayMemberValue } from './migrations/overlay-index.js';
5051
import {
5152
customizationBasePackageId,
@@ -254,12 +255,12 @@ export function packagelessOverlayWriteRefusal(
254255
closed: { base: string; pair: string },
255256
operation: PackagelessWriteOperation,
256257
): Error {
257-
const { item, shipped, pair, declared, writeInto } = closedOverlayWriteClauses(request, closed);
258-
const rule = 'A write naming no package is closed where `os migrate packageless-overlays` can move its row '
258+
const { item, shipped, pair, declared, writeInto, ceremony } = closedOverlayWriteClauses(request, closed);
259+
const rule = `A write naming no package is closed where ${ceremony} can move its row `
259260
+ `(ADR-0146 D11); a package-less row stored before it is served until that command moves it into '${closed.pair}'.`;
260261
const message = operation === 'rollback'
261262
? `${item} ${shipped}, and a rollback of its package-less row is closed (ADR-0146 D11): run `
262-
+ `\`os migrate packageless-overlays\` to move the row into ${pair}; to restore a version from before `
263+
+ `${ceremony} to move the row into ${pair}; to restore a version from before `
263264
+ `the move, save that version's body there, declaring ${declared}.`
264265
: operation === 'delete'
265266
? `${item} ${shipped}: remove its customization from ${pair}. ${rule}`
@@ -274,6 +275,13 @@ export function packagelessOverlayWriteRefusal(
274275
* the declaration, and the fix — write it into the pair, declaring the
275276
* override. ONE spelling: a writer refused at either address reads the same
276277
* fix, and only the rule clause that follows it says which address is closed.
278+
*
279+
* Two more clauses, one spelling each: `ceremony`, the command that moves a
280+
* stored row into the pair (both sentences name it), and `boundRowServed`,
281+
* what serves a row bound to the base stored before the close until one of
282+
* the three open doors acts on it — [#29392] that command, which moves it
283+
* into the pair with its declaration, the pair's declared override, or a
284+
* delete naming the base.
277285
*/
278286
function closedOverlayWriteClauses(
279287
request: { type: string; name: string },
@@ -286,7 +294,9 @@ function closedOverlayWriteClauses(
286294
const declared = `overrides: { package: '${closed.base}', basedOn }`;
287295
const writeInto = (operation: 'save' | 'publish') =>
288296
`${item} ${shipped}: ${operation === 'publish' ? 'publish' : 'save'} it into ${pair}, declaring ${declared}.`;
289-
return { item, shipped, pair, declared, writeInto };
297+
const ceremony = '`os migrate packageless-overlays`';
298+
const boundRowServed = `is served until moved by ${ceremony}, overridden by the pair, or deleted naming the base.`;
299+
return { item, shipped, pair, declared, writeInto, ceremony, boundRowServed };
290300
}
291301

292302
/** `403 NOT_OVERRIDABLE`, the code and status the sealed write already answers, so neither close gains a code. */
@@ -306,11 +316,11 @@ function closedOverlayWriteError(message: string): Error {
306316
* a row bound to the base. `rollback` is every restore of a stored version:
307317
* the per-item rollback and [#29390] a commit revert's restore limb (and the
308318
* package rollback through it), one sentence for both. ⛔ Not `delete`:
309-
* removing a row bound to the base writes nothing, and it is the one way to
310-
* clear such a row stored before the close — `os migrate packageless-overlays`
311-
* reads package-less rows only, so it never moves one. The removal serves the
312-
* base's own document again, or the pair's declared override where one exists
313-
* (it is served over the row).
319+
* removing a row bound to the base writes nothing, and it is one way to clear
320+
* such a row stored before the close; [#29392] `os migrate packageless-overlays`
321+
* is the other, moving it into the pair with its declaration. The removal
322+
* serves the base's own document again, or the pair's declared override where
323+
* one exists (it is served over the row).
314324
*/
315325
export type NamedBaseWriteOperation = Exclude<PackagelessWriteOperation, 'delete'>;
316326

@@ -322,12 +332,14 @@ export type NamedBaseWriteOperation = Exclude<PackagelessWriteOperation, 'delete
322332
* package itself (`?package=<base>`) onto an item of one of the five overlay
323333
* types that package ships stored the customization as a row bound to the
324334
* base, with no declaration: served at the base's address in place of its
325-
* document, outside the pair D1 puts every customization in, and not seen by
326-
* `os migrate packageless-overlays`. It is refused with the fix the
327-
* package-less close names ({@link closedOverlayWriteClauses}): write it into
328-
* the pair, declaring the override. A row stored that way before the close is
329-
* still served until the pair declares the override (the declared override is
330-
* served over it) or a delete naming the base removes it — both open.
335+
* document, outside the pair D1 puts every customization in. It is refused
336+
* with the fix the package-less close names ({@link closedOverlayWriteClauses}):
337+
* write it into the pair, declaring the override. A row stored that way before
338+
* the close is still served until one of three open doors acts on it, all
339+
* named in the sentence ({@link closedOverlayWriteClauses}' `boundRowServed`):
340+
* [#29392] `os migrate packageless-overlays` moves it into the pair with its
341+
* declaration, the pair declares the override (served over it), or a delete
342+
* naming the base removes it.
331343
*
332344
* Fix first, under the REST door's 500-character bound (`CLIENT_MESSAGE_MAX`,
333345
* `@objectstack/types`), as the package-less sentence.
@@ -337,13 +349,12 @@ export function namedBaseOverlayWriteRefusal(
337349
closed: { base: string; pair: string },
338350
operation: NamedBaseWriteOperation,
339351
): Error {
340-
const { item, shipped, pair, declared, writeInto } = closedOverlayWriteClauses(request, closed);
341-
const served = 'is served until the pair declares the override, or a delete naming the base removes it.';
352+
const { item, shipped, pair, declared, writeInto, boundRowServed } = closedOverlayWriteClauses(request, closed);
342353
const message = operation === 'rollback'
343354
? `${item} ${shipped}, and a rollback of its row bound to the read-only base is closed (ADR-0146 D1, D11): `
344-
+ `save the version's body into ${pair}, declaring ${declared}. The row ${served}`
355+
+ `save the version's body into ${pair}, declaring ${declared}. It ${boundRowServed}`
345356
: `${writeInto(operation)} The read-only base itself takes no customization (ADR-0146 D1, D11): `
346-
+ `a row bound to it from before ${served}`;
357+
+ `a row bound to it ${boundRowServed}`;
347358
return closedOverlayWriteError(message);
348359
}
349360

@@ -356,8 +367,31 @@ export interface OverlayPairFix {
356367
readonly base: string;
357368
readonly pair: string;
358369
readonly basedOn: string;
370+
/**
371+
* The closure the pair WILL have, present only while the pair is not
372+
* installed: the ensure door declares the pair's one dependency, the base
373+
* (`manifest.dependencies: { [base]: range }`), so its closure is the base
374+
* and what the base reaches. The gate's own read
375+
* (`resolveWritePackageScope`) answers nothing for a package the registry
376+
* cannot produce, and its `not_concerned` on a declared body is no verdict
377+
* about the write the fix takes.
378+
*/
379+
readonly pendingPairScope?: WrittenPackageScope;
380+
}
381+
382+
/** A written package and the closure of its declared dependencies, as `resolveWritePackageScope` answers it. */
383+
export interface WrittenPackageScope {
384+
readonly packageId: string;
385+
readonly dependencies: readonly string[];
359386
}
360387

388+
/**
389+
* The override write gate as a close asks it: {@link overrideWriteVerdict},
390+
* with the written package's closure when the caller holds it (a pair the
391+
* ensure step would create, {@link OverlayPairFix.pendingPairScope}).
392+
*/
393+
export type OverrideWriteVerdictAsk = (request: OverrideWriteSubject, writtenScope?: WrittenPackageScope) => OverrideWriteVerdict;
394+
361395
/**
362396
* [ADR-0146 D1, D11] The registry half of whether the fix both overlay-write
363397
* closes name — write it into `base`'s pair, declaring the override — can be
@@ -390,7 +424,11 @@ export function overlayPairFixOf(
390424
if (customizationPairEnsureRefusal(host.engine, registry, base, pair) !== undefined) return null; // the ensure door would refuse
391425
const basedOn = registry.getPackage(base)?.manifest?.version;
392426
if (typeof basedOn !== 'string' || basedOn === '') return null; // no `basedOn` to declare
393-
return { base, pair, basedOn };
427+
if (registry.getPackage(pair)) return { base, pair, basedOn };
428+
// The pair the ensure door would create: one dependency, the base.
429+
const baseClosure = resolvePackageClosure(base, (id) => registry.getPackage(id) as { manifest?: unknown } | undefined);
430+
const dependencies = [base, ...(baseClosure?.dependencies ?? []).filter((id) => id !== pair)];
431+
return { base, pair, basedOn, pendingPairScope: { packageId: pair, dependencies } };
394432
}
395433

396434
/**
@@ -400,9 +438,14 @@ export function overlayPairFixOf(
400438
* identity at `member`, declaring `overrides: { package: base, basedOn }`. An
401439
* `email_template` member at a locale the base does not ship is refused
402440
* there, so its write stays open.
441+
*
442+
* The gate is asked about the pair the fix writes into, installed or not: a
443+
* pair not yet ensured (a fresh install) is judged with the closure the
444+
* ensure door gives it ({@link OverlayPairFix.pendingPairScope}), so the
445+
* answer is the one the write meets once the pair exists.
403446
*/
404447
export function overlayPairFixAdmits(
405-
host: { readonly overrideWriteVerdict: (request: OverrideWriteSubject) => OverrideWriteVerdict },
448+
host: { readonly overrideWriteVerdict: OverrideWriteVerdictAsk },
406449
fix: OverlayPairFix,
407450
subject: {
408451
/** The singular type. */
@@ -419,7 +462,7 @@ export function overlayPairFixAdmits(
419462
...(body ?? { name, ...(disc && member !== null ? { [disc]: member } : {}) }),
420463
overrides: { package: fix.base, basedOn: fix.basedOn },
421464
};
422-
const verdict = host.overrideWriteVerdict({ type, name, packageId: fix.pair, item: declaredBody });
465+
const verdict = host.overrideWriteVerdict({ type, name, packageId: fix.pair, item: declaredBody }, fix.pendingPairScope);
423466
return verdict.verdict !== 'refuse';
424467
}
425468

@@ -429,7 +472,7 @@ export interface NamedBaseCloseHost {
429472
/** The read-only code packages that ship a name of an overlay type ([] for any other type). */
430473
readonly overlayBaseShippers: (type: string, name: string) => string[];
431474
/** The override write gate, as the declared write into the pair meets it at the save door. */
432-
readonly overrideWriteVerdict: (request: OverrideWriteSubject) => OverrideWriteVerdict;
475+
readonly overrideWriteVerdict: OverrideWriteVerdictAsk;
433476
}
434477

435478
/** The write naming a package that {@link namedBaseCloseSubject} judges. */
@@ -953,17 +996,25 @@ export function overrideWriteRefusalError(
953996
* member ({@link overrideWriteMemberOf}), and an artifact of another member is
954997
* no shipper: a registry that ignores `member` can answer one, as
955998
* `mergeArtifactProtection` guards on the read side.
999+
*
1000+
* `writtenScope`, when given for the written package, is its closure in
1001+
* place of the registry's read: a close asking about a pair the ensure step
1002+
* would create ({@link OverlayPairFix.pendingPairScope}). Every door's own
1003+
* write passes none.
9561004
*/
9571005
export function overrideWriteVerdict(
9581006
this: Pick<PackagedBaseRefusalHost, 'isWritablePackage' | 'resolveWritePackageScope' | 'shippedArtifactsOf'>,
9591007
request: OverrideWriteSubject,
1008+
writtenScope?: WrittenPackageScope,
9601009
): OverrideWriteVerdict {
9611010
const packageId = typeof request.packageId === 'string' && request.packageId.length > 0
9621011
? request.packageId
9631012
: undefined;
9641013
const declaration = declarationOf(request.item);
9651014
if (packageId === undefined && !declaration.present) return NOT_CONCERNED;
966-
const scope = packageId === undefined ? undefined : this.resolveWritePackageScope(packageId);
1015+
const scope = packageId === undefined
1016+
? undefined
1017+
: writtenScope?.packageId === packageId ? writtenScope : this.resolveWritePackageScope(packageId);
9671018
// A named package the registry cannot produce is not this gate's to
9681019
// judge: both doors ask `uninstalledNamedBaseRefusal`, which refuses a
9691020
// write naming an uninstalled base (`WRITABLE_PACKAGE_REQUIRED`) — the

‎packages/metadata-protocol/src/protocol-packageless-overlay-migration.ts‎

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -137,8 +137,7 @@ import {
137137
overlayPairFixAdmits,
138138
overlayPairFixOf,
139139
overrideWriteMemberOf,
140-
type OverrideWriteSubject,
141-
type OverrideWriteVerdict,
140+
type OverrideWriteVerdictAsk,
142141
} from './protocol-packaged-base-refusal.js';
143142
import { readOnlyShippingPackages, shippingPackageIds } from './protocol-environment-rows.js';
144143
import { isServedOverrideRowOf } from './served-override.js';
@@ -689,8 +688,12 @@ export interface PackagelessCloseHost {
689688
readonly rethrowUnlessMetadataStoreUnprovisioned: ObjectStackProtocolImplementation['rethrowUnlessMetadataStoreUnprovisioned'];
690689
/** The read-only code packages that ship a name of an overlay type ([] for any other type). */
691690
readonly overlayBaseShippers: (type: string, name: string) => string[];
692-
/** The override write gate, as the ceremony's step 2 meets it through the save door. */
693-
readonly overrideWriteVerdict: (request: OverrideWriteSubject) => OverrideWriteVerdict;
691+
/**
692+
* The override write gate, as the ceremony's step 2 meets it through the
693+
* save door: on a pair step 1 has not ensured yet, with the closure the
694+
* ensure door gives it (`OverlayPairFix.pendingPairScope`).
695+
*/
696+
readonly overrideWriteVerdict: OverrideWriteVerdictAsk;
694697
}
695698

696699
/** The package-less write {@link packagelessCloseSubject} judges, and the row it would write. */

0 commit comments

Comments
 (0)