Repository navigation
Commit b77c160
feat(spec)!: refuse a homeAmount rateField declaring scale below 12; document CURRENCY_FRACTION_DIGITS as CLDR fraction digits (#30644)
<!-- claude-projects-attribution: {"github_login":"os-sales"} -->
_Requested by **Sales** · [project
thread](https://claude.ai/code/project/chan_01D5bWFY3rFzgJcytD5gD4Uu?thread=cmsg_01D5bWFY3rFzgJcytD5gD4UuV5cRafodM2YQHxeDkdiWJM)_
domain:spec#2 · os-sales · session_01L6bhgVugtCCwBs98FeR27P
Fixes #30103
Clause-②: no (narrowing)
Metadata-protocol: narrowed
Skills-docs: none — `skills/objectstack-data/rules/field-types.md`
(L175-178) teaches `rateField` as a readonly `number` with no `scale`,
which this diff still accepts; the floor ships in the refusal text, the
changeset's FROM → TO and the upgrade-guide entry
`home-amount-rate-field-scale-floor`.
## 维护者速读
- **改了什么:** 对象校验新增一条:`homeAmount.rateField` 指向的数字字段若声明 `scale` 小于
12(汇率的小数位,裁决 6469382727),建模时拒收,错误点名字段、下限 12 和改法;未声明 `scale` 照常通过。同时把
`CURRENCY_FRACTION_DIGITS` 与币种文档改写为 CLDR 小数位(不是 ISO 4217 最小单位),并列出实测不同的
16 个币种。
- **为什么改:** 引擎按汇率行原值(12 位小数)存汇率,字段声明的 scale 只影响显示;声明 scale 4 时页面显示
0.1389,与实际折算用的 0.138912345678 不一致,看到的汇率复算不出存下的本币金额。
- **风险与回滚:** 收窄了 18.3.0 已发布的接受集:已声明 scale 0–11 汇率字段的应用会在 `os
validate`/保存时报错,改法一行(改成 `scale: 12` 或删掉
`scale`);已存的记录数据不受影响。已构建的旧产物若含此类字段,加载时整份被拒,需修源后重建;`sys_metadata`
里已存的对象行照常加载注册,启动日志打 `[metadata_spec_invalid]`,下次保存被 422 拒收,直到把 scale 提到
12 以上或删掉。回滚即撤销本 PR。
- **先看:** ① `packages/spec/src/data/object.zod.ts` 的 `rateField` 分支(新增
scale 判断);② `packages/spec/src/data/currency-config-home-amount.test.ts`
末尾新 describe;③ `.changeset/30103-home-amount-rate-field-scale-floor.md`。
- **两分钟验证:** `pnpm --filter @objectstack/spec exec vitest run
src/data/currency-config-home-amount.test.ts`,预期 38 条全过,其中 scale 4/0/11
被拒、12/14/未声明通过。
- **需要你做的:** 无,席位落地(收窄需席位做一次契约级复审)。
## What changes
- **`refuseDishonestHomeAmount`, the `rateField` arm**
(`packages/spec/src/data/object.zod.ts`). After the existing target and
writer checks, a rate field declaring `scale` below 12 is refused at
`fields.AMOUNT.currencyConfig.homeAmount.rateField`. The message names
the field, its `scale`, the floor 12 and the fix (`scale: 12` or more,
or delete `scale`). The floor is a module-private
`HOME_AMOUNT_RATE_SCALE = 12` citing ruling 6469382727. It restates
platform-objects' `EXCHANGE_RATE_SCALE` because spec cannot import that
package. A rate column shared by consistent amounts is judged once, like
the readonly mark.
- **Undeclared `scale` is accepted, and here is the reading behind it.**
At `origin/main` `1c5cee67`:
- The engine stores the rate row's own value, never rounded
(`packages/objectql/src/home-amount-projection.ts` L38-44).
- Every `number` column is one exact decimal, `NUMERIC_COLUMN_PRECISION
65` / `NUMERIC_COLUMN_SCALE 30`
(`packages/spec/src/data/numeric-column-representation.ts` L174/L181;
`driver-sql` `table.decimal(name, 65, 30)`), whatever `scale` the field
declares.
- The rate row itself carries scale 12 / precision 24
(`sys_exchange_rate.rate`).
- The field schema says an omitted `scale` on a non-percent numeric type
declares no fixed width. `resolveFieldScale` answers `undefined` for it,
and the console's number formatter then uses `maximumFractionDigits:
20`. So an undeclared rate field shows all twelve stored digits.
This follows from the existing field contract. It needs no choice beyond
the ruling.
- **`CURRENCY_FRACTION_DIGITS` TSDoc**
(`packages/spec/src/data/currency-fraction-digits.ts`). It now opens
"CLDR currency fraction digits — NOT ISO 4217 minor units" and carries a
measured table of the 16 differing codes.
- **`content/docs/data-modeling/currency.mdx`.** The fraction-digits
sentence now lists the same 16 codes.
- **Changeset** (`@objectstack/spec` minor, `**BREAKING**`, FROM → TO).
It has a semantic upgrade-guide entry
`18.home-amount-rate-field-scale-floor.ts` (disposition `registered`).
**The 16 codes, measured.** The table's 162 entries were compared with
SIX's ISO 4217 List One, published 2024-06-25: the
`iso-4217-list-one.xml` that `currency-codes@2.2.0` ships, sha256
`2dea9812…71b8b`. The cross-read used the `datasets/currency-codes`
`codes-all.csv` active rows (sha256 `c4b6829a…7407f`). Both give the
same set:
| ISO 4217 minor unit | CLDR digits | Codes |
|:---|:---|:---|
| 3 | 0 | IQD |
| 2 | 0 | AFN, ALL, COP, HUF, IDR, IRR, KPW, LAK, LBP, MGA, MMK, PKR,
SOS, SYP, YER |
Not comparable: XDR and XSU (ISO `N.A.`, table 2); HRK, SLL and ZWL are
withdrawn and absent from List One. XCG is absent from the 2024-06-25
XML and equals 2 in the CSV. ANG, BGN and CUC are absent from the CSV's
active rows and equal in the XML. SIX's own host (`www.six-group.com`)
was refused by this container's egress proxy, so the list was read from
those two copies. The triage's "about seventeen" included SLL. SLL is
withdrawn, so no current ISO minor unit exists to compare it against.
## Verification
<details><summary>Census: in-repo <code>homeAmount.rateField</code>
targets declaring <code>scale</code> < 12 — 1 found, not
changed</summary>
I ran `git grep -n "rateField:"` over the whole tree and read every rate
field definition: `content/docs`, `skills/`, objectql, cli,
plugin-audit, service-analytics, service-settings, dogfood and the spec
tests. `examples/` declares no `homeAmount`.
- **1 hit:**
`packages/objectql/src/engine-home-amount-projection.test.ts` L331,
`invoice({ rate: { scale: 4 } })`. It is an engine pin that a scale-4
rate field still stores the full rate (from PR #28302). It registers the
object through `engine.registry.registerObject`, which does not run
`ObjectSchema` parse, and it stays green (below). It pins what the
engine does with an object stored before this release (stored rows are
kept as authored). It is outside this PR's file surface (objectql) and
is left as is.
- Every other carrier is undeclared (`Field.number({ readonly: true })`)
and is accepted.
</details>
<details><summary>Pins
(<code>ObjectStackDefinitionSchema.safeParse</code>, the parse <code>os
validate</code> and <code>os compile</code> call)</summary>
There is one new top-level `describe` in the existing harness file
`packages/spec/src/data/currency-config-home-amount.test.ts`:
- `scale: 4` is refused. The pin asserts `code: 'custom'`, path
`objects.0.fields.amount.currencyConfig.homeAmount.rateField`, the
message's first sentence (field, `scale: 4`, floor 12), the
prescription, and that the message has no tracker number.
- `scale` 0 and 11 are refused at the same path.
- `scale` 12 and 14 are accepted, and an undeclared `scale` is accepted.
- A shared rate column at `scale: 4` gets exactly one issue.
File: 38/38. Full `@objectstack/spec` suite: 816/816 files, 25671
passed, 1 todo.
</details>
<details><summary>Ablation</summary>
`node scripts/ablation-replace.mjs` replaced the arm's condition with
`if (false && …)`. The anchor went x1 → x0 and the blob `7f70cb81` →
`c41206f1`. Result: **3 red** (the `scale: 4`, the 0/11, and the
shared-column pins) and 35 green. The preservation pins stayed green.
The tool restored the file: blob == HEAD `7f70cb81`, `git diff HEAD`
empty.
</details>
<details><summary>Dogfood battery and consumer tests</summary>
- `pnpm --filter @objectstack/dogfood test` on `997cb02d30`, after a
build of the `@objectstack/dogfood^...` closure (exit 0): VERDICT
command-exit 0, 420 passed / 3 skipped of 423 files, 3500 passed / 36
skipped of 3536 tests. No failure.
- Consumer test files that parse `homeAmount` objects: each exit
captured separately, all 0: objectql `engine-home-amount-projection` +
`home-amount-projection` + `home-amount-write-paths` 3/3 files, 73/73
(the scale-4 engine pin among them); plugin-audit
`home-amount-recompute-audit.integration` 1/1; service-analytics
`home-amount-consolidated-measure` 18 passed / 18 skipped (PostgreSQL
legs, no `OS_TEST_POSTGRES_URL`); service-settings
`localization-currency-lock` 24/24; cli `recompute-home-amounts` 9/9.
</details>
<details><summary>Gates on <code>997cb02d30</code> (exit code captured
before any pipe)</summary>
| Family | Exit |
|:---|:---|
| `pnpm --filter @objectstack/spec build` | 0 |
| `pnpm --filter @objectstack/spec check:generated` (14/14 up to date,
`--fix` not needed) | 0 |
| `dispatch-gates --commands` derived set: 103 commands | 103 × 0 |
| `dispatch-gates --ran` reconciliation | 0 (103 derived, 103 run, 0
NOT-MEASURED, 0 UNRUN) |
| `pnpm check:adr-0087-registration` | 0 (`registered
home-amount-rate-field-scale-floor`, new here) |
| `pnpm check:doc-authoring` | 0 |
| `node scripts/check-nul-bytes.mjs` | 0 |
| eslint `--no-inline-config` on the 4 changed `.ts` files | 0 |
Six derived commands first exited **3 (PREREQUISITE NOT MET: unbuilt
dists)**. Each was re-run after the closure build, and that re-run is
the exit recorded for `--ran`: `check:doc-formula-expressions`,
`check:doc-security-posture`, `check:skill-examples`,
`check:docs-transcript-drift`, `check:lean-entry-closure`,
`check:dual-build-cjs-loads`, all 0. `check:generated` was run at
`--max-old-space-size=6144` (CI's figure). Under the default heap,
`check:test-typecheck`'s tsc ran out of memory: no verdict, not a
finding.
</details>
## Acceptance notes
- **File surface, one addition.** The ADR-0087 disposition has to be
`registered`, because a FROM → TO body refuses
`no-migration-prescription`, and every spec `no (narrowing)` changeset
in stock is registered. So the diff adds
`packages/spec/src/migrations/entries/semantic/18.home-amount-rate-field-scale-floor.ts`,
which the generated registry picks up. Nothing else outside the claimed
surface is touched.
- **`Metadata-protocol: narrowed`**, read from the diff: the change
refuses a value an author could write before (`scale` 0–11 on the named
rate field). The card body says `none`, and the claim already says
`narrowed`.
- **Prose still reading "ISO 4217 minor unit" outside this surface.**
`git grep -n -i "ISO 4217 minor unit" -- packages/spec/src
':!**/*.test.ts'` gives 20 lines. They are in `field.zod.ts` describes
and refusals, five ADR-0087 migration entries, and
`registry.ts.template`; `field-types.mdx` L237/L248/L596 and
`validation-rules.mdx` L160 say the same. They describe how a currency
amount's decimals display, and the console reads those from `Intl`
(CLDR). Several quote ruling 5805782503's own remedy words. Noted, not
filed. carrier: domain:spec seat.
- **`HomeAmountSchema.rateField` `.describe()`** (`field.zod.ts` L657)
does not mention the floor. It is outside this surface. The refusal
carries the prescription. carrier: domain:spec seat.
- **The engine pin at scale 4** (census) stays as an engine-level pin
for stored objects that predate this refusal.
- **Patch round (contract review ④ 3).** The upgrade entry and the
changeset now name the two metadata legs: an older built artifact
carrying such a rate field fails its `ObjectStackDefinitionSchema` parse
at load (`packages/metadata/src/plugin.ts` `_parseAndRegisterArtifact`,
a throwing `.parse` before any registration, reached from `start`), so
it is refused whole and must be rebuilt; a stored `sys_metadata` object
row registers anyway with a `[metadata_spec_invalid]` line
(`packages/objectql/src/registry.ts` `registerItem` → `validate` →
`ObjectSchema.parse`, caught and logged) and its next save is refused
`422 INVALID_METADATA`
(`packages/metadata-protocol/src/protocol-save.ts`, the `object` schema
is `ObjectSchema`). Stored data rows are unaffected.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01L6bhgVugtCCwBs98FeR27P)_
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_01L6bhgVugtCCwBs98FeR27P
<!-- signed: domain:spec#2 · os-sales · session_01L6bhgVugtCCwBs98FeR27P
-->
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 47c457b commit b77c160
6 files changed
Lines changed: 194 additions & 13 deletions
File tree
- .changeset
- content/docs/data-modeling
- packages/spec/src
- data
- migrations/entries/semantic
| 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 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
300 | 300 | | |
301 | 301 | | |
302 | 302 | | |
303 | | - | |
304 | | - | |
305 | | - | |
306 | | - | |
| 303 | + | |
| 304 | + | |
| 305 | + | |
| 306 | + | |
| 307 | + | |
307 | 308 | | |
308 | 309 | | |
309 | 310 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
17 | 17 | | |
18 | 18 | | |
19 | 19 | | |
| 20 | + | |
20 | 21 | | |
21 | 22 | | |
22 | 23 | | |
| |||
256 | 257 | | |
257 | 258 | | |
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 | + | |
| 283 | + | |
| 284 | + | |
| 285 | + | |
| 286 | + | |
| 287 | + | |
| 288 | + | |
| 289 | + | |
| 290 | + | |
| 291 | + | |
| 292 | + | |
| 293 | + | |
| 294 | + | |
| 295 | + | |
| 296 | + | |
| 297 | + | |
| 298 | + | |
| 299 | + | |
| 300 | + | |
| 301 | + | |
| 302 | + | |
| 303 | + | |
| 304 | + | |
| 305 | + | |
| 306 | + | |
| 307 | + | |
| 308 | + | |
| 309 | + | |
| 310 | + | |
| 311 | + | |
| 312 | + | |
| 313 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1 | 1 | | |
2 | 2 | | |
3 | 3 | | |
4 | | - | |
5 | | - | |
6 | | - | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
7 | 7 | | |
8 | 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 | + | |
9 | 34 | | |
10 | 35 | | |
11 | 36 | | |
| |||
51 | 76 | | |
52 | 77 | | |
53 | 78 | | |
54 | | - | |
55 | | - | |
56 | | - | |
57 | | - | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
58 | 86 | | |
59 | 87 | | |
60 | 88 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1948 | 1948 | | |
1949 | 1949 | | |
1950 | 1950 | | |
| 1951 | + | |
| 1952 | + | |
| 1953 | + | |
| 1954 | + | |
| 1955 | + | |
| 1956 | + | |
| 1957 | + | |
| 1958 | + | |
| 1959 | + | |
| 1960 | + | |
| 1961 | + | |
| 1962 | + | |
| 1963 | + | |
| 1964 | + | |
| 1965 | + | |
| 1966 | + | |
| 1967 | + | |
| 1968 | + | |
| 1969 | + | |
| 1970 | + | |
1951 | 1971 | | |
1952 | 1972 | | |
1953 | 1973 | | |
| |||
1976 | 1996 | | |
1977 | 1997 | | |
1978 | 1998 | | |
1979 | | - | |
| 1999 | + | |
| 2000 | + | |
| 2001 | + | |
1980 | 2002 | | |
1981 | 2003 | | |
1982 | 2004 | | |
| |||
1996 | 2018 | | |
1997 | 2019 | | |
1998 | 2020 | | |
1999 | | - | |
| 2021 | + | |
2000 | 2022 | | |
2001 | 2023 | | |
2002 | 2024 | | |
| |||
2146 | 2168 | | |
2147 | 2169 | | |
2148 | 2170 | | |
| 2171 | + | |
| 2172 | + | |
| 2173 | + | |
| 2174 | + | |
| 2175 | + | |
| 2176 | + | |
| 2177 | + | |
| 2178 | + | |
| 2179 | + | |
| 2180 | + | |
| 2181 | + | |
| 2182 | + | |
| 2183 | + | |
| 2184 | + | |
| 2185 | + | |
| 2186 | + | |
2149 | 2187 | | |
2150 | 2188 | | |
2151 | 2189 | | |
| |||
| 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 | + | |
0 commit comments