Skip to content

Commit 91f6a0b

Browse files
fix(metadata-protocol): os migrate packageless-overlays also moves overlay rows bound to the read-only base itself into its .custom pair (#29477)
<!-- claude-projects-attribution: {"github_login":"os-steve"} --> _Requested by **Steve** · [project thread](https://claude.ai/code/project/chan_01FdL1XeUyvVNxLreh3F4Qi9?thread=cmsg_01FdL1XeUyvVNxLreh3F4Qi9RJBhhH7Lot8tR4dpMhoecu)_ Fixes #29392 Clause-②: no Metadata-protocol: none ## 维护者速读 - 改了什么:`os migrate packageless-overlays` 的盘点现在也列出「绑定在只读基础包本身」的视图、仪表盘、报表、翻译、邮件模板行(#28905 关门前用 `?package=基础包` 保存留下的),`--apply` 把它们像无包行一样搬进 `.custom` 配对包并声明 `overrides`(或作为配对包自己的语言成员),结果词不变。 - 为什么改:这类行一直盖住基础包文档,但盘点只读无包行看不到它们,唯一出路是删除,删除会丢掉定制。 - 风险与回滚:只多搬行、不拒绝任何写入;同一项既有无包行又有绑定行时先搬无包行,绑定行保留并报 failed,下次预览列为 shadowed,绝不覆盖。回滚即撤销本 PR。 - 先看:`packages/metadata-protocol/src/protocol-packageless-overlay-migration.ts` 的 `listPackagelessOverlays`(`binding` 判定)与 `migratePackagelessOverlays`(先后顺序与 `movedFrom` 防覆盖);`packages/cli/src/commands/migrate/packageless-overlays.ts` 的渲染。 - 两分钟验证:`pnpm --filter @objectstack/objectql exec vitest run src/protocol-packageless-overlay-migration.test.ts -t 29392`(需先 build metadata-protocol),应见 6 个用例全过。 - 需要你做的:无,席位落地。 ## What changes **The census** (`listPackagelessOverlays`, `protocol-packageless-overlay-migration.ts`). Besides the package-less rows, every row of the five overlay types whose `package_id` is a READ-ONLY code package that ships the name is now in the population: a row bound to the base itself, as a save naming the base stored it before #28905 closed that write. "Read-only package that ships the name" is the reading the #28905 close calls packaged (`overlayBaseShippers` = `readOnlyShippingPackages` over `shippedArtifactsOf`), the same function the census already reads for a package-less row. Such a row: - is classified by the same `classifyPackagelessRow`, with its binding as the one shipper, so a name several packages ship is never `multi_shipped` here (the binding names the base); - carries `boundToBase: true` (a new optional field on the census row and in the cli `--json` rows); - takes `addedMember` exactly as a package-less row does (an email template locale the base does not ship). A row bound to a writable package (the pair included) or to a read-only package that does not ship the name is not listed. Package-less rows are classified exactly as before. **The move** (`migratePackagelessOverlays`). Unchanged steps: ensure the pair, save the declared body (or the undeclared own member) into the pair through the save door, read it back, then delete the source row by its own address, now `packageId: BASE` for a bound row. Same outcomes (`moved` with `basedOn`, or `moved` with `addedMember`); no new outcome word. **One item, two bindings.** A package-less row and a row bound to the base of the same item would both land on the pair's one row. The move takes the package-less rows first; a row of an item this run already moved a row of the other binding into is kept, reported `failed` ("'PAIR' now holds this item … kept beside it, never merged"), and the next preview lists it `shadowed` with that disposition's two ways out. Nothing is overwritten. **The cli** (`packageless-overlays.ts`, render only): the preview names a bound row ("bound to PACKAGE → PAIR"), `--json` rows carry `boundToBase`, and a bound `foreign_declaration` row's remedy names the delete naming the base and the save into the pair (a save naming the base is closed now, so the package-less row's "remove the key in place" remedy does not apply). Help text and three wording spots no longer say "package-less" where the row may be bound. Changeset: `.changeset/29392-packageless-overlays-base-bound.md`, `patch` for `@objectstack/metadata-protocol` and `@objectstack/cli`. ## Tests - `packages/objectql/src/protocol-packageless-overlay-migration.test.ts`, new describe `[#29392]` (6 tests, in-memory topology, rows laid down through the engine as an older release stored them): - the dry run lists active and draft rows bound to the base, `movable`, with base and pair, writing nothing; `--apply` moves both with `overrides: { package: BASE, basedOn: '1.2.0' }` in their own state; the layered read at the base shows the base's document as the code layer and the customization as effective; the five reads serve the customization; a re-run moves 0; - a name two read-only packages ship, bound to one: `movable` into that base's pair, not `multi_shipped`; - a bound `foreign_declaration` and a bound `shadowed` row: listed and left; - a package-less row and a bound row of one item: the package-less one moves, the bound one is kept `failed`, nothing overwritten, next census lists it `shadowed`; - CONTROL: a package-less row still moves as before; rows bound to a writable package and to a read-only package that does not ship the name are not listed. - `packages/objectql/src/environment-overlay-email-locale-restart.test.ts`, one new case in the #28904 describe: a `zh-CN` row of a template the package ships `en-US` only, bound to the package: listed `movable` + `addedMember` + `boundToBase`, moved as the pair's own member with no `overrides`, still sent in `zh-CN` at once and after a restart. - `packages/cli/src/commands/migrate/packageless-overlays.integration.test.ts` (SQLite topology): a second template database with a row bound to the package; the preview lists it and writes nothing; `--apply` moves it with the declaration, keeps the package-less control, a fresh boot serves the customization at all four reads, a re-run moves 0. The seeding moved into one `insertRows` helper (both templates use it); the existing three cases are unchanged. - No forced edits to existing tests. ## Acceptance notes - **The #28905 predicate (measured).** `namedBaseCloseSubject` is not the census's predicate and is not called: its first step (a read-only package among `overlayBaseShippers`) is the population test here, read through the same `readOnlyShippingPackages` the census host already carries. Its remaining steps (pair id, ensure door, base version, the D2 gate admitting a declared body) would, as a census filter, hide again a bound row whose fix cannot be taken, and drop the unshipped-locale rows this card moves as the pair's own member (the gate refuses their declared body, so the close answers `null`). The census keeps its package-less precedent: classify, then let the move's own steps answer `failed` with the reason. The two close predicates are not folded here (#29397). - **Comments now stale in files outside this card's surface** (carrier #29397, which edits them next): `protocol.ts` `namedBaseOverlayWriteRefusal` docblock ("its census reads package-less rows only", "the only way to clear such a row … the ceremony moves package-less rows only") and the `listPackagelessOverlays` docblock ("bound to no package"); `protocol-packaged-base-refusal.ts` `NamedBaseWriteOperation` and refusal docblocks; `protocol.named-base-overlay-write.test.ts` header. - **JSON shape.** `boundToBase` is a new optional field in the census rows and `--json`; additive. ## Verification Red, green and ablations measured on `d74ab15d`..`7ebc8019` (the implementation is unchanged since); suites, typecheck, gates and lint re-run on the final head `30bfafa62` (main merged at `a64416366`), tree `objectstack-ai/objectstack`. <details><summary>Red on main, then green</summary> `protocol-packageless-overlay-migration.ts` restored from `origin/main` 7f277a8 in the worktree, metadata-protocol rebuilt (`boundToBase` in dist: 0), objectql file run: the 5 bound-row pins red, the dry run answering `[]` for the bound rows (`+ []` against the two expected `movable … bound` lines). The CONTROL case was red in that run too, from an expected-order slip in the test itself (`[OTHER, WORKSPACE]` sorted), fixed before the green run; the email bound case was written after. Restored to HEAD, rebuilt (dist hits 3): `protocol-packageless-overlay-migration.test.ts` + `environment-overlay-email-locale-restart.test.ts` 68/68. </details> <details><summary>Ablations (rebuild + dist preflight each leg)</summary> 1. The bound arm dropped (`if (binding !== undefined && !shippers.includes(binding)) continue;` → every bound row skipped), via `scripts/ablation-replace.mjs`, marker in both dist files: objectql 6 failed / 63 passed (the 5 bound-row pins plus the email bound case; CONTROL stays green); cli integration 2 failed / 3 passed (both bound-row cases). Restored: blob == HEAD, `git diff HEAD` empty, rebuilt, `--absent` preflight clean. 2. The collision guard dropped (`movedFrom` check → never true): objectql 1 failed / 68 passed, the two-bindings case (`AssertionError … ['package-less','moved'], …` second outcome no longer `failed`). Restored the same way, `--absent` clean. </details> <details><summary>Suites and typecheck (under os-verify-lock, after a full `./packages/**` + `./examples/**` build)</summary> Final head `30bfafa62`, after `pnpm install --frozen-lockfile` and `turbo run build --filter='./packages/**' --filter='./examples/**'` (VERDICT command-exit 0, held 41m42s for the whole round, shared box): - `pnpm --filter @objectstack/metadata-protocol typecheck` exit 0; `@objectstack/objectql` exit 0; `@objectstack/cli` exit 0 - metadata-protocol `vitest run`: 22161 passed, 26 skipped (22187) - objectql `vitest run`: 10326 passed (10326) - cli `--project unit`: 5267 passed (5267) - cli `--project integration src/commands/migrate/packageless-overlays.integration.test.ts`: 5 passed (5). The rest of the integration tier is declared to CI. </details> <details><summary>Derived gates</summary> `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` on `30bfafa62`: 62 commands, every one exit 0 (the same list as before the merge). `--ran` reconciliation: "62 derived famil(ies) accounted for — 62 run, 0 NOT-MEASURED". `check:type-check-debt` re-measured 0 raw tsc errors, none above its record. `check-plugin-teardown-shape --self-test` needs its pinned fixture commit fetched first (exit 3 before, 48 cases pass after). `pnpm lint` (repo-wide eslint, `--no-inline-config`): exit 0. </details> Dogfood: not owed (no `!`, no narrowing arm). --- _Generated by [Claude Code](https://claude.ai/code/session_01ADHkmQZSg4zWtKMdRnKmKS)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 09abc05 commit 91f6a0b

6 files changed

Lines changed: 353 additions & 36 deletions
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` also moves a view, dashboard, report, translation or email template row bound to the read-only package itself into its `.custom` pair
7+
8+
Clause-②: no
9+
10+
Metadata-protocol: none
11+
12+
Before the write naming a read-only code package itself (`?package=<base>`) was closed, such a save stored the customization as a row bound to that package, declaring nothing, served at the package's address in place of its own document. The ceremony's census read rows bound to no package only, so it never listed such a row, and its one exit was a delete, which drops the customization. The census now lists, beside the package-less rows, every row of the five overlay types bound to a read-only package that ships the name (`boundToBase` in `--json`, "bound to PACKAGE" in the listing), classified the same way with that package as its base. `--apply` moves it into the pair exactly as it moves a package-less row: the declared override with `basedOn` (or, for an email template locale the package does not ship, the pair's own member), read back, and only then the bound row deleted. The outcomes are the same `moved` ones.
13+
14+
A row bound to a writable package (the pair included), or to a read-only package that does not ship the name, is not listed. When one item has both a package-less row and a row bound to the package, the package-less row moves and the bound one is kept and reported failed, never merged over it; the next preview lists it `shadowed`, with that disposition's ways out.

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

Lines changed: 91 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@
2424
* controls, and a FRESH boot of the same database then serves the moved
2525
* content under the base's name on the package-less read, the read naming the
2626
* base, the layered read and the base's list; a second `--apply` moves 0.
27+
*
28+
* [#29392] A second template holds a row bound to the package itself (a save
29+
* naming it, before that write was closed): listed and moved the same way.
2730
*/
2831

2932
import { describe, it, expect, beforeAll, afterAll, beforeEach, afterEach, vi } from 'vitest';
@@ -78,7 +81,7 @@ const viewBody = (name: string, columns: readonly string[]) => ({
7881
config: { type: 'grid', data: { provider: 'object', object: 'm28413_task' }, columns: columns.map((field) => ({ field })) },
7982
});
8083

81-
interface SeedRow { type: string; name: string; state: string; body: Record<string, unknown> }
84+
interface SeedRow { type: string; name: string; state: string; body: Record<string, unknown>; packageId?: string }
8285
const SEED: SeedRow[] = [
8386
{ type: 'view', name: VIEW, state: 'active', body: viewBody(VIEW, STORED_COLUMNS) },
8487
{ type: 'view', name: VIEW, state: 'draft', body: viewBody(VIEW, DRAFT_COLUMNS) },
@@ -89,22 +92,52 @@ const SEED: SeedRow[] = [
8992
{ type: 'permission', name: 'm28413_rep', state: 'draft', body: { name: 'm28413_rep', label: 'Saved in the environment', objects: {} } },
9093
];
9194

95+
/**
96+
* [#29392] A database whose customization a save naming the package itself
97+
* stored before that write was closed: an active row of the package's view
98+
* bound to the package, beside the package-less environment row (control).
99+
*/
100+
const BOUND_SEED: SeedRow[] = [
101+
{ type: 'view', name: VIEW, state: 'active', body: viewBody(VIEW, STORED_COLUMNS), packageId: PKG },
102+
{ type: 'view', name: ENV_VIEW, state: 'active', body: viewBody(ENV_VIEW, STORED_COLUMNS) },
103+
];
104+
92105
let dir: string;
93106
let templateDb: string;
107+
let boundTemplateDb: string;
94108
const savedCwd = process.cwd();
95109
const ENV_KEYS = ['OS_ARTIFACT_PATH', 'OS_DATABASE_URL', 'DATABASE_URL', 'OS_DATABASE_DRIVER', 'OS_HOME', 'NODE_ENV',
96110
'OS_AUTH_SECRET', 'AUTH_SECRET', 'BETTER_AUTH_SECRET', 'OS_ENVIRONMENT_ID'] as const;
97111
const savedEnv: Record<string, string | undefined> = {};
98112

99113
let caseSeq = 0;
100-
function caseDb(): string {
114+
function caseDb(template = templateDb): string {
101115
caseSeq += 1;
102116
const file = join(dir, 'cases', `${caseSeq}.db`);
103117
mkdirSync(dirname(file), { recursive: true });
104-
copyFileSync(templateDb, file);
118+
copyFileSync(template, file);
105119
return file;
106120
}
107121

122+
/** The rows an older release left, written at the driver the way that release stored them. */
123+
async function insertRows(dbFile: string, rows: readonly SeedRow[]): Promise<void> {
124+
const driver = new SqlDriver({ client: 'better-sqlite3', connection: { filename: dbFile }, useNullAsDefault: true });
125+
try {
126+
const k = (driver as any).knex;
127+
const now = new Date().toISOString();
128+
let n = 0;
129+
for (const row of rows) {
130+
n += 1;
131+
await k('sys_metadata').insert({
132+
id: `seed_${n}`, type: row.type, name: row.name, package_id: row.packageId ?? null, state: row.state,
133+
version: 1, checksum: null, created_at: now, updated_at: now, metadata: JSON.stringify(row.body),
134+
});
135+
}
136+
} finally {
137+
await driver.disconnect();
138+
}
139+
}
140+
108141
/** Every `sys_metadata` row, `type/name@binding/state`, read on a connection of our own. */
109142
async function storedRows(dbFile: string): Promise<string[]> {
110143
const driver = new SqlDriver({ client: 'better-sqlite3', connection: { filename: dbFile }, useNullAsDefault: true });
@@ -215,21 +248,10 @@ beforeAll(async () => {
215248
extraPlugins: [new PackageServicePlugin()],
216249
});
217250
await seed.shutdown();
218-
const driver = new SqlDriver({ client: 'better-sqlite3', connection: { filename: templateDb }, useNullAsDefault: true });
219-
try {
220-
const k = (driver as any).knex;
221-
const now = new Date().toISOString();
222-
let n = 0;
223-
for (const row of SEED) {
224-
n += 1;
225-
await k('sys_metadata').insert({
226-
id: `seed_${n}`, type: row.type, name: row.name, package_id: null, state: row.state,
227-
version: 1, checksum: null, created_at: now, updated_at: now, metadata: JSON.stringify(row.body),
228-
});
229-
}
230-
} finally {
231-
await driver.disconnect();
232-
}
251+
boundTemplateDb = join(dir, 'bound-template.db');
252+
copyFileSync(templateDb, boundTemplateDb);
253+
await insertRows(templateDb, SEED);
254+
await insertRows(boundTemplateDb, BOUND_SEED);
233255
}, CASE_TIMEOUT_MS);
234256

235257
afterAll(() => {
@@ -311,3 +333,54 @@ describe('os migrate packageless-overlays', () => {
311333
expect(await storedRows(db)).toEqual(before);
312334
}, CASE_TIMEOUT_MS);
313335
});
336+
337+
/**
338+
* [#29392] A row bound to the read-only package itself, stored by a save
339+
* naming it before that write was closed: the census lists it beside the
340+
* package-less rows (`boundToBase`), and `--apply` moves it into the pair with
341+
* the declaration, the same outcome. Before, the census read package-less
342+
* rows only: the preview listed nothing and the row's one exit was a delete.
343+
*/
344+
describe('os migrate packageless-overlays: a row bound to the read-only package', () => {
345+
it('the preview lists it with its target pair and writes nothing', async () => {
346+
const db = caseDb(boundTemplateDb);
347+
const before = await storedRows(db);
348+
const { payload, exitCode } = await runJson(['--database-url', `file:${db}`, '--json']);
349+
expect(payload.error, JSON.stringify(payload).slice(0, 600)).toBeUndefined();
350+
expect(exitCode).toBe(0);
351+
expect(payload.rows.map((r: any) => `${r.type}/${r.name}/${r.state} ${r.disposition} ${r.base} → ${r.pair} ${r.boundToBase} ${r.outcome}`)).toEqual([
352+
`view/${VIEW}/active movable ${PKG} → ${PAIR} true listed`,
353+
]);
354+
expect(payload.movable).toBe(1);
355+
expect(payload.environmentRows).toBe(1);
356+
expect(await storedRows(db)).toEqual(before);
357+
expect(await storedPackages(db)).not.toContain(PAIR);
358+
}, CASE_TIMEOUT_MS);
359+
360+
it('--apply moves it into the pair with its `overrides` declaration, keeps the control, serves the same customization over the package\'s document, and a re-run moves 0', async () => {
361+
const db = caseDb(boundTemplateDb);
362+
const served = await servedColumns(db);
363+
expect(served[1]).toEqual(STORED_COLUMNS);
364+
365+
const { payload, exitCode } = await runJson(['--apply', '--yes', '--database-url', `file:${db}`, '--json']);
366+
expect(payload.error, JSON.stringify(payload).slice(0, 600)).toBeUndefined();
367+
expect(exitCode).toBe(0);
368+
expect(payload.bases).toEqual([{ base: PKG, pair: PAIR, pairCreated: true, moved: 1, declarationsCompleted: 0, failed: 0, shadowed: 0 }]);
369+
expect(payload.rows.map((r: any) => `${r.state} ${r.outcome} ${r.packageId} ${r.basedOn} ${r.boundToBase}`)).toEqual([
370+
`active moved ${PAIR} 1.4.0 true`,
371+
]);
372+
expect(await storedRows(db)).toEqual([`view/${ENV_VIEW}@-/active`, `view/${VIEW}@${PAIR}/active`]);
373+
expect((await storedBody(db, { type: 'view', name: VIEW, package_id: PAIR, state: 'active' })).overrides)
374+
.toEqual({ package: PKG, basedOn: '1.4.0' });
375+
expect(await storedPackages(db)).toContain(PAIR);
376+
377+
// A fresh boot serves the customization at every read of the package's item.
378+
expect(await servedColumns(db)).toEqual([STORED_COLUMNS, STORED_COLUMNS, STORED_COLUMNS, STORED_COLUMNS]);
379+
380+
const again = await runJson(['--apply', '--yes', '--database-url', `file:${db}`, '--json']);
381+
expect(again.payload.error, JSON.stringify(again.payload).slice(0, 600)).toBeUndefined();
382+
expect(again.exitCode).toBe(0);
383+
expect(again.payload.moved).toBe(0);
384+
expect(again.payload.rows).toEqual([]);
385+
}, CASE_TIMEOUT_MS * 3);
386+
});

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

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,11 @@ function reasonOf(row: CensusRow): string {
6565
return `'${row.base}' has no paired .custom package id, so there is nowhere to move it. Left as it is.`;
6666
case 'foreign_declaration':
6767
return `its body already carries an \`overrides\` key that does not name '${row.base}'. Left as it is: `
68-
+ 'remove the key with a save naming no package, then re-run: the move declares the right override.';
68+
+ (row.boundToBase
69+
// [#29392] A save naming the base is closed, so the key cannot be removed in place.
70+
? `delete it with a delete naming '${row.base}', then save its body into '${row.pair}' declaring `
71+
+ `overrides: { package: '${row.base}', basedOn }.`
72+
: 'remove the key with a save naming no package, then re-run: the move declares the right override.');
6973
case 'shadowed':
7074
return (row.shadowDeclares
7175
? `'${row.pair}' already overrides this item (a customization made in Studio), and that row is the one `
@@ -94,6 +98,7 @@ function jsonRow(row: CensusRow, outcome?: Outcome): Record<string, unknown> {
9498
...(row.pair !== undefined ? { pair: row.pair } : {}),
9599
...(row.shadowDeclares !== undefined ? { shadowDeclares: row.shadowDeclares } : {}),
96100
...(row.addedMember !== undefined ? { addedMember: row.addedMember } : {}),
101+
...(row.boundToBase ? { boundToBase: true } : {}),
97102
outcome: outcome?.outcome ?? (row.disposition === 'movable' ? 'listed' : 'left'),
98103
...(outcome && outcome.outcome !== 'failed'
99104
? { packageId: outcome.packageId, ...(outcome.basedOn !== undefined ? { basedOn: outcome.basedOn } : {}) }
@@ -106,7 +111,9 @@ function jsonRow(row: CensusRow, outcome?: Outcome): Record<string, unknown> {
106111
* `os migrate packageless-overlays` — list, and with `--apply` move, every
107112
* package-less overlay row of a name exactly one installed code package ships
108113
* into that package's paired `<base>.custom`, as a declared override
109-
* (ADR-0146 D11, the migration half of N-16).
114+
* (ADR-0146 D11, the migration half of N-16) — and every overlay row bound to
115+
* a read-only code package that ships its name, which a save naming that
116+
* package stored before such a save was closed (listed `boundToBase`).
110117
*
111118
* ## Why a ceremony
112119
*
@@ -125,7 +132,7 @@ function jsonRow(row: CensusRow, outcome?: Outcome): Record<string, unknown> {
125132
* header holds the rules): the pair is ensured through its one creation door,
126133
* each body is saved into the pair through the save door with the declaration
127134
* (`basedOn` = the installed base's version now), read back as the served
128-
* override, and only then is the package-less row deleted. A name no package
135+
* override, and only then is the moved row deleted. A name no package
129136
* ships is environment metadata and is not touched; a name several packages
130137
* ship, and a row the pair already overrides (a Studio customization made
131138
* first), are listed and left alone. A pair row declaring an override with no
@@ -158,8 +165,8 @@ function jsonRow(row: CensusRow, outcome?: Outcome): Record<string, unknown> {
158165
export default class MigratePackagelessOverlays extends Command {
159166
static override description =
160167
'List, and with --apply move, the package-less view, dashboard, report, translation and email-template rows '
161-
+ 'over a name one installed package ships into that package\'s paired .custom package, as declared overrides '
162-
+ '(ADR-0146 D11). Run once after upgrading.';
168+
+ 'over a name one installed package ships, and those bound to a read-only package that ships the name, into '
169+
+ 'that package\'s paired .custom package, as declared overrides (ADR-0146 D11). Run once after upgrading.';
163170

164171
static override examples = [
165172
'$ os migrate packageless-overlays',
@@ -242,7 +249,7 @@ export default class MigratePackagelessOverlays extends Command {
242249
}
243250
printWarning(
244251
'Apply mode moves the listed sys_metadata rows into each base\'s paired .custom package (created when '
245-
+ 'absent) and deletes the package-less rows. Re-run with --yes to confirm, or run without --apply to preview.',
252+
+ 'absent) and deletes the rows moved. Re-run with --yes to confirm, or run without --apply to preview.',
246253
);
247254
this.exit(1);
248255
return;
@@ -381,7 +388,8 @@ export default class MigratePackagelessOverlays extends Command {
381388
console.log(chalk.bold(`${movable.length} row(s) to move:`));
382389
for (const row of movable) {
383390
// [#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
391+
// [#29392] A row bound to the base (a save naming it, before that write was closed) moves the same way.
392+
console.log(` • ${itemOf(row)}${row.boundToBase ? ` bound to ${row.base}` : ''} → ${row.pair} ${row.addedMember !== undefined
385393
? `(its own member; ${row.base} ships none at ${row.addedMember})`
386394
: `(overrides ${row.base})`}`);
387395
}
@@ -413,7 +421,7 @@ export default class MigratePackagelessOverlays extends Command {
413421
+ `${line.declarationsCompleted} basedOn recorded, ${line.shadowed} shadowed, ${line.failed} failed`);
414422
}
415423
if (failed > 0) {
416-
printError(`${failed} row(s) could not be moved; each package-less row that failed is kept as it was.`);
424+
printError(`${failed} row(s) could not be moved; each row that failed is kept as it was.`);
417425
} else {
418426
printSuccess(`Done ${chalk.dim(`(${timer.display()})`)}. Restart the server so it serves the moved rows.`);
419427
}

0 commit comments

Comments
 (0)