Repository navigation
Commit 6abcb72
fix(objectql): a currency field written as an object is refused with the bare-number lesson (#27258)
<!-- claude-projects-attribution: {"github_login":"os-support-ai"} -->
_Requested by **Yibo Wang** · [project
thread](https://claude.ai/code/project/chan_01BVUyyPie8gHwoPM18iABwB?thread=cmsg_01BVUyyPie8gHwoPM18iABwB3XVq26dCwy6GNva763uff5)_
Fixes #27236
Ruling-ref: 6464202770
Claim: 6468731037
Clause-②: no
Metadata-protocol: none
The accept set does not move, only the message. Measured: the same four
writes (an object, `'abc'`, `10`, an object on a `number` field) answer
the same verdict and the same wire code before and after; only the first
one's `message` changes. No authorable key, schema, type or export
changes.
## 维护者速读
- **改了什么:** 货币字段的值写成对象 `{ value, currency }` 时,拒收码仍是
`invalid_number`,但提示改为说明:货币值是裸数字(ADR-0104),币种在字段的 `currencyConfig`
里声明,dynamic 模式下取组织默认币种。
- **为什么改:** #27182 退役了 `{ value, currency }` 这个旧形状,AI
作者和老代码仍会这样写;以前只看到「必须是数字」,不知道该怎么改。
- **风险与回滚:** 只换提示文字,错误码、错误字段、接受与拒收的集合都不变;回滚 = revert 本 PR。
- **先看:** `packages/objectql/src/validation/record-validator.ts`(数值分支那一行
`fail`)、`packages/spec/src/system/validation-message.ts`(四种语言的新句子)。
- **两分钟验证:** `pnpm --filter @objectstack/objectql exec vitest run
src/validation/record-validator.test.ts -t 27236` → 7 个用例全绿。
- **需要您做的:** 无,席位落地。
## What changes
- **`@objectstack/objectql`, the record validator's numeric arm**
(`record-validator.ts`, `isJudgedNumberType`). When the field is
`currency` and the value is a plain object (an object literal or a
null-prototype record; never an array, a `Date` or another class
instance), the `invalid_number` refusal renders the catalog message
`invalid_number_currency_object` instead of the plain `invalid_number`
one. The code stays `invalid_number` and the error carries no new key,
so no client contract moves. Everything else in the arm keeps the plain
sentence.
- **`@objectstack/spec`, the validation-message catalog**
(`system/validation-message.ts`). It gains
`invalid_number_currency_object` in en, zh-CN, ja-JP and es-ES. **One
message source:** the catalog owns the sentence and the validator passes
only its key, so there is no second hand-written copy. A deployment can
override it under `validation.field.invalid_number_currency_object`, as
with every built-in message.
- **No default currency is named.** The sentence names no currency code
and no symbol in any locale; it points at `currencyConfig` (the `fixed`
mode's `defaultCurrency`) or, under `dynamic`, the tenant default
(#27119 Q2 = X). A spec pin checks this in every locale.
- **The spec's `valueSchemaFor('currency')` arm is not touched.** The
order asked for the sentence there only if `os validate` seed data
reaches it. Measured: it does not (see Verification). That arm also
lives in `packages/spec/src/data/field-value.zod.ts`, which PR #27233
edits, so leaving it alone is also what the serial constraint requires.
- **Serial constraint.** PR #27233 (#27182) is open on the currency
surface. This PR edits none of its 26 files (file lists compared at
#27233 head `c85e08be`). It lands after #27233.
## Tests
- `packages/objectql/src/validation/record-validator.test.ts`, a new
`[#27236]` block, 7 cases:
- `{ value: 10, currency: 'USD' }` on a dynamic-mode and on a fixed-mode
currency field, on insert and on update, is refused as exactly `[{
field, code: 'invalid_number', message: LESSON, label }]`, the same keys
as before.
- The lesson is not the plain sentence, starts with "Amount is a
currency amount: write it as a bare number", and names `currencyConfig`.
- A bare number passes.
- CONTROL: `'abc'` on a currency field keeps the plain sentence.
- CONTROL: `[10]` on a currency field keeps the plain sentence.
- CONTROL: an object on a `number` field keeps the plain sentence,
because the lesson is currency-only.
- `packages/spec/src/system/validation-message.test.ts`, a new block:
every locale names `{ value, currency }`, `currencyConfig`,
`defaultCurrency`, `fixed`, `dynamic` and ADR-0104, and no locale spells
a three-letter currency code or a currency symbol. The existing
completeness pins (every locale defines every key, every template names
`{{label}}`) cover the new key.
## Acceptance notes
- `os validate` does not judge seed record values against their field
type. A seed row giving a currency field `{ value: 10, currency: 'USD'
}` passes validation (exit 0) and is only refused later, when the seed
is written through the record validator. This is a separate finding,
reported to the seat and not changed here.
## Verification
All readings below are on head `6d7035fe` (branch off `origin/main`
`d745bb5e`).
<details><summary>The refusal before and after (objectql validator,
field def with no label, so the label is the API name)</summary>
Before, on `origin/main` `d745bb5e`:
```text
currency { value: 10, currency: 'USD' } → invalid_number "amount must be a number"
currency (fixed, EUR) same object → invalid_number "amount must be a number"
currency 'abc' → invalid_number "amount must be a number"
currency 10 → PASS
number { value: 10, currency: 'USD' } → invalid_number "amount must be a number"
```
After, on `6d7035fe`:
```text
currency { value: 10, currency: 'USD' } → invalid_number "amount is a currency amount: write it as a bare number (ADR-0104), such as 10, not an object such as { value, currency }. A currency field declares its currency in currencyConfig: the fixed mode's defaultCurrency, or under the dynamic mode (the default) the tenant default currency"
currency 'abc' → invalid_number "amount must be a number"
currency 10 → PASS
number { value: 10, currency: 'USD' } → invalid_number "amount must be a number"
```
The error object's keys are `field`, `code`, `message` and `label` in
every row, before and after.
</details>
<details><summary>Does os validate seed data reach
valueSchemaFor('currency')? No</summary>
`examples/app-todo`, temporarily given `budget: Field.currency({ label:
'Budget' })` and a seed row with `budget: { value: 10, currency: 'USD'
}`, then `node packages/cli/bin/run.js validate` on `origin/main`
`d745bb5e` (cli closure built). Result: `✓ Validation passed`, exit 0.
The only line naming the field is the `field-no-consumers` warning,
which lists `data[0].records[4].budget` as a carrier site, so the seed
row was read and its value was not judged. `SeedRecordValueSchema`
(`data/seed.zod.ts`) is `z.unknown()` apart from the seed-file claim.
The temporary edits were restored with `git checkout HEAD --` and `git
diff HEAD` was empty afterwards.
</details>
<details><summary>Ablation</summary>
With the fix committed, `node scripts/ablation-replace.mjs` replaced `t
=== 'currency' && isPlainObjectValue(value) ?` with `false /*
ablated-27236 */ ?` (anchor 1 → 0, marker 0 → 1, blob `f47f64b9` →
`845a0720`), then ran `record-validator.test.ts`: **2 failed | 131
passed**, the two `refuses { value, currency } … with invalid_number and
the lesson` cases. The restore confirmed blob equals HEAD and `git diff
HEAD` empty. The objectql test imports the validator from `src`, so no
rebuild was needed for that leg. A first attempt was a no-op and is not
counted: its replacement text `undefined` already appeared in the file,
and the tool refused it before running anything.
</details>
<details><summary>Package tests and typechecks (under os-verify-lock,
each exit captured separately)</summary>
- `pnpm --filter @objectstack/spec test`: exit 0, Test Files 743 passed
(743), Tests 22265 passed, 1 todo.
- `pnpm --filter @objectstack/spec typecheck`: exit 0.
- `pnpm --filter @objectstack/objectql test`: exit 0, Test Files 477
passed (477), Tests 8906 passed (8906).
- `pnpm --filter @objectstack/objectql typecheck`: exit 0.
- Dogfood battery: not run (no `!` in the title, and `Clause-②` has no
`(narrowing)` arm).
</details>
<details><summary>Derived gates: 82 derived, 81 run with exit 0, 1 NOT
MEASURED</summary>
`node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` derived 82 families. Each was run serially
and recorded as `CMD :: exit N`, and `--ran` reconciled: **82 accounted,
81 run, 1 NOT-MEASURED, 0 UNRUN**.
- `pnpm check:dual-build-cjs-loads`: exit 3, PREREQUISITE NOT MET (it
needs a full-repo build; only the cli and objectql closures were built).
NOT MEASURED; CI runs it.
- `node scripts/check-plugin-teardown-shape.mjs --self-test`: exit 3 on
the first run (the shallow clone lacked its pinned fixture commit), exit
0 after `git fetch origin 621a487`.
- Every other derived family, including `check-adr-0087-registration
--base origin/main`, `check-changeset-no-major`,
`check-empty-changeset`, `check:doc-authoring`,
`check:cross-package-test-inputs` and the spec `check:*` set, exited 0.
</details>
---
_Generated by [Claude
Code](https://claude.ai/code/session_01WGufzenS8UJFmncftEGHsd)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent bc4c963 commit 6abcb72
5 files changed
Lines changed: 130 additions & 1 deletion
File tree
- .changeset
- packages
- objectql/src/validation
- spec/src/system
| 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 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1361 | 1361 | | |
1362 | 1362 | | |
1363 | 1363 | | |
| 1364 | + | |
| 1365 | + | |
| 1366 | + | |
| 1367 | + | |
| 1368 | + | |
| 1369 | + | |
| 1370 | + | |
| 1371 | + | |
| 1372 | + | |
| 1373 | + | |
| 1374 | + | |
| 1375 | + | |
| 1376 | + | |
| 1377 | + | |
| 1378 | + | |
| 1379 | + | |
| 1380 | + | |
| 1381 | + | |
| 1382 | + | |
| 1383 | + | |
| 1384 | + | |
| 1385 | + | |
| 1386 | + | |
| 1387 | + | |
| 1388 | + | |
| 1389 | + | |
| 1390 | + | |
| 1391 | + | |
| 1392 | + | |
| 1393 | + | |
| 1394 | + | |
| 1395 | + | |
| 1396 | + | |
| 1397 | + | |
| 1398 | + | |
| 1399 | + | |
| 1400 | + | |
| 1401 | + | |
| 1402 | + | |
| 1403 | + | |
| 1404 | + | |
| 1405 | + | |
| 1406 | + | |
| 1407 | + | |
| 1408 | + | |
| 1409 | + | |
| 1410 | + | |
| 1411 | + | |
| 1412 | + | |
| 1413 | + | |
| 1414 | + | |
| 1415 | + | |
| 1416 | + | |
| 1417 | + | |
| 1418 | + | |
| 1419 | + | |
| 1420 | + | |
| 1421 | + | |
| 1422 | + | |
| 1423 | + | |
1364 | 1424 | | |
1365 | 1425 | | |
1366 | 1426 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
776 | 776 | | |
777 | 777 | | |
778 | 778 | | |
| 779 | + | |
| 780 | + | |
| 781 | + | |
| 782 | + | |
| 783 | + | |
| 784 | + | |
| 785 | + | |
| 786 | + | |
| 787 | + | |
| 788 | + | |
| 789 | + | |
779 | 790 | | |
780 | 791 | | |
781 | 792 | | |
| |||
1201 | 1212 | | |
1202 | 1213 | | |
1203 | 1214 | | |
| 1215 | + | |
| 1216 | + | |
| 1217 | + | |
| 1218 | + | |
| 1219 | + | |
1204 | 1220 | | |
1205 | 1221 | | |
1206 | | - | |
| 1222 | + | |
1207 | 1223 | | |
1208 | 1224 | | |
1209 | 1225 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
253 | 253 | | |
254 | 254 | | |
255 | 255 | | |
| 256 | + | |
| 257 | + | |
| 258 | + | |
| 259 | + | |
| 260 | + | |
| 261 | + | |
| 262 | + | |
| 263 | + | |
| 264 | + | |
| 265 | + | |
| 266 | + | |
| 267 | + | |
| 268 | + | |
| 269 | + | |
| 270 | + | |
| 271 | + | |
| 272 | + | |
| 273 | + | |
| 274 | + | |
| 275 | + | |
| 276 | + | |
| 277 | + | |
| 278 | + | |
| 279 | + | |
| 280 | + | |
| 281 | + | |
| 282 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
112 | 112 | | |
113 | 113 | | |
114 | 114 | | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
115 | 120 | | |
116 | 121 | | |
117 | 122 | | |
| |||
174 | 179 | | |
175 | 180 | | |
176 | 181 | | |
| 182 | + | |
177 | 183 | | |
178 | 184 | | |
179 | 185 | | |
| |||
219 | 225 | | |
220 | 226 | | |
221 | 227 | | |
| 228 | + | |
222 | 229 | | |
223 | 230 | | |
224 | 231 | | |
| |||
264 | 271 | | |
265 | 272 | | |
266 | 273 | | |
| 274 | + | |
267 | 275 | | |
268 | 276 | | |
269 | 277 | | |
| |||
0 commit comments