Repository navigation
Commit 9b4fa6c
<!-- claude-projects-attribution: {"github_login":"os-warren"} -->
_Requested by **Warren** · [project
thread](https://claude.ai/code/project/chan_01U5RdnAAmkSQmZbqh6FtqZ8?thread=cmsg_01U5RdnAAmkSQmZbqh6FtqZ8VQJupmprD1XpcbZZYuS4Kh)_
Fixes #29643
Clause-②: no
Metadata-protocol: none
## 维护者速读
- 改了什么:新增 `os migrate option-codes`,把 #29562
之前写进选项字段的旧值(内存驱动的原始数字/布尔、SQLite 的
`"3.0"`/`"1.0"`/`"0.0"`、多值数组里的原始元素)一次性改写成写入口如今存的选项码字符串。
- 为什么改:旧行与新行存成两种形态,`{ f: "3" }` 筛不到旧行、读出来是旧形态、唯一字段上新 `"3"` 与旧 `"3.0"`
不冲突(#29643 实测)。
- 风险与回滚:默认只读演练;`--apply` 只改能判定的值,判不了的(无选项字段上的 `"3.0"`、`"1"` 与 `"true"`
同时声明时的 `"1.0"`、读成未声明码的值)只列出不改;回滚即撤 PR,已改写的值与新写入同形。
- 先看:① `packages/cli/src/commands/migrate/option-codes.ts` 的
`decideOptionCode`(判定表);② 同文件 `migrateOptionCodes` 的整行改写与逐行失败;③
`option-codes.integration.test.ts`(真实 SQLite)。
- 两分钟验证:`pnpm --filter @objectstack/cli exec vitest run --project
integration src/commands/migrate/option-codes.integration.test.ts`,应见
26/26 通过。
- 需要你做的:无,席位落地。
## What changes
- **New command `os migrate option-codes`**
(`packages/cli/src/commands/migrate/option-codes.ts`), the data half of
#29562: a sibling of `os migrate blank-single-values`, not an arm of `os
migrate value-shapes`. `value-shapes` documents that it rewrites
nothing, and the server seat's ruling on #29431 (record 6588116815) put
the last rewriting step beside it rather than in it, so this one follows
that precedent. It walks every option-family field
(`SINGLE_OPTION_TYPES` ∪ `MULTI_OPTION_TYPES` from
`@objectstack/spec/data`, the sets the #29562 door reads; a `multiple:
true` select or radio is walked as an array), `readonly` ones included,
by keyset on `id`.
- **The decision (`decideOptionCode`)**, per stored scalar or per array
element:
- a raw number or boolean is decidable by type and becomes
`String(value)`; on a field declaring codes, only when that string is
one, else it is reported `undeclared`;
- a SQLite `"N.0"` that is not itself a declared code is decidable only
against the declared codes: its readings are `String(Number(s))`, plus
`"true"` for `"1.0"` and `"false"` for `"0.0"`; exactly one declared
reading → that code, none → `undeclared`, two → `ambiguous`, no declared
codes → `free-field`;
- a picklist-bound field whose options did not resolve is reported
`picklist-unresolved`;
- every other string and `null` is left alone.
A multi-value array is rewritten whole only when every old element in it
is decidable, so no row ends half-migrated.
- **Dry run by default**, as the family does: counts per object and
field, a few `stored → code` samples, no row written, read-only boot.
`--apply` writes each affected row once through the engine in the system
context; a failure (a unique collision with a row that already holds the
code) is a per-row failure and the walk goes on. `--object` narrows and
refuses an undeclared name; `--apply` keeps the SQLite occupancy gate
(`--force`); `--json` keeps stdout parseable. No deployment flag: a
re-run that reports nothing to rewrite is the verification.
- **Rosters**: the new command joins the three family pins its sibling
sits in (`schema-migrate.one-shot-family.integration.test.ts`,
`migrate-confirmation-exit.pin.test.ts`,
`json-stdout-purity.e2e.test.ts`) and gets a row and a section in
`content/docs/deployment/cli.mdx`.
- **Changeset** `.changeset/29643-migrate-option-codes.md`:
`@objectstack/cli` patch, both declaration lines.
- No `packages/spec/**` hunk, no write-door change, no `protocol.ts`, no
new error code.
## Why
Wanted 1 asked to measure first. Rows written below the engine's door
(what the pre-#29562 door handed the driver; PR #29638's
recording-driver pin shows it passed the raw scalar through), then a
fresh engine on the same store, at `origin/main` `7dae3b3c`:
| question | SQLite (`SqlDriver`, file) | driver-memory (`persistence:
'file'`) |
|:--|:--|:--|
| stored `3` / `true` / `false` on a single select | `"3.0"` / `"1.0"` /
`"0.0"` | `3` / `true` / `false` |
| stored `[3, true]` on a multiselect | JSON `[3,true]` | `[3, true]` |
| engine read | the old form as stored | the old form as stored |
| `{ coded: "3" }`, `{ coded: "true" }`, `{ free: "3" }` | no row | no
row |
| `{ free: 3 }` | the `"3.0"` row | the `3` row |
| `{ multi: { $contains: "3" } }` | the `[3,true]` row | the `[3,true]`
row |
| unique `uq`: insert `"3"` beside an old `3` / `"3.0"` | admitted
beside it | admitted beside it |
| unique `uq`: then insert `3` | 409 (collides with the new `"3"`) | 409
|
| text field `42` (#29241, item 3) | `"42.0"` | `42` |
So one option splits into two stored values on both drivers, which is
what this rewrite closes. A raw scalar is decidable by its type; a
SQLite `"N.0"` is decidable only when the field declares exactly one of
its readings, which is why the free-field case is reported and never
rewritten.
**Item 3, the text family (#29241): not folded in; its own card is named
in the report.** Its decidable part is only driver-memory's raw `42` /
`true`: on SQLite a text field holds `"42.0"`, and with no declared
codes nothing can tell an old `42` from text someone typed, so a text
pass could rewrite nothing on SQLite and would have to stay silent
there. The text family's set (`BOUNDED_STRING_FIELD_TYPES`) also takes
in `password`, whose reads a walk would have to treat differently. That
makes it a different decision from this one, so it gets its own card
rather than an arm here.
## Verification
Head `70a9ce67`: `541cafa9` (command, tests, rosters, docs, changeset)
and `70a9ce67` (the walk test on SQLite only, see Acceptance note 1).
Workspace built with `turbo run build --filter=./packages/**
--filter=./examples/**` under `os-verify-lock` (VERDICT command-exit 0)
before the gates.
<details>
<summary>Tests</summary>
```text
on 70a9ce6, under os-verify-lock, VERDICT command-exit 0:
pnpm --filter @objectstack/cli typecheck OK
vitest run option-codes.integration + blank-single-values.integration
+ schema-migrate.one-shot-family.integration + migrate-confirmation-exit.pin
+ vitest-tiers-partition Test Files 5 passed (5) · Tests 241 passed (241)
on 541cafa (the roster edit; unchanged since), under the lock, VERDICT command-exit 0:
OS_TEST_TIERS=nightly vitest run test/json-stdout-purity.e2e.test.ts Test Files 1 passed (1) · Tests 89 passed (89)
option-codes.integration.test.ts alone on 70a9ce6 Tests 26 passed (26)
```
</details>
<details>
<summary>Ablation (restored: blob == HEAD, `git diff HEAD`
empty)</summary>
```text
each through scripts/ablation-replace.mjs (anchor 1 -> 0, blob 16ad735 -> mutated), restored by the tool, blob after restore 16ad735 == HEAD, git diff HEAD empty
A sqlite-real arm off: matches.length === 1 never rewrites
option-codes.integration.test.ts Tests 8 failed | 18 passed (26) e.g. "select codes 3/true/false · '3.0' -> rewrite '3'"
B free field rewrites instead of reporting: codes.length === 0 -> rewrite String(Number(stored))
option-codes.integration.test.ts Tests 3 failed | 23 passed (26) "{ type: 'select' } · '3.0' -> report free-field", "--apply rewrites to the code …", "a free field and an ambiguous 1.0 are reported …"
```
</details>
<details>
<summary>Gates and lint (after the last commit, on 70a9ce6)</summary>
```text
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack 92 derived (7 paths vs merge base 7dae3b3)
each run on 70a9ce6, exit captured before any pipe; --ran reconciliation:
92 derived, 92 run, 0 NOT-MEASURED, 0 UNRUN (dispatch-gates --ran exit 0)
first pass (on 541cafa) found one real red and five prerequisite exits:
check:driver-memory-census exit 1: the walk test bound @objectstack/driver-memory -> fixed in 70a9ce6, re-run exit 0
check-plugin-teardown-shape --self-test exit 3 (fixture commit 621a487 fetched), check:skill-examples,
check:dual-build-cjs-loads, check:i18n, check:i18n-coverage exit 3 (built output missing) -> full build, all exit 0
pnpm lint (eslint . --no-inline-config) exit 0
```
</details>
## Acceptance notes
1. **driver-memory is measured, not a test backend.** The walk test runs
on SQLite only: a first version also ran on `InMemoryDriver` and
`check:driver-memory-census` refused the new consumer (exit 1, `x
LEDGERED: … option-codes.integration.test.ts:26`). The ruling there is
the maintainer's, so the consumer was dropped rather than ledgered.
driver-memory's face (a raw scalar on a single-value field) is the
`scalar` arm of `decideOptionCode`, pinned by the decision table, and
the same arm runs end to end on SQLite through the raw JSON elements of
`multi` / `tags`.
2. **Postgres / MySQL: NOT MEASURED** (no server in this container). A
JS number bound into a `text` column there is sent as its decimal
string, so the `"N.0"` arm is not expected to fire; the raw-element arm
in JSON arrays applies on every driver.
3. **Filter face, observed and not changed**: on base, `{ free: 3 }` (a
number comparand) matches an old SQLite `"3.0"` row through SQLite's own
numeric comparison, and the old raw `3` on driver-memory. How a number
comparand should read against option codes belongs to #29486; this PR
does not touch the filter door.
## Human Minimum
- [x] 1. No `packages/spec` change.
- [x] 2. Names follow the casing for their kind (`option-codes`,
`decideOptionCode`, `migrateOptionCodes`).
- [x] 3. No formatter ran; `pnpm lint` result above.
- [x] 4. One issue, one branch off `main`, one pull request.
- [x] 5. Changeset carries both declaration lines.
- [x] 6. n/a (no `packages/spec`).
- [x] 7. No hand edits to references, CHANGELOGs or releases.
- [ ] 8. The maintainer quick-read section is in Chinese by the fleet's
dev contract; everything else is English.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_01JQwxbYbcN3M7eorMnGovXG
---
_Generated by [Claude
Code](https://claude.ai/code/session_01JQwxbYbcN3M7eorMnGovXG)_
---------
Co-authored-by: Claude <noreply@anthropic.com>
1 parent 7b55ea9 commit 9b4fa6c
7 files changed
Lines changed: 896 additions & 0 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 | + | |
| 15 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1304 | 1304 | | |
1305 | 1305 | | |
1306 | 1306 | | |
| 1307 | + | |
1307 | 1308 | | |
1308 | 1309 | | |
1309 | 1310 | | |
| |||
1529 | 1530 | | |
1530 | 1531 | | |
1531 | 1532 | | |
| 1533 | + | |
| 1534 | + | |
| 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 | + | |
1532 | 1560 | | |
1533 | 1561 | | |
1534 | 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 | + | |
| 189 | + | |
| 190 | + | |
| 191 | + | |
| 192 | + | |
| 193 | + | |
| 194 | + | |
| 195 | + | |
| 196 | + | |
| 197 | + | |
| 198 | + | |
| 199 | + | |
| 200 | + | |
| 201 | + | |
| 202 | + | |
| 203 | + | |
| 204 | + | |
| 205 | + | |
| 206 | + | |
| 207 | + | |
| 208 | + | |
| 209 | + | |
| 210 | + | |
| 211 | + | |
| 212 | + | |
| 213 | + | |
| 214 | + | |
| 215 | + | |
| 216 | + | |
| 217 | + | |
| 218 | + | |
| 219 | + | |
| 220 | + | |
| 221 | + | |
| 222 | + | |
| 223 | + | |
| 224 | + | |
| 225 | + | |
| 226 | + | |
| 227 | + | |
| 228 | + | |
| 229 | + | |
| 230 | + | |
| 231 | + | |
0 commit comments