Repository navigation
Commit b13fc74
<!-- claude-projects-attribution: {"github_login":"os-bill"} -->
_Requested by **Bill** · [project
thread](https://claude.ai/code/project/chan_016iMXWSZDzqWRWvWzdNo8wg?thread=cmsg_016iMXWSZDzqWRWvWzdNo8wgKpJdJ6QTJwiKsZotmRvbid)_
domain:server#4 · os-bill · session_01Utp6CZMkHvuDeVYgTD73qF
## 维护者速读
- **改了什么:** 新增一次性清理命令 `os migrate
blank-multi-values`:把多选、复选框、标签、多选查找这类可选多值字段里以前存下的空列表 `[]` 和空字符串 `''` 改写成
`null`,和现在清空字段时写入口存的形式一致。默认只预览(按对象、按字段计数,不写任何行),加 `--apply` 才改写。
- **为什么改:** #29343 之后清空一个多值字段存的是 `null`,但之前清空的旧行还是 `'[]'` 或
`'""'`,于是「为空」筛选、分组和读出来的值把新旧两批清空的行分成三种;`'""'` 连 `$empty` 都找不到。
- **风险与回滚:** 只在 `--apply` 时改数据,且只改「空列表 / 空字符串」这两种值;必填字段、只读字段(比如
`sys_activity.changed_fields` 里平台盖章的 `[]`)、单值字段、有内容的列表都不碰。列集直接读写入口自己的规则
`blankAsNullColumns`,两边不会不一致。重复运行只会报 0。回滚即撤本 PR(已改写的 `null`
与今天清空的行完全相同,无需回写)。
- **先看:** `packages/cli/src/commands/migrate/blank-multi-values.ts` 顶部的
`blankMultiValueColumns` 和
`isBlankMultiValue`(选哪些列、什么算空);`blank-single-values.ts` 里抽出的共享遍历
`walkBlankColumns`(兄弟命令改为调用它,行为不变)。
- **两分钟验证:** `pnpm --filter @objectstack/cli exec vitest run
--maxWorkers=2
src/commands/migrate/blank-multi-values.integration.test.ts`,预期 6
条全绿,其中「改写前 `$null` 只找到今天清空的那行」和「`--apply` 后三行都读作 null」两条。
- **需要你做的:** 无,席位落地。
Fixes #29653
Clause-②: no
Metadata-protocol: none
## What changes
**A new data step, `os migrate blank-multi-values`, beside
`blank-single-values`.** The card names an `os migrate value-shapes`
step; the vehicle follows the server seat's ruling on #29431 (record
6588116815) for the single-value sibling: `value-shapes` documents that
it rewrites nothing and records the ADR-0104 flag, so a repair of stored
blanks is a command of its own, with no deployment flag. The binding
text of ruling A on #29343 (record 6591577985, execution step 3) is "an
`os migrate` data step, filed as its own card, like #29431", and the
maintainer's words were 「另开一张清理已存 '[]' 的 migrate 卡」; `value-shapes`
appears only in the card title. Named in the report's deviations so the
seat can overrule.
- **Columns** (`blankMultiValueColumns`): the `'emptyList'` arm of
`blankAsNullColumns` (`@objectstack/core`), the one rule the write door
and the seed replay read. That is every optional column on the `$empty`
table's `multi_value` row (`isMultiValueField`: `multiselect`,
`checkboxes`, `tags`, and `select` / `radio` / `lookup` / `user` /
`file` / `image` with `multiple: true`). Out by that same rule, with no
second list: `required` columns, lifecycle columns, and `readonly: true`
columns (ruling record 6598733139, the scope note on the card:
`sys_activity.changed_fields` stores a stamped `[]`).
- **What counts as blank** (`isBlankMultiValue`): asked of the door's
own `blankStorageForm(value, 'emptyList')`, so `[]` and exactly `''`,
never whitespace and never a non-empty list.
- **The walk** is the sibling's: `cleanBlankSingleValues`' body moves,
unchanged, into an exported `walkBlankColumns(engine, logger, options,
spec)`, where `spec` names the step, the column selector and the blank
test. `cleanBlankSingleValues` now calls it with its own two; its tests
pass unchanged. Keyset walk by `id`, value tested in JS, per-row
failures reported and the rest continued, unreadable objects reported
and the run marked truncated.
- **The command** mirrors `blank-single-values`: dry run by default on a
read-only boot, `--apply` (with `--yes` or a prompt), SQLite occupancy
gate (`--force`), `--object` narrowing that refuses an undeclared name,
`--max-records`, `--json`.
- **Rosters**: the one-shot family
(`schema-migrate.one-shot-family.integration.test.ts`), JSON stdout
purity (`json-stdout-purity.e2e.test.ts`) and the confirmation exit pin
(`migrate-confirmation-exit.pin.test.ts`) gain the new command, as PR
#29615 added its sibling.
- **Docs**: `content/docs/deployment/cli.mdx` gains the table row and a
section, beside `blank-single-values`.
- **Postgres**: not covered, as the sibling step is not (its integration
test is SQLite only); the walk reads and writes through the engine, not
dialect SQL.
## Tests
`packages/cli/src/commands/migrate/blank-multi-values.integration.test.ts`
(new, real `ObjectQL` + SQLite `SqlDriver`; legacy rows planted through
the driver below the door):
- **Premise (first act of the order):** the door, through
`engine.update` with `[]`, stores `null` on all four types; the
driver-planted legacy rows store `'[]'` and `'""'`.
- **Red before:** `$null: true` finds only the row cleared today;
`$empty: true` misses the stored `''`; the read shapes are `null`, `[]`
and `''` for the three rows.
- **Column set:** exactly one column per type (multiselect, checkboxes,
tags, `multiple: true` lookup); `required`, `readonly` and single-value
columns are out.
- **Dry run:** counts 2 per in-scope column plus 1 on a second object
(9), writes nothing.
- **`--apply`:** every legacy value physically `null`; `$null` finds all
three rows; `$empty` finds the former `''`; `required` / `readonly` /
single-value columns and non-empty lists stay byte-identical; a re-run
reads zero.
- **`objects`** narrows the walk.
<details><summary>Runs (head cdb3666..4864b01, before the main
merge)</summary>
- `pnpm --filter @objectstack/cli exec vitest run --maxWorkers=2
src/commands/migrate/blank-multi-values.integration.test.ts
src/commands/migrate/blank-single-values.integration.test.ts` → `Test
Files 2 passed (2)`, `Tests 12 passed (12)`, lock VERDICT command-exit
0.
- Ablation (committed first, through `scripts/ablation-replace.mjs`,
anchor x1 → x0, blob f45c48e → 25dba17b): `isBlankMultiValue` reduced
to the empty list only → `Tests 2 failed | 4 passed (6)` (the dry-run
count and the `--apply` pin); restored, blob == HEAD, `git diff HEAD`
empty.
- `pnpm --filter @objectstack/cli typecheck` → exit 0 (test layer: 3
file(s) / 28 error(s) held in the ledger, unchanged).
- `pnpm --filter @objectstack/cli exec vitest run --project unit
--maxWorkers=2` → `Test Files 2 failed | 388 passed (390)`, `Tests 5453
passed | 29 skipped`; the two files were `published-subpath-console.pin`
and `published-subpath-hook-body.pin`, which refused because
`packages/cli/dist` was not built. After `pnpm --filter @objectstack/cli
build`, those two plus `migrate-confirmation-exit.pin` → `Test Files 3
passed (3)`, `Tests 81 passed (81)`.
- `src/utils/schema-migrate.one-shot-family.integration.test.ts` →
`Tests 164 passed (164)`.
- `OS_TEST_TIERS=nightly … vitest run
test/json-stdout-purity.e2e.test.ts` → `Tests 101 passed (101)`.
- `pnpm lint` → exit 0.
</details>
## Acceptance notes
- `required` multi-value columns stay out of scope, as the card says.
- The card's title names `value-shapes`; the ruling binds only "an `os
migrate` data step … like #29431", so this follows #29431's vehicle; see
What changes.
## Verification
Local subset, derived by `node scripts/pm/dispatch-gates.mjs --commands
--repo objectstack-ai/objectstack` from the merge base `c58008187` (8
paths, +620 / -5, under the 3000-line threshold); every exit code
captured before any pipe.
- `dispatch-gates --ran`: **92 derived famil(ies) accounted for — 89 run
(all exit 0), 3 NOT-MEASURED (exit 3, prerequisite not met)**:
- `node scripts/check-plugin-teardown-shape.mjs --self-test` — its
fixture is pinned to a commit this shallow clone does not hold.
- `pnpm --filter @objectstack/spec run check:skill-examples` — reads the
built client SDK (`@objectstack/client-react`, `@objectstack/client`),
outside this diff's build closure.
- `pnpm check:dual-build-cjs-loads` — reads `@objectstack/studio`'s
`dist/`, outside this diff's build closure.
- `check:i18n`, `check:i18n-coverage` and `check:i18n-walk-parity` first
answered exit 3 (CLI not yet built) and were re-run after `pnpm --filter
@objectstack/cli build`: exit 0 each.
- None of the three reads a path this PR changes; CI runs each.
- `pnpm lint` → exit 0.
- After `git merge origin/main` (driver-sql and spec moved): `pnpm
install --frozen-lockfile`, `pnpm --filter '@objectstack/cli...' build`,
`rm -rf packages/runtime/.objectstack`, then the new suite, the sibling
suite, the one-shot family and the confirmation pin → `Test Files 4
passed (4)`, `Tests 228 passed (228)`, lock VERDICT command-exit 0, at
head `2ae51bb5`.
- Dogfood: n/a (no `!` in the title, no `(narrowing)` arm).
- CI check-runs: not yet reported at the time of writing.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01Utp6CYyGh7z9Ki2krSKBC6)_
---------
Co-authored-by: Claude <noreply@anthropic.com>
1 parent d2e2f6f commit b13fc74
8 files changed
Lines changed: 620 additions & 5 deletions
File tree
- .changeset
- content/docs/deployment
- packages/cli
- src
- commands/migrate
- utils
- test
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1305 | 1305 | | |
1306 | 1306 | | |
1307 | 1307 | | |
| 1308 | + | |
1308 | 1309 | | |
1309 | 1310 | | |
1310 | 1311 | | |
| |||
1533 | 1534 | | |
1534 | 1535 | | |
1535 | 1536 | | |
| 1537 | + | |
| 1538 | + | |
| 1539 | + | |
| 1540 | + | |
| 1541 | + | |
| 1542 | + | |
| 1543 | + | |
| 1544 | + | |
| 1545 | + | |
| 1546 | + | |
| 1547 | + | |
| 1548 | + | |
| 1549 | + | |
| 1550 | + | |
| 1551 | + | |
| 1552 | + | |
| 1553 | + | |
| 1554 | + | |
| 1555 | + | |
| 1556 | + | |
| 1557 | + | |
| 1558 | + | |
| 1559 | + | |
1536 | 1560 | | |
1537 | 1561 | | |
1538 | 1562 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
| 179 | + | |
| 180 | + | |
| 181 | + | |
| 182 | + | |
| 183 | + | |
| 184 | + | |
| 185 | + | |
| 186 | + | |
| 187 | + | |
| 188 | + | |
0 commit comments