Skip to content

Commit a19759a

Browse files
fix(metadata-protocol): os migrate packageless-overlays moves an email template row of a locale the base does not ship as the pair's own member (#29258)
<!-- claude-projects-attribution: {"github_login":"os-steve"} --> _Requested by **Steve** · [project thread](https://claude.ai/code/project/chan_01FdL1XeUyvVNxLreh3F4Qi9?thread=cmsg_01FdL1XeUyvVNxLreh3F4Qi9Wh1GxXkF35pyjEZvT2LLas)_ Fixes #28904 Clause-②: no Metadata-protocol: none ## 维护者速读 - **改了什么**:`os migrate packageless-overlays` 现在能迁移「基础包不发运该语言」的旧版无包邮件模板行(例如只发运 `en-US` 的模板的 `zh-CN` 翻译):把它作为配对包 `BASE.custom` 自己的该语言成员写入,不带 `overrides` 声明,回读确认后再删除无包行,结果报 `moved` 并注明 `addedMember`。 - **为什么改**:以前仪式给这类行加上 `overrides` 再存入配对包,被 #28777 的写入闸门拒绝(基础包不发运该语言),结果 `failed`、行原地不动;客户已有的翻译邮件迁不走。 - **风险与回滚**:低。只改迁移仪式的分支和 CLI 的展示文字;写入闸门、读取侧、关闭无包写入的判定都不变。基础包发运的语言照旧按声明式覆盖迁移。回滚即撤销本 PR。 - **先看**:`packages/metadata-protocol/src/protocol-packageless-overlay-migration.ts` 的 `addedMemberOf` 与迁移循环第 2、3 步的 `[#28904]` 分支;`packages/cli/src/commands/migrate/packageless-overlays.ts` 的预览与报告两行。 - **两分钟验证**:`pnpm --filter @objectstack/metadata-protocol build && pnpm --filter @objectstack/objectql exec vitest run src/environment-overlay-email-locale-restart.test.ts -t 28904`,预期 5/5 通过(main 上 2 红)。 - **需要你做的**:无,席位落地。 ## What changes An `email_template` is one item per `(name, locale)` (`ITEM_KEY_DISCRIMINATORS`). The ceremony classified a package-less row against the packages that ship its NAME, so a `zh-CN` row of a template the base ships in `en-US` only was `movable`, and the move saved it into the pair declaring `overrides: { package: BASE, basedOn }`. The override write gate counts shippers at the bundle member and refuses that declaration (`declared_package_ships_no_such_item`, #28777), so the outcome was `failed` and the row stayed. `packages/metadata-protocol/src/protocol-packageless-overlay-migration.ts`: - `addedMemberOf` (private): the bundle member a row's body is, when the base ships the name but not that member, counted as `overrideWriteVerdict` counts a shipper (the artifacts at that member, less any another member answers, bound to the base). `undefined` for a name-identified type and for a shipped member. - The census sets `addedMember` on such a `movable` row (new optional field of `PackagelessOverlayRow`). The disposition vocabulary is unchanged. - The move, for a row with `addedMember`: step 2 saves the body into the pair with NO `overrides` (a stored `overrides` naming the base is dropped), so no `basedOn` is needed or recorded and a base without a version does not fail it; step 3 reads back the pair's row at that member and requires it to declare nothing (`isOwnMemberRow`); step 4 deletes the unbound row as before, its history message naming the member. Outcome: `{ outcome: 'moved', packageId: PAIR, addedMember }` (a new arm of `PackagelessOverlayOutcome`, no new outcome word). - Every other row moves exactly as before. - The close (`packagelessCloseSubject`) is unchanged: it judges the declared-override move only, so an unshipped member's package-less write stays open, as on `main`. Its docblock now says why. `packages/cli/src/commands/migrate/packageless-overlays.ts`: the preview lists such a row as `→ PAIR (its own member; BASE ships none at zh-CN)` instead of `(overrides BASE)`, the `--apply` line reads `moved into PAIR (as its own member zh-CN)` instead of `(basedOn …)`, and the `--json` row carries `addedMember` and omits `basedOn` for it. This file is in the claim's surface conditionally ("only if the preview needs a new outcome word"); no word was added, but without this edit the preview and report would print an `overrides` / `basedOn` the move does not record. ## Tests `packages/objectql/src/environment-overlay-email-locale-restart.test.ts`, new describe `[#28904] …` (5 cases, the file's real engine, protocol and in-memory driver; the package ships `acme.digest` in `en-US` only): 1. an unshipped locale moves into the pair as its own member with no `overrides`; subject, `bodyHtml`, `bodyText`, `replyTo` and `fromOverride` are unchanged on the execution list the send path reads and on the by-name read, at once and after a restart; the base's `en-US` member is untouched; a re-run moves 0; 2. an unshipped-locale row whose stored body declares the base: the declaration is dropped on the move; 3. READ BACK FIRST: a save that writes nothing keeps the unbound row and reports `failed`; 4. CONTROL: a shipped locale (`acme.welcome` `zh-CN`) still moves as the declared override with `basedOn`; 5. the #28777 refusal is unchanged: after the move, a save into the pair DECLARING an override of the unshipped locale answers `403 NOT_OVERRIDABLE`. Forced test edits: none. Two docblocks in the same file that said the ceremony cannot move an unshipped member (the file header and `resetZh`) now say it moves it as the pair's own member and its package-less reset stays open; comment-only. <details><summary>Red on main, green on the fix</summary> Red: `metadata-protocol` built from `main`'s source (the fix held out of the tree), cases 1 and 2 red (`os-verify-lock` VERDICT command-exit 1, 2 failed / 2 passed): the census had no `addedMember`, and the move answered `failed` where `moved` was expected. Green at `505a5ab7` and again at `c62af5d2`: 5/5; the three ceremony suites (`environment-overlay-email-locale-restart`, `protocol-packageless-overlay-migration`, `environment-overlay-five-types`) 87/87. </details> <details><summary>Ablations (one per pinned behaviour; each leg through ablation-replace.mjs, metadata-protocol rebuilt, ablation-dist-preflight.mjs marker present, then restored to the HEAD blob and preflight --absent)</summary> - A1, census `addedMember` ignored by the move (the old declared-override save): cases 1, 2, 3 red; case 1's outcome became `failed` with `… declares an override of 'com.acme.mail', which ships no email_template named 'acme.digest' with locale 'zh-CN' …`. - A2, keep a stored `overrides` on the member save: case 2 red (`failed` where `moved`). - A3, member read-back always accepts: case 3 red (`moved` where `failed`). - A4, the gate's `declared_package_ships_no_such_item` branch disabled (`protocol-packaged-base-refusal.ts`, mutated and restored, not in the diff): case 5 red (`expected undefined to match object { code, status }`). Every restore: `git hash-object` equals the HEAD blob, `git diff HEAD` empty, marker absent from `dist/`. </details> ## Acceptance notes - **The close stays as on `main`.** #28576 ruling 2 closes a package-less write where the ceremony would move the row and the move can succeed. The close still reads the declared-override move only, so it keeps the unshipped member's package-less write open, now while the ceremony can move that row. Closing it too would refuse a write accepted today (a `(narrowing)` arm), needs refusal text in `protocol-packaged-base-refusal.ts` (next on this seat, #28905) that does not tell the author to declare `overrides`, and changes the spec registry entry's prose. Raised in the report's `open_questions`, not done here. - **The spec migration entry's prose** (`18.packageless-overlay-write-closed`, `replacement`) still lists "an email template locale the base does not ship" among the `failed` moves and says each `movable` row is moved declaring `overrides`. After this PR such a row moves as the pair's own member. `packages/spec/**` is out of this card's surface; reported to the seat for the spec lane. - **#28780's row kind** (a row already IN the pair declaring an override of an unshipped locale, stored before #28777) is not touched. This move takes the same reading as #28780's "drop `overrides`" option: a member the base does not ship enters the pair declaring nothing. - `ObjectStackProtocolImplementation#migratePackagelessOverlays`'s docblock in `protocol.ts` still says "as a declared override". The module header is the stated authority and is updated; `protocol.ts` is outside the claim's surface. - Version history: the move carries what it carried before (a save into the pair and the unbound row's delete tombstone); no history is copied. ## Verification All at head `2ebd0841` unless noted (the last commit after the package runs is comment-only in a test file). - `@objectstack/metadata-protocol` typecheck exit 0; test 300 files passed, 4 skipped, 22100 tests passed (at `d485bebc`). - `@objectstack/objectql` typecheck exit 0; test 527 files, 10143 tests passed (at `d485bebc`). - `@objectstack/cli` typecheck exit 0; `vitest run --project unit` 372 files, 5250 tests passed (at `d485bebc`). The integration tier is declared to CI. - `eslint --no-inline-config` over `packages/metadata-protocol/src`, `packages/cli/src/commands/migrate` and the test file: exit 0, no findings. - `dispatch-gates --commands` derived 62 families; 62 run; 59 exit 0 at first run, 3 exit 3 on prerequisites and re-run exit 0 on the same head (`check-plugin-teardown-shape --self-test` after fetching its pinned fixture commit; `check:dual-build-cjs-loads` and `check:i18n-coverage` once the unbuilt packages had dist). `--ran` with exit codes: "62 derived, 62 run, 0 NOT-MEASURED, 0 UNRUN", a derived zero. - Full dogfood battery: n/a (no `!` in the title, no `(narrowing)` arm). - `origin/main` merged once before opening (`79774af3`). Changeset `.changeset/28904-packageless-overlays-added-member.md`: `@objectstack/metadata-protocol` and `@objectstack/cli` patch, both declaration lines. --- _Generated by [Claude Code](https://claude.ai/code/session_01ADHkmS3jJf5LgjHTU7Cs6W)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 31826d3 commit a19759a

4 files changed

Lines changed: 240 additions & 22 deletions

File tree

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
---
2+
'@objectstack/metadata-protocol': patch
3+
'@objectstack/cli': patch
4+
---
5+
6+
fix(metadata-protocol): `os migrate packageless-overlays` moves a package-less email template row of a locale the base package does not ship into the base's `.custom` pair as the pair's own member
7+
8+
Clause-②: no
9+
10+
Metadata-protocol: none
11+
12+
A Setup translation stored bound to no package before ADR-0146 D11, of a template the installed package ships in other locales only (a `zh-CN` row of a template shipped `en-US` only), was saved into the pair declaring `overrides`, which the override write gate refuses for a locale the base does not ship. The move failed and the row was kept. The ceremony now saves such a row into the pair with no `overrides` declaration, as the pair's own member at that locale (the base's address serves it as the pair's addition), reads it back and only then deletes the unbound row. Its subject, bodies and envelope columns move unchanged, and it keeps sending in that locale. A row of a locale the base ships moves as the declared override, as before; a declared override of a locale the base does not ship is still refused.
13+
14+
The preview names such a row's locale (`addedMember` in `--json`, "its own member" in the listing), and `--apply` reports it `moved` with that `addedMember` in place of a `basedOn`, since the pair's own member records none.

‎packages/cli/src/commands/migrate/packageless-overlays.ts‎

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -93,8 +93,11 @@ function jsonRow(row: CensusRow, outcome?: Outcome): Record<string, unknown> {
9393
...(row.base !== undefined ? { base: row.base } : {}),
9494
...(row.pair !== undefined ? { pair: row.pair } : {}),
9595
...(row.shadowDeclares !== undefined ? { shadowDeclares: row.shadowDeclares } : {}),
96+
...(row.addedMember !== undefined ? { addedMember: row.addedMember } : {}),
9697
outcome: outcome?.outcome ?? (row.disposition === 'movable' ? 'listed' : 'left'),
97-
...(outcome && outcome.outcome !== 'failed' ? { packageId: outcome.packageId, basedOn: outcome.basedOn } : {}),
98+
...(outcome && outcome.outcome !== 'failed'
99+
? { packageId: outcome.packageId, ...(outcome.basedOn !== undefined ? { basedOn: outcome.basedOn } : {}) }
100+
: {}),
98101
...(outcome?.outcome === 'failed' ? { error: outcome.error } : {}),
99102
};
100103
}
@@ -376,7 +379,12 @@ export default class MigratePackagelessOverlays extends Command {
376379
if (!apply) {
377380
if (movable.length > 0) {
378381
console.log(chalk.bold(`${movable.length} row(s) to move:`));
379-
for (const row of movable) console.log(` • ${itemOf(row)} → ${row.pair} (overrides ${row.base})`);
382+
for (const row of movable) {
383+
// [#28904] A member the base does not ship moves in as the pair's own, declaring nothing.
384+
console.log(` • ${itemOf(row)} → ${row.pair} ${row.addedMember !== undefined
385+
? `(its own member; ${row.base} ships none at ${row.addedMember})`
386+
: `(overrides ${row.base})`}`);
387+
}
380388
}
381389
if (result.incompleteDeclarations.length > 0) {
382390
console.log(chalk.bold(`${result.incompleteDeclarations.length} declared override(s) missing basedOn:`));
@@ -395,7 +403,8 @@ export default class MigratePackagelessOverlays extends Command {
395403
console.log(` ${chalk.red('✗')} ${itemOf(row)} — ${outcome.error}`);
396404
} else {
397405
const verb = outcome.outcome === 'moved' ? 'moved into' : 'basedOn recorded in';
398-
console.log(` ${chalk.green('✓')} ${itemOf(row)} ${verb} ${outcome.packageId} ${chalk.dim(`(basedOn ${outcome.basedOn})`)}`);
406+
const detail = outcome.addedMember !== undefined ? `as its own member ${outcome.addedMember}` : `basedOn ${outcome.basedOn}`;
407+
console.log(` ${chalk.green('✓')} ${itemOf(row)} ${verb} ${outcome.packageId} ${chalk.dim(`(${detail})`)}`);
399408
}
400409
}
401410
console.log('');

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

Lines changed: 89 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,23 @@
6767
* `failed` outcome; the next row is still tried. A second run finds nothing to
6868
* move (idempotent).
6969
*
70+
* ## …a bundle member the base does not ship (#28904)
71+
*
72+
* For a type whose identity is a pair (`ITEM_KEY_DISCRIMINATORS`: an
73+
* `email_template` is one item per `(name, locale)`), the base may ship the
74+
* name and not the row's member — a `zh-CN` translation of a template shipped
75+
* `en-US` only. Such a row overrides nothing the base ships: the pair holds it
76+
* as its OWN member, declaring nothing (ADR-0146 D2; a declared override of it
77+
* is refused, `declared_package_ships_no_such_item`, #28777), and the base's
78+
* address serves it as the pair's addition (#28802). The census names that
79+
* member on the `movable` row (`addedMember`), from the shipped artifacts at
80+
* that member as the write gate counts them ({@link addedMemberOf}), and
81+
* the move differs in steps 2 and 3 only: the body is saved into the pair
82+
* WITHOUT `overrides` (one it carries naming the base is dropped), so no
83+
* `basedOn` is recorded and none is needed; the pair's row read back must be
84+
* that member's own row, declaring nothing. Its outcome is `moved`, naming
85+
* the `addedMember` in place of a `basedOn`.
86+
*
7087
* ## …and the stored declarations missing `basedOn`
7188
*
7289
* A row of a pair whose body declares `overrides: { package: <base> }` with no
@@ -91,8 +108,8 @@ import { storedRowDocument } from './item-lock.js';
91108
import { OVERLAY_MEMBER_COLUMN, overlayMemberField, overlayMemberOfBody, overlayMemberValue } from './migrations/overlay-index.js';
92109
import type { ObjectStackProtocolImplementation } from './protocol.js';
93110
import { customizationPairEnsureRefusal, type PairRegistry } from './protocol-customization-package.js';
94-
import type { OverrideWriteSubject, OverrideWriteVerdict } from './protocol-packaged-base-refusal.js';
95-
import { readOnlyShippingPackages } from './protocol-environment-rows.js';
111+
import { overrideWriteMemberOf, type OverrideWriteSubject, type OverrideWriteVerdict } from './protocol-packaged-base-refusal.js';
112+
import { readOnlyShippingPackages, shippingPackageIds } from './protocol-environment-rows.js';
96113
import { isServedOverrideRowOf } from './served-override.js';
97114
import { SysMetadataRepository } from './sys-metadata-repository.js';
98115

@@ -138,6 +155,13 @@ export interface PackagelessOverlayRow {
138155
readonly pair?: string;
139156
/** `shadowed` only: whether the pair's row declares the override of the base. */
140157
readonly shadowDeclares?: boolean;
158+
/**
159+
* [#28904] `movable` only, for a type whose identity is a pair: the bundle
160+
* member (the row's discriminator value, e.g. its locale) the base does
161+
* not ship. The row moves in as the pair's own member at it, declaring no
162+
* override (module header, "a bundle member the base does not ship").
163+
*/
164+
readonly addedMember?: string;
141165
}
142166

143167
/** One pair row whose declaration lacks `basedOn` (module header, last section). */
@@ -164,7 +188,9 @@ export interface PackagelessOverlayCensus {
164188

165189
/** What {@link migratePackagelessOverlays} did with one movable row or one incomplete declaration. */
166190
export type PackagelessOverlayOutcome =
167-
| { readonly id: string; readonly outcome: 'moved' | 'declaration_completed'; readonly packageId: string; readonly basedOn: string }
191+
| { readonly id: string; readonly outcome: 'moved' | 'declaration_completed'; readonly packageId: string; readonly basedOn: string; readonly addedMember?: undefined }
192+
/** [#28904] Moved as the pair's own member the base does not ship: no declaration, so no `basedOn`. */
193+
| { readonly id: string; readonly outcome: 'moved'; readonly packageId: string; readonly addedMember: string; readonly basedOn?: undefined }
168194
| { readonly id: string; readonly outcome: 'failed'; readonly error: string };
169195

170196
/** One base's line of the report. */
@@ -260,6 +286,28 @@ function baseVersionOf(host: Pick<PackagelessOverlayMigrationHost, 'engine'>, ba
260286
return typeof version === 'string' && version !== '' ? version : undefined;
261287
}
262288

289+
/**
290+
* [#28904, ADR-0146 D2] The bundle member `body` is, when `base` ships the
291+
* name `name` but not that member — the member the row adds to the pair; `undefined` for a type whose identity is
292+
* its name alone, and for a member `base` ships. Counted as the override
293+
* write gate counts a shipper (`overrideWriteVerdict`): the artifacts at that
294+
* member, less any another member answers (a registry that ignores `member`
295+
* can answer one), bound to `base` — ⛔ never a second reading.
296+
*/
297+
function addedMemberOf(
298+
host: Pick<PackagelessOverlayMigrationHost, 'shippedArtifactsOf'>,
299+
type: string,
300+
name: string,
301+
base: string,
302+
body: unknown,
303+
): string | undefined {
304+
const member = overrideWriteMemberOf(type, body);
305+
if (member === undefined) return undefined;
306+
const atMember = host.shippedArtifactsOf(type, name, undefined, member)
307+
.filter((artifact) => overrideWriteMemberOf(type, artifact) === member);
308+
return shippingPackageIds(atMember).has(base) ? undefined : member;
309+
}
310+
263311
/**
264312
* [#28576] How the census classifies ONE package-less row of a name some
265313
* read-only code package ships (module header, "The population"), from the
@@ -345,8 +393,12 @@ export async function listPackagelessOverlays(
345393
...(member !== undefined ? { member } : {}),
346394
shippers,
347395
};
348-
const classified = classifyPackagelessRow(shippers, bodyOf(row), (pair) => shadowOf(pair, type, name, member, state));
349-
listed.push({ ...common, ...classified });
396+
const body = bodyOf(row);
397+
const classified = classifyPackagelessRow(shippers, body, (pair) => shadowOf(pair, type, name, member, state));
398+
const addedMember = classified.disposition === 'movable' && classified.base !== undefined
399+
? addedMemberOf(this, type, name, classified.base, body)
400+
: undefined;
401+
listed.push({ ...common, ...classified, ...(addedMember !== undefined ? { addedMember } : {}) });
350402
}
351403

352404
// Every type a declaration applies to, not only the five the move acts on.
@@ -393,6 +445,12 @@ async function storedPairRow(
393445
return (rows ?? []).find((row) => memberOf(row) === item.member);
394446
}
395447

448+
/** [#28904] Whether a pair's stored row is its own item: a document that declares no override. */
449+
function isOwnMemberRow(row: unknown): boolean {
450+
const body = row === undefined ? undefined : bodyOf(row);
451+
return body !== undefined && !('overrides' in body);
452+
}
453+
396454
/**
397455
* Move every `movable` row of the census into its base's pair, and complete
398456
* every declaration missing `basedOn` (module header, "The move"). One outcome
@@ -451,25 +509,33 @@ export async function migratePackagelessOverlays(
451509
};
452510
const ensuredPair = await ensure(row.base, row.pair);
453511
if (!ensuredPair.ok) { fail(ensuredPair.error); continue; }
454-
const basedOn = baseVersionOf(this, row.base);
455-
if (basedOn === undefined) { fail(`The installed package '${row.base}' declares no version, so no basedOn can be recorded.`); continue; }
512+
// [#28904] A member the base does not ship is the pair's own: no
513+
// declaration, so no `basedOn` to record.
514+
const { addedMember } = row;
515+
const basedOn = addedMember === undefined ? baseVersionOf(this, row.base) : undefined;
516+
if (addedMember === undefined && basedOn === undefined) { fail(`The installed package '${row.base}' declares no version, so no basedOn can be recorded.`); continue; }
456517
const stored = rowsById.get(row.id);
457518
const body = stored === undefined ? undefined : bodyOf(stored);
458519
if (body === undefined) { fail('The stored body could not be read as a document.'); continue; }
459520
try {
460-
// Step 2: the declared body, saved into the pair through the save door.
521+
// Step 2: the declared body — or, for an added member, the body
522+
// with no `overrides` — saved into the pair through the save door.
523+
const { overrides: _declared, ...undeclared } = body;
461524
await this.saveMetaItem({
462525
type: row.type,
463526
name: row.name,
464-
item: { ...body, overrides: { package: row.base, basedOn } },
527+
item: addedMember === undefined ? { ...body, overrides: { package: row.base, basedOn } } : undeclared,
465528
packageId: row.pair,
466529
mode: row.state === 'draft' ? 'draft' : 'publish',
467530
actor: request.actor,
468531
} as never);
469-
// Step 3: read back — the pair's row must be the served override.
532+
// Step 3: read back — the pair's row must be the served override,
533+
// or, for an added member, that member's own row declaring nothing.
470534
const written = await storedPairRow(this, row, row.pair);
471-
if (!isServedOverrideRowOf(written, row.base)) {
472-
fail(`The row saved into '${row.pair}' does not read back as the declared override of '${row.base}'; the unbound row is kept.`);
535+
if (addedMember === undefined ? !isServedOverrideRowOf(written, row.base) : !isOwnMemberRow(written)) {
536+
fail(addedMember === undefined
537+
? `The row saved into '${row.pair}' does not read back as the declared override of '${row.base}'; the unbound row is kept.`
538+
: `The row saved into '${row.pair}' does not read back as its own member '${addedMember}', declaring no override; the unbound row is kept.`);
473539
continue;
474540
}
475541
// Step 4: only now, the unbound row, by its own address.
@@ -479,15 +545,19 @@ export async function migratePackagelessOverlays(
479545
parentVersion: (typeof stored.checksum === 'string' ? stored.checksum : null) as string,
480546
actor: request.actor,
481547
source: request.actor,
482-
message: `Moved into '${row.pair}' as a declared override of '${row.base}' (ADR-0146 D11)`,
548+
message: addedMember === undefined
549+
? `Moved into '${row.pair}' as a declared override of '${row.base}' (ADR-0146 D11)`
550+
: `Moved into '${row.pair}' as its own member '${addedMember}', which '${row.base}' does not ship (ADR-0146 D2, D11)`,
483551
intent: 'override-artifact',
484552
state: row.state,
485553
packageId: null,
486554
...(row.member !== undefined ? { member: row.member } : {}),
487555
} as never,
488556
);
489557
line.moved += 1;
490-
outcomes.push({ id: row.id, outcome: 'moved', packageId: row.pair, basedOn });
558+
outcomes.push(addedMember === undefined
559+
? { id: row.id, outcome: 'moved', packageId: row.pair, basedOn: basedOn! }
560+
: { id: row.id, outcome: 'moved', packageId: row.pair, addedMember });
491561
} catch (error) {
492562
fail(messageOf(error));
493563
}
@@ -584,8 +654,11 @@ export interface PackagelessWriteSubject {
584654
* takes: the ensure door would ensure the pair
585655
* ({@link customizationPairEnsureRefusal}), the base records a version
586656
* (`basedOn`), and the override write gate admits the declared body into
587-
* the pair (an `email_template` member at a locale the base does not ship
588-
* is refused there, #28777, and stays open: #28904).
657+
* the pair. An `email_template` member at a locale the base does not ship
658+
* is refused there (#28777) and stays open: the ceremony moves such a row
659+
* as the pair's own member, declaring nothing (#28904), and the close
660+
* reaches only the declared-override moves, so its package-less write is
661+
* the one it had before the close.
589662
*
590663
* Registry reads first; the one store read (the item's rows) only for a name
591664
* the registry has not already left open.

0 commit comments

Comments
 (0)