Repository navigation
Commit 4145210
feat(spec):
epic:#8345#1 · os-bill · session_01Utp6CbTZrFqRYNgjSaSbN5
<!-- claude-projects-attribution: {"github_login":"os-bill"} -->
_Requested by **Bill** · [project
thread](https://claude.ai/code/project/chan_016iMXWSZDzqWRWvWzdNo8wg?thread=cmsg_016iMXWSZDzqWRWvWzdNo8wg9oTskgBVZmtxXLAEh251Cb)_
Fixes #27184
⛔ not released alone — R1: pairs with #27186, #27187, #27188 (one
Version Packages release)
Merge-after: #27218, #27233, #27239 (all three landed; `main` merged in
at `73725c20`)
Clause-②: yes (widening)
Metadata-protocol: added
Skills-docs: updated here
Head: `b6f152bd` — CI fully green (65 check runs: 58 success, 7 skipped,
0 failed). Contract review PASS at `90114641` (comment 6487611079); its
should-fixes are applied in `f3d286ce` and `b6f152bd` (see
"Contract-review follow-ups" below).
## 维护者速读
- **改了什么:** 金额字段(及 `returnType: currency` 的公式)新增第三种币种模式
`record`:`currencyConfig.currencyField` 指向同对象上一个声明了 `valueDomain:
'iso_4217_currency'` 的文本字段,每条记录的币种就存在那里;金额本身仍是裸数字。前端
`resolveFieldCurrency` 对 `record` 模式字段返回
`undefined`(不回落租户默认币种),逐行币种由按行渲染读取。`record` 下 `currencyField`
不能为空,也不允许再写 `defaultCurrency`。
- **为什么改:** 平台此前没有任何键能表达「这条记录是什么币种」,这是多币种(ADR-0144
D2)的第一块,海外客户按客户币种开票依赖它。
- **风险与回滚:** 纯新增可选键(minor);不写 `record` 的应用零变化,`dynamic`/`fixed`
的解析输出逐字节不变(有测试钉住)。回滚即撤 PR。⛔ 不单独发版:与 #27186/#27187/#27188 同一次发布。
- **先看:** `packages/spec/src/data/object.zod.ts` 的
`refuseDishonestRecordCurrencyField`(三种拒收);`packages/spec/src/data/field.zod.ts`
的 `CurrencyConfigSchema` 与
`refuseRecordModeKeyMismatch`;`skills/objectstack-data/rules/field-types.md`
的币种段。
- **两分钟验证:** `pnpm --filter @objectstack/spec exec vitest run
src/data/currency-config-record-mode.test.ts`,预期 15 条全绿(含空指针、`record` 下的
`defaultCurrency`,以及指向不存在 / 非文本 / 无值域三种拒收及候选字段列表)。
- **需要你做的:** 三个前置(#27218、#27233、#27239)均已合并,`main` 已合入;契约复核在 `90114641`
已 PASS,其 should-fix 已在 `b6f152bd` 落实,等同一复核人的增量复核。本 PR 触及
`skills/**`(Tier H),落地需一次授权审批。
## What changes
- `CurrencyConfigSchema.currencyMode` gains `record` (ADR-0144 D2).
`currencyField` (new, optional key) names the sibling field holding each
record's ISO 4217 code. `currencyField` is never empty (`.min(1)`). A
config-level refinement, `refuseRecordModeKeyMismatch`, makes it
required under `record`, refuses it under `dynamic` / `fixed`, and
refuses `defaultCurrency` under `record`, each at its own key with a
prescription; it is chained after #27157's
`refuseFixedCurrencyWithoutCode` on the same schema. `dynamic` keeps
tolerating an inert `defaultCurrency` for the rows #27157 left behind;
`record` is new, so it needs no such tolerance (ADR-0078). The amount
stays a bare number (ADR-0104 D1, kept by ADR-0144 D2).
- `ObjectSchema`'s `superRefine` gains
`refuseDishonestRecordCurrencyField`, beside
`refuseNonPictureImageField`: for every field whose config is `record`,
the pointer must name another declared field whose type may carry a
value domain (`VALUE_DOMAIN_FIELD_TYPES`, `text` today; the door reads
the set rather than repeating the literal) and which declares
`valueDomain: 'iso_4217_currency'`. Three refusals, each located at
`fields.FIELD.currencyConfig.currencyField`, each carrying the
prescription and the object's candidate code fields (or, when there are
none, how to declare one). A `formula` with `returnType: 'currency'`
meets both doors in the same shape, since it carries the same
`currencyConfig`.
- `field.form.ts`: the `record` option and a `currencyField` row in the
currency composite; the `currencyMode` help text describes all three
modes, `currencyConfig` says the value is a bare number "in every mode",
and `defaultCurrency` says it is refused in record mode. The
metadata-form translation bundles are regenerated
(`check-i18n-bundles.mjs --write`), and the zh-CN / ja-JP / es-ES text
for the new row and the three changed help texts is written by hand.
- `@object-ui/i18n` `resolveFieldCurrency`: the parameter type accepts
`record`, and a `record`-mode field resolves to `undefined` outright,
with no fallback to the tenant default, `config.defaultCurrency` or a
legacy `defaultCurrency`. The field has no currency of its own; a record
whose code is blank shows no symbol rather than a currency it never
named (aligned with #27340, the per-row display).
- Generated: `content/docs/references/**` (gen:docs) and
`packages/spec/authorable-surface/data.json` (gen:schema, `+
data/CurrencyConfig:currencyField`). The
`dropped-refinements.baseline.json` sites this PR first added (the
`data/CurrencyConfig` root plus `currencyConfig` under 12 schemas,
header 252 / 900) are now identical to what #27218 landed, so the ledger
is no longer in this diff.
- `skills/objectstack-data/rules/field-types.md`: the currency block now
covers all three modes, with a one-line record-mode example (`{
currencyMode: 'record', currencyField: 'currency_code' }`, the
conventional name per ADR-0144 D8 rule 2). "A measure's own `currency`
wins" is scoped to `fixed` / `dynamic` (#27338 refuses a measure
`currency` over a record amount). The home-projection names
`AMOUNT_home` / `AMOUNT_rate` are dropped until that projection exists.
Paid for inside the same file: 322 → 320 lines, 3151 / 3158 tokens
(ratchet green); no `SKILL.md` touched.
- Changeset `.changeset/27184-currency-record-mode.md`:
`@objectstack/spec` minor, `@object-ui/i18n` minor,
`@objectstack/platform-objects` minor (it ships the regenerated
metadata-form bundles).
- ADR anchors: ADR-0144 added to
`scripts/adr-anchors/packages__spec__src__data__field.zod.ts.json` and
`…object.zod.ts.json`, each with its D2 invariant.
## Tests
- New `packages/spec/src/data/currency-config-record-mode.test.ts` (15
tests): `record` ⇔ `currencyField` at the config door (required /
refused under `dynamic`, `fixed` and the defaulted mode); an empty
`currencyField` refused in every mode; `defaultCurrency` refused under
`record` (and still tolerated under `dynamic`); the three object-level
refusals plus the self-pointer; the formula shape; `parse(parse(x))`
equals `parse(x)` for configs and an object; `dynamic` and `fixed`
outputs pinned byte-identical (`JSON.stringify`) to the post-#27157
output. Refusals assert `code`, `path` and the message's first sentence.
- `ui/packages/i18n/src/__tests__/resolveFieldCurrency.test.ts`: one new
case — a parsed `record` config resolves to `undefined` with a tenant
default, with `config.defaultCurrency`, with a legacy `defaultCurrency`,
and with no tenant default.
- `packages/platform-objects/src/apps/translations/`: two hand-pinned
catalog counts move for the new row — `field-panel-echo-decisions`
UNWALKED_CHILDREN 10 → 11 (names `currencyConfig.currencyField`), and
the `object-lifecycle-panel-echo-decisions` per-locale positive control
694 → 695 (main's #26674 took it 695 → 694).
- Ablations, one per new refusal, through `scripts/ablation-replace.mjs`
on the committed head (anchor 1 → 0 on disk, restored to the HEAD blob):
`.min(1)` → `.min(0)` turns exactly "refuses an empty `currencyField`…"
red (1 failed / 14 passed); disabling the `record` + `defaultCurrency`
arm turns exactly "refuses `defaultCurrency` under `record`…" red (1
failed / 14 passed).
- At `b6f152bd`: `@objectstack/spec` 835 files / 23779 tests pass,
typecheck exit 0; `@objectstack/platform-objects` 97 files / 1369 tests
pass. At `90114641`: `ui` `packages/i18n/` 89 files / 1374 tests pass,
`@object-ui/i18n` type-check exit 0 (no ui file changed since). Earlier,
at `bf7f2b74`, `ui` `packages/i18n/ plugin-detail/ plugin-gantt/ types/`
15806 tests pass.
## Acceptance notes
- **`main` is merged in, not rebased.** Two merge commits: `94161588`
(#27218) and `e527c3b6` (#27233, #27239, base `73725c20`), each followed
by its own regeneration commit (`a9f5abfd`, `90114641`). The second
merge conflicted only on the object-lifecycle positive-control count
(combined to 695). The os-regen driver kept this branch's side of
`authorable-surface/data.json`, which still listed the `CurrencyValue`
keys #27233 retired; the baseline was taken from `main` and rebuilt, so
the only line this PR adds there is `currencyField`.
- **ADR-0144 citations checked against the landed text (`ad89eb22`).**
Code, test, changeset and the two new anchors cite only D2, whose
wording matches ("An amount field declares `currencyMode: 'record'`. Its
`currencyField` names a text field on the same object with `valueDomain:
'iso_4217_currency'`"; the value stays a bare number, ADR-0104 D1 kept).
The skills naming convention matches D8 rule 2. D-numbers are unchanged;
nothing here cites Open item 12 or the Builds-on ADR-0112 sentence.
`check-adr-anchors` is green.
- **A producer that reads the new mode wrongly, outside this card's file
surface:** `@objectstack/formula`'s `currencyOf`
(`packages/formula/src/formula-return.ts`) maps every non-`fixed` config
to `dynamic`, so `inferFormulaReturn` over a `record`-mode amount would
stamp a currency formula with no `currencyConfig` (tenant currency)
instead of `{ currencyMode: 'record', currencyField }`. Not reachable
before this PR lands; carried as a declared rider on #27186.
- `ObjectExtensionSchema` does not judge the pointer: an extension's
fields merge into an object it does not hold, so its pointer may name an
owner-declared field (stated in the helper's docblock).
- `check:i18n`, `check:i18n-stale-fill` and `check:i18n-coverage` are
green with the hand-translated three-mode texts.
- The docs-drift bot comment lists hand-written pages naming
`currencyConfig` / `currencyMode`; documenting the record mode there is
left to #27189.
## Contract-review follow-ups (review 6487611079 at `90114641`, PASS)
- ② `@objectstack/platform-objects` added to the changeset.
- ④.1a `currencyField` `.min(1)`; ④.1b `defaultCurrency` refused under
`record`; ④.1c / ④.2b ADR-0144 anchors on `field.zod.ts` and
`object.zod.ts`.
- ④.2a the object door reads `VALUE_DOMAIN_FIELD_TYPES`.
- ④.3 / ③ zh-CN / ja-JP / es-ES `currencyMode` help text now describes
three modes (plus the `currencyConfig` and `defaultCurrency` texts that
the new rule changed).
- ④.4a–c the skill: measure-currency sentence scoped, `AMOUNT_home` /
`AMOUNT_rate` dropped, record-mode example added, within the ratchet.
- Not this PR's and left as the review placed them: the share-link
projection gap (escalated to the epic PM), the `os-zhuang` approval on
`5232c8b1` (escalated to the maintainer), and the inherited
`AddFieldOperation` ledger drop (#27218).
## Verification
<details><summary>Dispatch gates — 123 derived, 123 run, all exit 0 at
b6f152b (reconciled with --ran)</summary>
Derived with `node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` at `b6f152bd` (20 paths vs merge base
`73725c20`); every family run with the closure built, each exit code
recorded; reconciliation: "123 derived famil(ies) accounted for — 123
run, 0 NOT-MEASURED (a DERIVED zero — all 123 recorded an exit code and
none of them is 3)". All 123 exit 0. `check:generated` (spec): all
artifacts current.
Earlier rounds, kept for the record: at `90114641`, 115 / 115 exit 0; at
`5232c8b1`, 112 derived / 112 run, with `check-skills-token-ratchet`
(fixed by deletion in the same file) and `check:i18n` (bundles
regenerated and translated) red on the first pass and green after; at
`a9f5abfd`, 112 / 112 exit 0.
</details>
<details><summary>CI at b6f152b — 65 check runs, 58 success, 7 skipped,
0 failed</summary>
The same set as below, all success again: Lint & Repo Gates, TypeScript
Type Check, Test Core (1–8/8), Test UI (1–16/16), Type Check UI, Lint
UI, Build & E2E UI, Live E2E UI, Dogfood Regression Gate, Build Core /
Docs / UI Deps, Check Changeset, PR-body guards, Governed Surface Queue
Guard and the rest; the same 7 opt-in or schedule-gated jobs skipped.
</details>
<details><summary>CI at 9011464 (the reviewed head) — 65 check runs, 58
success, 7 skipped, 0 failed</summary>
Success: Lint & Repo Gates (and its Lint · … jobs), TypeScript Type
Check (and its Type Check · … jobs), Test Core (1–8/8), Test Core, Test
UI (1–16/16), Test UI, Type Check UI, Lint UI, Build & E2E UI, Live E2E
UI, Dogfood Regression Gate, Build Core, Build Docs, Build UI Deps, Plan
Test Core shards, Check Changeset, Check PR Size, Check Documentation
Links, Spec property liveness, Governed Surface Queue Guard, Flag docs
affected by code changes, PR-body guards, Auto Label, filter. Skipped
(opt-in or schedule-gated): Dogfood Verify CLI, Dogfood Regression Gate
matrix, Temporal Conformance, UI Published Artifacts, 18.x upgrade
harness, Packed-tarball smoke, Schedule gate.
</details>
---
_Generated by [Claude
Code](https://claude.ai/code/session_01Utp6CbfVSc1hXtxLUPHZ2f)_
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_01Utp6CbfVSc1hXtxLUPHZ2f
---------
Co-authored-by: Claude <noreply@anthropic.com>currencyMode: 'record' + currencyField — a record-mode amount points at a sibling ISO 4217 code field (multi-currency M0-1) (#27268)1 parent 273137d commit 4145210
20 files changed
Lines changed: 428 additions & 49 deletions
File tree
- .changeset
- content/docs/references
- data
- system
- packages
- platform-objects/src/apps/translations
- spec
- authorable-surface
- src/data
- scripts/adr-anchors
- skills/objectstack-data/rules
- ui/packages/i18n/src
- __tests__
| 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 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
28 | 28 | | |
29 | 29 | | |
30 | 30 | | |
31 | | - | |
32 | | - | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
33 | 34 | | |
34 | 35 | | |
35 | 36 | | |
| |||
89 | 90 | | |
90 | 91 | | |
91 | 92 | | |
92 | | - | |
| 93 | + | |
93 | 94 | | |
94 | 95 | | |
95 | 96 | | |
| |||
236 | 237 | | |
237 | 238 | | |
238 | 239 | | |
239 | | - | |
240 | | - | |
| 240 | + | |
| 241 | + | |
| 242 | + | |
241 | 243 | | |
242 | 244 | | |
243 | 245 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
268 | 268 | | |
269 | 269 | | |
270 | 270 | | |
271 | | - | |
| 271 | + | |
272 | 272 | | |
273 | 273 | | |
274 | 274 | | |
| |||
631 | 631 | | |
632 | 632 | | |
633 | 633 | | |
634 | | - | |
| 634 | + | |
635 | 635 | | |
636 | 636 | | |
637 | 637 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
102 | 102 | | |
103 | 103 | | |
104 | 104 | | |
105 | | - | |
| 105 | + | |
106 | 106 | | |
107 | 107 | | |
108 | 108 | | |
| |||
533 | 533 | | |
534 | 534 | | |
535 | 535 | | |
536 | | - | |
| 536 | + | |
537 | 537 | | |
538 | 538 | | |
539 | 539 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
618 | 618 | | |
619 | 619 | | |
620 | 620 | | |
621 | | - | |
| 621 | + | |
622 | 622 | | |
623 | 623 | | |
624 | 624 | | |
625 | | - | |
| 625 | + | |
| 626 | + | |
| 627 | + | |
| 628 | + | |
| 629 | + | |
626 | 630 | | |
627 | 631 | | |
628 | 632 | | |
629 | | - | |
| 633 | + | |
630 | 634 | | |
631 | 635 | | |
632 | 636 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
618 | 618 | | |
619 | 619 | | |
620 | 620 | | |
621 | | - | |
| 621 | + | |
622 | 622 | | |
623 | 623 | | |
624 | 624 | | |
625 | | - | |
| 625 | + | |
| 626 | + | |
| 627 | + | |
| 628 | + | |
| 629 | + | |
626 | 630 | | |
627 | 631 | | |
628 | 632 | | |
629 | | - | |
| 633 | + | |
630 | 634 | | |
631 | 635 | | |
632 | 636 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
708 | 708 | | |
709 | 709 | | |
710 | 710 | | |
711 | | - | |
| 711 | + | |
| 712 | + | |
| 713 | + | |
712 | 714 | | |
713 | | - | |
| 715 | + | |
714 | 716 | | |
715 | 717 | | |
716 | 718 | | |
| |||
0 commit comments