Skip to content

Commit 66bf5b4

Browse files
fix(cli): os diff says a replay inserts an insert seed dataset again, as it does for replace (#31192)
<!-- claude-projects-attribution: {"github_login":"marchtian"} --> _Requested by **田騏** · [project thread](https://claude.ai/code/project/chan_01VmuUhXU9SSUzqJYyXSmYSi?thread=cmsg_01VmuUhXU9SSUzqJYyXSmYSiCWLkoGmW8Dzox677cXtnd4)_ Fixes #31139 Clause-②: no Metadata-protocol: none ## 维护者速读 - **改了什么:** 只加一句提示。`os diff` 的升级计划对 `mode: 'insert'` 的种子数据集,现在和 `replace` 一样说明重放会怎样:"a replay inserts these again, and a row the database refuses is that row's error"。 - **为什么改:** 加载器里 `insert` 和 `replace` 走的是同一条路:不查匹配,每次重放都把记录再插一遍。上一个 PR 只给 `replace` 加了提示,`insert` 数据集在计划里看起来像无害,同一种行为被说成两样。 - **风险与回滚:** 无行为变化,只动一条 CLI 提示字符串、它的测试和一个 changeset。回滚即 revert。 - **先看:** `packages/cli/src/commands/diff.ts` 的 `seedModeNote`(新增的 `insert` 一行);`packages/cli/test/diff-upgrade-plan.test.ts` 新增的用例。 - **两分钟验证:** `pnpm --filter @objectstack/cli exec vitest run --project unit test/diff-upgrade-plan.test.ts`,预期 8 个测试全过,新用例里 `insert` 数据集新增和修改两行都带这句提示,`ignore` 那行不带。 - **需要你做的:** 无,席位落地。 ## What changes Text only; no behaviour change. - `packages/cli/src/commands/diff.ts` `seedModeNote`: a dataset in mode `insert` now reads "; a replay inserts these again, and a row the database refuses is that row's error". The `replace` and `upsert`/`update` lines are unchanged. - Worded from the loader on `main`, not from the card: `SeedLoaderService.writeRecord` case `'insert'` calls `engine.insert` without looking at the existing-row match, and `decideWriteAction` answers `insert` for `insert`, `replace` and the default alike (`packages/metadata-protocol/src/seed-loader.ts`). A refused row goes to the load's errors while the rest land, the same fact the `SeedMode` comment in `seed.zod.ts` states since #31135. - `packages/cli/test/diff-upgrade-plan.test.ts`: one new case. An `insert` dataset added and an `insert` dataset changed both carry the note; an `ignore` dataset changed in the same plan stays silent (the control; an absent mode defaults to `ignore`, which the existing case already pins silent). - `.changeset/cli-diff-insert-seed-note.md`: `@objectstack/cli` patch. ## Tests - `pnpm --filter @objectstack/cli exec vitest run --project unit test/diff-upgrade-plan.test.ts`: 1 file, 8 tests passed. - Ablation (the source is imported from `src`, so no build leg): `scripts/ablation-replace.mjs` deleted the new `insert` line (anchor 1 → 0, blob b02482c → 5a8ab9b); the new case went red ("Tests 1 failed | 7 passed (8)"), the other seven stayed green. Restored: blob == HEAD and `git diff HEAD` empty. - `packages/cli` unit layer, whole (`vitest run --project unit`): 396 files passed; the 2 other files (`published-subpath-console.pin`, `published-subpath-hook-body.pin`) refused because `packages/cli/dist` was not built yet. After `pnpm --filter @objectstack/cli build` both passed (2 files, 29 tests). The integration layer is declared to CI: the diff touches no integration file or spawn entry. - `pnpm --filter @objectstack/cli typecheck`: exit 0. ## Acceptance notes - No Fable review: the diff touches no spec, skills or ADR (rule of 2026-11-07 16:07Z). - Merged `origin/main` once before opening (1f5316d); the incoming commits touch no `packages/cli` file. ## Verification <details><summary>Derived gates (dispatch-gates --commands) — 62 families, all exit 0 on 1f5316d</summary> - `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --ran ran.txt` :: exit 0 — "62 derived famil(ies) accounted for — 62 run, 0 NOT-MEASURED". - First pass: 3 families exited 3 (PREREQUISITE NOT MET): `check-plugin-teardown-shape --self-test` (shallow clone), `check:dual-build-cjs-loads` and `check:i18n-coverage` (workspace packages not built). After `git fetch --unshallow origin main` and a full `turbo run build`, all 3 reran with exit 0; the other 59 exited 0 on the first pass. - `eslint` on `diff.ts` and `diff-upgrade-plan.test.ts` :: exit 0 (repo-wide `pnpm lint` is CI's). </details> 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_017ihL2bzGzPEiCJX3i1fYRj Co-authored-by: Claude <noreply@anthropic.com>
1 parent 2ce2646 commit 66bf5b4

3 files changed

Lines changed: 33 additions & 0 deletions

File tree

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
'@objectstack/cli': patch
3+
---
4+
5+
fix(cli): `os diff` now says what a replay does to a seed dataset in mode `insert`, as it already did for `replace`: a replay inserts its records again, and a row the database refuses is that row's error. The loader's `insert` arm inserts every record without looking for a match, so the plan no longer reads an `insert` dataset as harmless.
6+
7+
Clause-②: no
8+
Metadata-protocol: none

‎packages/cli/src/commands/diff.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ function diffNamedArrays(
7979
function seedModeNote(mode: string): string {
8080
if (seedModeOverwritesMatchedRows(mode)) return '; a replay writes its columns over the rows it matches';
8181
if (mode === 'replace') return '; a replay deletes nothing and inserts these again, so rows already in the table stay';
82+
if (mode === 'insert') return "; a replay inserts these again, and a row the database refuses is that row's error";
8283
return '';
8384
}
8485

‎packages/cli/test/diff-upgrade-plan.test.ts‎

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,4 +153,28 @@ describe('os diff — what the upgrade plan adds', () => {
153153
{ type: 'modified', category: 'data', name: 'acme_region', breaking: false, detail: 'seed dataset changed: records; mode ignore' },
154154
]);
155155
});
156+
157+
it('an insert dataset, added or changed, says a replay inserts its records again; ignore stays silent', () => {
158+
const before = lead({}, [
159+
{ object: 'acme_source', mode: 'insert', records: [{ name: 'web' }] },
160+
{ object: 'acme_region', mode: 'ignore', records: [{ name: 'emea' }] },
161+
]);
162+
const after = lead({}, [
163+
{ object: 'acme_source', mode: 'insert', records: [{ name: 'web' }, { name: 'event' }] },
164+
{ object: 'acme_region', mode: 'ignore', records: [{ name: 'apac' }] },
165+
{ object: 'acme_channel', mode: 'insert', records: [{ name: 'email' }] },
166+
]);
167+
const insertNote = "a replay inserts these again, and a row the database refuses is that row's error";
168+
expect(diffStacks(before, after)).toEqual([
169+
{
170+
type: 'added', category: 'data', name: 'acme_channel', breaking: false,
171+
detail: `seed dataset added: 1 record(s); mode insert; ${insertNote}`,
172+
},
173+
{
174+
type: 'modified', category: 'data', name: 'acme_source', breaking: false,
175+
detail: `seed dataset changed: records; mode insert; ${insertNote}`,
176+
},
177+
{ type: 'modified', category: 'data', name: 'acme_region', breaking: false, detail: 'seed dataset changed: records; mode ignore' },
178+
]);
179+
});
156180
});

0 commit comments

Comments
 (0)