You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit 2bba35a
Browse filesBrowse the repository at this point in the historyBrowse files
fix(cli): os migrate record-currency-codes judges --code before the probe, prompt and boot; document the command and the record-mode switch (#28416)
<!-- claude-projects-attribution: {"github_login":"os-zhuang"} -->
_Requested by **Sam** · [project
thread](https://claude.ai/code/project/chan_01LjZwNRFiG2fkp8SNf6rHNx?thread=cmsg_01LjZwNRFiG2fkp8SNf6rHNxTiTuqLhvqhUP6JVyRUyTJk)_Fixes#28168
Clause-②: no
Metadata-protocol: none
## 维护者速读
- 改了什么:`os migrate record-currency-codes` 现在一进来就判 `--code`,不在 ISO 4217
里的码在探库、`--apply` 确认提示和 boot 之前就以 `INVALID_FIELD` / 400 退出;CLI 文档和
Currency 文档页补上这条命令、切换时的 409 与 dry run / `--apply` 两步。
- 为什么改:之前坏码要等 boot 之后才被引擎拒绝,而 `--apply` 的 boot
会先建表建列;文档里兄弟命令都有条目,唯独这条没有。
- 风险与回滚:只在命令开头加一个纯函数判断,判据与引擎同一个
`isValueDomainMember('iso_4217_currency', …)`,引擎侧检查原样保留;回滚即 revert 本 PR。
- 先看:`packages/cli/src/commands/migrate/record-currency-codes.ts`(新函数与
`run()` 开头那段),其测试文件新增的
describe,`content/docs/data-modeling/currency.mdx` 新增小节。
- 两分钟验证:`pnpm --filter @objectstack/cli exec vitest run --project
integration src/commands/migrate/record-currency-codes.test.ts`,预期 12
个用例全绿,含「坏码不探库、不弹提示、不 boot」两条。
- 需要你做的:无,席位落地。
## What changes
- **S2, `packages/cli/src/commands/migrate/record-currency-codes.ts`.**
`run()` now judges `--code` first, right after the header and before the
occupancy probe, the `--apply` confirmation prompt and
`bootSchemaStack`. The predicate is the one
`backfillRecordCurrencyCodes` applies,
`isValueDomainMember('iso_4217_currency', code)` from
`@objectstack/spec/shared` (a light import; `@objectstack/objectql`
stays lazily loaded). The error is the engine's envelope: `code:
'INVALID_FIELD'`, `status` and `httpStatus` 400, so `--json` answers `{
error, code: 'INVALID_FIELD', httpStatus: 400 }` and exit 1, as it did
after the boot. The human face prints the message and exits 1.
- **Wording.** The message keeps the engine's prefix and its sentences:
`[record-currency-backfill] 'usd' is not an ISO 4217 currency code, so …
would refuse it: give the uppercase alphabetic code of the currency
these amounts are in (for example USD).` One part cannot match word for
word: the engine names the code field (`'opportunity.currency_code'`),
which is read from the object's declaration, and that is not loaded
before the boot. The early refusal names the amount instead (`the
currency code field of 'opportunity.amount'`) and ends `Refused before
the database was opened or any row was read.`
- **Engine unchanged.**
`packages/objectql/src/record-currency-backfill.ts` is untouched and
keeps its own check for every other caller. The CLI's post-boot catch
comment now says the code is judged before the boot.
- The `--code` flag description now says a non-ISO code is refused
before the database is opened.
- **S3, `content/docs/deployment/cli.mdx`.** A row in the
data-migrations table, after `summary-nulls`, and a `#### os migrate
record-currency-codes` section shaped like `summary-nulls`: why the rows
owe a code, the three invocations, the code is the operator's and never
derived, the early `--code` refusal and the post-boot `OBJECT_NOT_FOUND`
/ `INVALID_FIELD` refusals, writes through the engine, idempotent, no
flag.
- **S3, `content/docs/data-modeling/currency.mdx`.** A `### Switching a
field that already holds rows` section under the record mode: the 409
`destructive_change` with finding `currency_mode_to_record`, lifted by
re-submitting with `force` (`?force=true` on `PUT
/api/v1/meta/:type/:name`), then the dry run and `--apply`. It says the
code is the one given with `--code` and is never derived, and links to
the CLI section. Both pages are hand-written trees under AGENTS.md's
Documentation Guardrails, not release-owned.
- `.changeset/28168-record-currency-code-before-boot.md`:
`@objectstack/cli` patch, both declaration lines.
## Tests
`packages/cli/src/commands/migrate/record-currency-codes.test.ts`
(integration tier, as the file already was):
- New describe `a code outside ISO 4217 is refused before the probe, the
prompt and the boot`:
- `--json --apply` with `usd` and no `--yes`: exit 1, the envelope is
`INVALID_FIELD` / 400 (the same `code` and `httpStatus` as the real
engine's refusal of the same code, which the test produces), never
`confirmation_required`; the message's leading clause equals the
engine's; `bootSchemaStack`, `probeMigrationTarget` and the backfill are
never called.
- Human face, `--apply` on a terminal (`stdin.isTTY` true) with `EURO`:
exit 1, the engine's leading clause is printed, no readline prompt is
opened, and neither the boot nor the probe is called.
- Control: a member code (`USD`) still reaches the boot and the
backfill.
- The existing envelope case fed the engine's code refusal through a
mocked backfill. That path is now unreachable for a bad code, so the
case now feeds the engine's other pre-walk refusal (a member code on a
field that is not record-mode), which only the booted engine can make,
and still pins `INVALID_FIELD` / 400 / the message reaching `--json`.
## Acceptance notes
- The message names `'OBJECT.FIELD'`'s code field instead of the code
field by name (reason above). The envelope (`code`, `httpStatus`) and
every other sentence are the engine's.
- No out-of-scope findings.
## Verification
All readings at head `738019ef`.
<details><summary>Tests and typecheck</summary>
- `pnpm --filter @objectstack/cli exec vitest run --project integration
--maxWorkers=2 src/commands/migrate/record-currency-codes.test.ts`: Test
Files 1 passed, Tests 12 passed; lock VERDICT command-exit 0.
- `pnpm --filter @objectstack/cli exec vitest run --project unit
--maxWorkers=2 test/migrate-confirmation-exit.pin.test.ts`: Tests 38
passed.
- `pnpm --filter @objectstack/cli typecheck`: exit 0 (tsc, then
check:test-typecheck OK).
- The rest of the unit and integration layers are declared to CI.
</details>
<details><summary>Ablation</summary>
`node scripts/ablation-replace.mjs` replaced the call
`refuseNonMemberCurrencyCode(flags.object, flags.field, flags.code);`
with a marker (anchor 1 to 0, blob 6bf55f7 to 9501b58a), then ran the
test file: the two new refusal cases failed (Tests 2 failed, 10 passed)
and the control passed. Restored: the file's blob equals HEAD's
(6bf55f7) and `git diff HEAD` is empty. The command imports from `src`,
so no rebuild was involved.
</details>
<details><summary>Gates</summary>
- `node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack`: 93 families. All 93 ran, each recorded as
`COMMAND :: exit N`. Five (`check:skill-examples`,
`check:dual-build-cjs-loads`, `check:i18n`, `check:i18n-coverage`,
`check:i18n-walk-parity`) first exited 3 (PREREQUISITE NOT MET); after a
full `pnpm build` (lock VERDICT command-exit 0, Tasks 116 successful of
116) all five exited 0.
- `dispatch-gates --ran`: 93 derived, 93 run, 0 NOT-MEASURED, 0 UNRUN,
exit 0.
- `pnpm lint`: exit 0.
- CI check-runs are cited in the report once they settle.
</details>
## Human Minimum
- [x] 1. Schemas start as Zod; `packages/spec` holds no runtime logic.
(Not touched.)
- [x] 2. Every name I added uses the casing for its kind.
- [x] 3. No formatter ran; `pnpm lint` passes.
- [x] 4. One issue, one branch off an up-to-date `main`, one pull
request.
- [x] 5. A change that publishes carries a changeset, and both
declaration lines are filled in here and in the changeset.
- [x] 6. `packages/spec` touched: not touched.
- [x] 7. No hand edits to `content/docs/references/`,
`packages/*/CHANGELOG.md` or `content/docs/releases/`.
- [x] 8. Title, body and comments are in English, except the 维护者速读
summary, which the PM loop writes in Chinese.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_01QUs3BvyNBMezKJUd7aydyG
---------
Co-authored-by: Claude <noreply@anthropic.com>
fix(cli): `os migrate record-currency-codes` refuses a `--code` outside ISO 4217 before it probes the database, prompts or boots
6
+
7
+
Clause-②: no
8
+
Metadata-protocol: none
9
+
10
+
A code that is not an uppercase ISO 4217 code (`usd`, `EURO`) used to be refused only by the backfill, after the app had booted, and an `--apply` boot creates missing tables and columns before that refusal. The command now judges `--code` first, with the same predicate the backfill uses, and answers with the same `INVALID_FIELD` / 400 envelope (`code` and `httpStatus` under `--json`) and exit 1. Nothing is probed, prompted or booted for a bad code. The backfill in `@objectstack/objectql` keeps its own check unchanged.
Copy file name to clipboardExpand all lines: content/docs/deployment/cli.mdx
+38Lines changed: 38 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -1302,6 +1302,7 @@ where the data lives.
1302
1302
| `os migrate files-to-references` | Convert legacy file-field values to `sys_file` references, verify the ownership ledger, and record the deployment's migration flag |
1303
1303
| `os migrate value-shapes` | Scan stored reference and structured-JSON field values against the platform's value contract, and record the deployment's migration flag when clean |
1304
1304
| `os migrate summary-nulls` | Backfill roll-up `count` / `sum` columns still stored as `NULL` on parent rows created before the insert-time seed. Repairs values; no flag, nothing depends on it having run |
1305
+
| `os migrate record-currency-codes` | Write the currency code you give into the rows of a record-mode currency field that hold an amount and no code, the rows stored before the field was switched to `currencyMode: 'record'`. The code is never derived. No flag; idempotent. See [below](#os-migrate-record-currency-codes) |
1305
1306
| `os migrate comment-reactions` | Carry the emoji reactions a 17.x deployment stored in the retired `sys_comment.reactions` column into `sys_comment_reaction` records, list every stored value that does not read, and record the verified run that lets `os migrate apply --allow-destructive` drop the column |
1306
1307
| `os migrate organization-ownership` | The v18 ownership ceremony (ADR-0131 D10): write a plan of what each table's rows will become, then `--apply` that reviewed plan. It upgrades a database 17.x or an 18.0 pre-release (`18.0.0-next.*`) created; a v18 server refuses to start on one until it has run. See [below](#os-migrate-organization-ownership) |
1307
1308
| `os migrate activity-changed-fields` | Stamp the two read-rule columns (`changed_fields`, `milestone_key`) on the `sys_activity` rows written before they existed. Until it runs, those rows are withheld from every reader but the system, so run it at once after upgrading. No flag; idempotent |
@@ -1504,6 +1505,43 @@ A deployment whose database was seeded fresh on this version has nothing to do
1504
1505
here: its parents were created with the value already in place, and the run
1505
1506
reports zero.
1506
1507
1508
+
#### `os migrate record-currency-codes`
1509
+
1510
+
Switching an existing currency field to `currencyMode: 'record'` leaves every row
1511
+
already stored holding an amount beside a blank code. Nothing refuses such a row for
1512
+
resting, but until it holds a code it shows as a plain number, falls into the `null`
1513
+
group of a grouped aggregate, and sits outside every filter on the amount and its
1514
+
code. The save that makes the switch answers 409 `destructive_change` naming this
0 commit comments