Skip to content

Commit b7075d9

Browse files
docs(adr): ADR-0144 settles Open items 2, 3, 4, 6 and 9, records ruling 6539345871 (A′ and B), the rounding mode and CLDR fraction digits (multi-currency, epic #8345) (#30182)
<!-- claude-projects-attribution: {"github_login":"os-bill"} --> _Requested by **Bill** · [project thread](https://claude.ai/code/project/chan_016iMXWSZDzqWRWvWzdNo8wg?thread=cmsg_016iMXWSZDzqWRWvWzdNo8wg8JgVyS9BQnLALTRES6fash)_ epic:#8345#1 · os-bill · session_01Utp6CZWjnSeed8H4dMjt9d Fixes #30102 Supersedes #30174 (same branch and head `a0c8d7ca`, body unchanged; reopened through the fleet relay, which the dispatch protocol requires for a pull request). Ruling-ref: 6539345871 Not release-paired: `docs/adr/**` only, no changeset — the diff publishes nothing from any released package (`skip-changeset`). Clause-②: no Metadata-protocol: none ## 维护者速读 - **改了什么:** ADR-0144 开放项 2、3、4、6、9 各加一条 *Note (2026-11-04)* 标为已定:2 取单据日当天或之前最近一条汇率,datetime 按组织 `localization.timezone` 取日历日(裁决 6539345871 B);3 精确小数、除法、一次远离零舍入到本位币 **CLDR** 小数位,汇率原样存不舍入;4 未设本位币拒写 `ERR_HOME_CURRENCY_UNSET`,不默认;6 同币种汇率 1 不查表;9 三个错误码已入 ledger。D9 与 TL;DR 记下裁决 A′(18.3.0 把 R1 与 R2 核心一起发出,#27193/#27190/#27196 为 R2.1);Context/Consequences 两处把「ISO 4217 最小单位」改为 CLDR 小数位;状态块「the only ledger additions」删去,改为三个码。开放项 13 未动。 - **为什么改:** ADR 每有答复就要修订;main 上这几项仍写着「未决」、ledger 仍写「两个码」,与 18.3.0 已发出的代码不一致。 - **风险与回滚:** 纯文字,不动代码、schema、skills;每条注记按 `file#symbol` 引用 main(`f68b47ca`)上的树,符号锚门禁全部解析;Tier H(`docs/adr/**`)。回滚即撤 PR。 - **先看:** Open items 2、3、4、6、9 末尾的注记,再看 D9 末尾的注记与状态块「Builds on」一行。 - **两分钟验证:** `pnpm check:adr-anchors`、`pnpm check:adr-index`、`pnpm check:adr-symbol-anchors`、`node scripts/check-adr-links.mjs` 均退出 0。 - **需要你做的:** Tier H:席位跑 CONTRACT_REVIEW_TIER 复审并 ACCEPT 后,由 `os-zhuang` 或 `hotlong` 在该 head 批准。 ## What changes One file, `docs/adr/0144-…`, eleven edits, each in the dated-note form PR #28181 used for Open items 11–13. Every claim about behaviour cites the tree on `main` at `f68b47ca` by `file#symbol`, read for this PR, not recalled; every landing cites the pull request and its merge commit. **Open items (the Task list's first bullet):** - **Item 2 — which rate row.** Settled as workload row M2-2 read it: the organization's `sys_exchange_rate` row for the record's currency with the latest `effective_date` on or before the document date; a later row never applies; none in force refuses `ERR_EXCHANGE_RATE_MISSING`. Evidence: the `find` under `$lte` / `desc` / `limit: 1`, as a system read in the row's own organization (`engine.ts#homeAmountServing`); a non-positive rate counts as missing and the read is memoized per (organization, currency, day) (`home-amount-projection.ts#projectHomeAmounts`, `#memoizeHomeAmountServing`). **Ruling 6539345871 letter B** is recorded here: a `datetime` document date is its calendar day in the organization's `localization.timezone`, UTC when unset or unknown, never the request zone (`#documentDateOf`); not chosen: A. The blank-date `VALIDATION_FAILED` / `required` refusal is named. Review 6539117549 and its delta 6539668963 are cited. - **Item 3 — arithmetic and rounding.** Settled with one correction to the item's own sentence: the digit count is CLDR's, not ISO 4217's. Amount ÷ rate in exact scaled-decimal (`BigInt`), **rounded once, half away from zero** (`1.005` → `1.01`, `-0.125` → `-0.13`) to the home currency's fraction digits, stored as the bare number ADR-0104 D1 requires (`#divideToPlaces`, `#divideRoundHalfAwayFromZero`, `#toScaledDecimal`); **the rate is stored unrounded** so the home amount reproduces from the stored pair; a declared `scale` on the rate field reaches display only because every numeric column takes the one `decimal(65, 30)` representation (`numeric-column-representation.ts#numericColumnFor`), and the spec-side refusal of a scale below 12 is named as spec-lane work, not decided. `CURRENCY_FRACTION_DIGITS` is CLDR `currencyData` (IQD 0 vs 3; HUF, COP, IDR, PKR and other unused-minor-unit codes 0 vs 2), exported from `@objectstack/spec/data` for this one reader; review F1 / should-fix S2 and the ruling's S2 order are cited. - **Item 4 — unset home currency.** Settled by refusal: `ERR_HOME_CURRENCY_UNSET` / 400 before anything is written, no default (`#HomeCurrencyUnsetError`, thrown by `#projectHomeAmounts`); the review's reasoning against D5's rejected fallbacks and D8 rule 5, and why the code is its own ledger member, are recorded (6539117549 ③.1). The settings bridge is named (`engine.ts#setHomeLocalizationResolver`, `plugin.ts`): no service or unregistered key reads as unset and refuses; any other settings error is the write's failure. The refusal is at write, not registration. The lock (#27198, PR #28297 `291bf44a`) refuses a change to a set value and passes the first setting of an unset one (`localization-currency-lock.ts#localizationCurrencyWritePredicate`). - **Item 6 — same currency as home.** Settled as the row read it: rate `1`, no lookup, no rate row needed, still rounded, rate field stores `1` (`#projectHomeAmounts`, the `code === home` arm). - **Item 9 — refusal codes.** Settled: three codes in ADR-0112's extension ledger under `@objectstack/objectql` (`error-code-ledger.zod.ts#ERROR_CODE_LEDGER`): `ERR_CURRENCY_CODE_REQUIRED` (#27186, PR #27339 `57c51aab`; `record-currency-code.ts#RecordCurrencyCodeRequiredError`), `ERR_EXCHANGE_RATE_MISSING` (`#ExchangeRateMissingError`, what its message names) and `ERR_HOME_CURRENCY_UNSET` (`#HomeCurrencyUnsetError`), the last two in PR #28302. The two `VALIDATION_FAILED` reuses (`required` on a blank document date; `invalid_value` on a divergent predicate update, `#settleBulkHomeAmounts`) and the `validate()` preview (`engine-references-and-master-detail.ts#previewHomeAmounts`, `engine-validate.ts`) are recorded. **Ruling 6539345871 letter A′ (second bullet):** a dated note at the end of **D9**: 18.3.0 (Version Packages #26517, version commit `83d7e4ae`, merged 2026-11-03) carries R1 and the R2 core together — `sys_exchange_rate` (#27185, PR #27246 `194e66c0`), `currencyConfig.homeAmount` (#27191, PR #28096 `73b72709`), the write-time projection (#27192, PR #28302 `6762dd91`) and the `localization.currency` lock (#27198, PR #28297 `291bf44a`), each named in its package's `## 18.3.0` CHANGELOG section and on `content/docs/releases/v18/18-3.mdx`; the pairing rule holds as written; what changed is the release gate of ruling 6522614678 (letter A), in one respect, with the ruling's reason; #27193 / #27190 / #27196 are R2.1; the options not chosen (A, B, C). The **TL;DR** delivery bullet gets a two-sentence note pointing at D9, since the claim's file surface lists TL;DR and the bullet otherwise contradicts it. **Rounding mode and the unrounded rate (third bullet):** in the Item 3 note, as above. **CLDR, not ISO 4217 minor units (fourth bullet):** in the Item 3 note; in **Context §1** (the ISO 4217 value-domain bullet, whose "no reader" sentence gets a dated note: the values have their reader, and they are CLDR fraction digits); and in **Consequences** ("may get their first reader" — they did, as CLDR digits). **"The only ledger additions" (fifth bullet):** the Status block's "Builds on" line now reads three codes — a blank code, a missing rate, an unset home currency (Open items 4 and 9) — registered in the ledger, with a dated note saying what the sentence said before and why the "only" claim is dropped. The **Evidence** line says where the 2026-11-04 notes were read (`f68b47ca`). **Open item 13 (sixth bullet):** untouched. The seat reported PR #28128 S1 unruled at this head. **Not touched:** code, `skills/**`, `.claude/**`, every other ADR, `docs/adr/README.md` (`adr-index --report` says it is current: the record's first status line did not change). ## Why The record's Open items preamble says "This record is amended with each answer." Items 2, 3, 4, 6 and 9 were answered by PR #28302 (its body's "ADR-0144 open items settled here" and its two contract-review records), and 18.3.0 shipped them on 2026-11-03; ruling 6539345871 amended the release pairing (A′) and settled the `datetime` day (B) and ordered the ADR amendment (review should-fix S4). On `main` the five items still read as open, the Status block still says two ledger codes, and Item 3 and Context §1 still say ISO 4217 minor units where the tree reads CLDR digits. Card #30102 carries this amendment from the predecessor's sign-off ledger. ## Verification Head `a0c8d7ca`: two docs commits on `claude/project-thread-b5ine2`, branched from `origin/main` at `f68b47ca` — the amendment (`244f2ff0`) and the contract review's two should-fixes (Provenance §1 names ruling 6539345871 and §3 the `f68b47ca` reading; the Open item 2 note quotes the rate predicate verbatim). The four ADR checks below were re-run on `a0c8d7ca`, all exit 0; the 21-command gate sweep and its `--ran` reconciliation ran on `244f2ff0`, and the second commit changes prose in the same file only. - `pnpm check:adr-anchors` (self-test 133 assertions + full sweep): exit 0. - `pnpm check:adr-index` (self-test 46 assertions + `--report`): exit 0, `docs/adr/README.md` current. - `pnpm check:adr-symbol-anchors`: exit 0 — every `file#symbol` this PR adds resolves (the sweep reports 3107 anchors across 149 records, 0 line anchors). - `node scripts/check-adr-links.mjs`: exit 0 (979 relative links resolve). - `node scripts/check-commit-card-trailers.mjs --range origin/main..HEAD`: exit 0. - `node scripts/pm/dispatch-gates.mjs --commands <the ADR path>` derives 21 commands (13 by path, 8 whole-tree). Each was run with its exit code captured before any pipe: | Command | Exit | |:--|:--| | `node scripts/check-adr-links.mjs` | 0 | | `node scripts/check-adr-links.mjs --self-test` | 0 | | `node scripts/check-closing-keyword-parity.mjs` | 0 | | `node scripts/check-closing-keyword-parity.mjs --self-test` | 0 | | `node scripts/check-comment-mask-corpus.mjs` | 0 | | `node scripts/ci/plan-test-core-shards.mjs --self-test` | 0 | | `pnpm --filter @objectstack/lint run check:doc-formula-expressions` | 0 | | `pnpm check:adr-anchors` | 0 | | `pnpm check:adr-index` | 0 | | `pnpm check:adr-symbol-anchors` | 0 | | `pnpm check:ci-filter-parity` | 0 | | `pnpm check:closed-ceremony-text` | 0 | | `pnpm check:cross-package-test-inputs` | 0 | | `pnpm check:doc-authoring` | 0 | | `pnpm check:driver-memory-census` | 0 | | `pnpm check:gitlink-declared` | 0 | | `pnpm check:nul-bytes` | 0 | | `pnpm check:pm-governed-merges` | 0 | | `pnpm check:pm-prior-rulings` | 0 | | `pnpm check:refd-timer-probe` | 0 | | `pnpm check:watch-hint-literal` | 0 | `pnpm --filter @objectstack/lint run check:doc-formula-expressions` first exited **3** (PREREQUISITE NOT MET: `@objectstack/formula` and `@objectstack/lint` unbuilt in a fresh clone; the gate measured nothing); after `pnpm exec turbo run build --filter=@objectstack/formula --filter=@objectstack/lint` it exited 0, the code recorded above. `dispatch-gates --ran` reconciliation on `244f2ff0`: 21 derived families accounted for, 21 run, 0 NOT-MEASURED, 0 UNRUN. - Clone depth: this checkout is shallow (50 commits), so the merge commits of #26517 (`83d7e4ae`, 2026-11-03T14:44Z), #28302 (`6762dd91`), #28297 (`291bf44a`) and #28096 (`73b72709`) were confirmed against the GitHub API and against the `## 18.3.0` CHANGELOG sections and `content/docs/releases/v18/18-3.mdx` on `main`; `194e66c0`, `a3d24c99`, `6dc06b2e` and `57c51aab` are in the clone or confirmed the same way. - Model tier: `dispatch-gates --tier` derives the mandated tier for `docs/adr/**` at effort xhigh; the text was drafted and read at that tier. ## 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. (no names added) - [x] 3. No formatter ran; markdown only. - [x] 4. One issue, one branch off an up-to-date `main`, one pull request. - [x] 5. A change that publishes carries a changeset: this diff publishes nothing (`docs/adr/**` only), so `skip-changeset`; both declaration lines are above. - [x] 6. `packages/spec` touched: n/a. - [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 (the 维护者速读 block and quoted rulings are Chinese by the Communication rule). 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01Utp6CaFs7UbZV5xCiu37Kk --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent e826e77 commit b7075d9

1 file changed

Lines changed: 125 additions & 5 deletions

File tree

‎docs/adr/0144-multi-currency-record-currency-dated-rates-write-time-home-projection.md‎

Lines changed: 125 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,12 @@
1111
[`6469382727`](https://github.com/objectstack-ai/objectstack/issues/27185#issuecomment-6469382727) on #27185. The maintainer chose the option 「小数点后 12 位(推荐)」.
1212
- **Builds on**: [ADR-0104](./0104-field-runtime-value-shape-contract.md) D1 (a `currency` value is a bare number,
1313
kept here). [ADR-0112](./0112-error-code-vocabulary-and-ledger.md) governs every refusal that answers on the wire.
14-
Two of them need new codes, a blank record currency code and a missing rate ([Open item](#open-items) 9): ADR-0112
15-
sets their codes and envelope, and both are registered in its extension ledger. They are the only ledger additions.
14+
Three of them need new codes, a blank record currency code, a missing rate and an unset home currency
15+
([Open items](#open-items) 4 and 9): ADR-0112 sets their codes and envelope, and all three are registered in its
16+
extension ledger (`packages/spec/src/api/error-code-ledger.zod.ts#ERROR_CODE_LEDGER`, under `@objectstack/objectql`).
17+
*Note (2026-11-04):* until Open item 4 settled by refusal (`ERR_HOME_CURRENCY_UNSET`, PR #28302, `6762dd91`) this
18+
sentence named two codes and called them "the only ledger additions"; the third code is that settlement's, and the
19+
"only" claim is dropped.
1620
"At build" in D1 and D6 covers the declared aggregates: a `summary` roll-up refuses at registration and a dataset
1721
measure at compile, with existing codes (`INVALID_METADATA` at registration, `DATASET_INVALID` at compile). An ad-hoc
1822
aggregate is never built, so it meets the same rule at the request door (the ObjectQL aggregate-by-field-type door
@@ -29,7 +33,8 @@
2933
shape verdict [`6041482631`](https://github.com/objectstack-ai/objectstack/issues/8345#issuecomment-6041482631);
3034
[#27119](https://github.com/objectstack-ai/objectstack/issues/27119) (design record, workload rows, ruling); this
3135
record's card [#27183](https://github.com/objectstack-ai/objectstack/issues/27183). Every claim below about current
32-
behaviour cites a file or symbol as read on `origin/main` at `cdec7473`.
36+
behaviour cites a file or symbol as read on `origin/main` at `cdec7473`; the dated notes of 2026-11-04 (Open items
37+
2, 3, 4, 6 and 9, D9) read `origin/main` at `f68b47ca`, and each names the pull request that landed its answer.
3338

3439
---
3540

@@ -51,19 +56,25 @@
5156
Metadata never names the home currency.
5257
- Delivery comes in three releases. R1 ships the record currency, the refusals and the per-row display. R2 ships the
5358
rates, the projection and the recompute. R3 is proof and polish. R1 and R2 each ship as one Version Packages release.
59+
*Note (2026-11-04):* 18.3.0 shipped R1 and the R2 core (the rate table, the `homeAmount` key, the write-time
60+
projection and the home-currency lock) in one Version Packages release, by ruling `6539345871`, letter A′; the
61+
recompute, the import check and the home field's display follow as R2.1. See D9.
5462

5563
## Provenance — what is ruled here, and what is not
5664

5765
Three kinds of text feed this record. Each section says which kind it carries.
5866

5967
1. **Ruled.** Ruling `6465431966` fixes shape C′, sub-questions Q0–Q4, parameters 1–4, the five design rules, the
6068
release pairing and the commitment. Ruling `6469382727` on #27185 fixes parameter 4's unit. [Decision](#decision)
61-
carries only these two. It quotes them wherever the wording matters.
69+
carries only these two. It quotes them wherever the wording matters. Ruling `6539345871` on #8345 (2026-10-31)
70+
amends D9's release gate (letter A′) and settles [Open item](#open-items) 2's `datetime` day (letter B); D9 and
71+
Open item 2 carry it as dated notes, and nothing else in [Decision](#decision) rests on it.
6272
2. **Analysis the ruling read.** This is the design record `6464438764`, the workload rows `6464936321`, the
6373
measurement round `6041378776` and the shape verdict `6041482631`. They supply the context, the shapes considered
6474
and the per-card plan. A detail that appears only there is not ruled. This record lists such details under
6575
[Open items](#open-items) or as a card's draft, never as a decision.
66-
3. **Readings.** Current behaviour, measured at `cdec7473` and cited by file or symbol.
76+
3. **Readings.** Current behaviour, measured at `cdec7473` and cited by file or symbol. The dated notes of
77+
2026-11-04 read `origin/main` at `f68b47ca` the same way.
6778

6879
**Two numberings meet here.** #27119 is a decision batch, and two of its items are also called Q1 and Q2. #27119 Q1
6980
asks when multi-currency starts, and the answer was 乙-现在. #27119 Q2 asks for one rule for an unknown currency, and
@@ -94,6 +105,11 @@ aggregation. The ruling names them by topic ("Q0 A", "rates A", "home currency A
94105
membership in `CURRENCY_FRACTION_DIGITS`
95106
(`packages/spec/src/data/currency-fraction-digits.ts#CURRENCY_FRACTION_DIGITS`). Only that table's key set is read.
96107
Its fraction-digit values have no reader.
108+
*Note (2026-11-04):* the values have their reader since PR #28302 (`6762dd91`): the write-time home projection
109+
rounds the home amount to them ([Open item](#open-items) 3), through the table's export from `@objectstack/spec/data`
110+
(`packages/spec/src/data/index.ts`). They are CLDR fraction digits, a checked-in `currencyData` snapshot as the
111+
table's own header records, not ISO 4217 minor units: for about seventeen codes the two differ (`IQD` 0 against 3;
112+
`HUF`, `COP`, `IDR`, `PKR` and other codes whose minor unit CLDR records as unused, 0 against 2).
97113
- **No conversion is implemented.**
98114
`git grep -n -i -E "exchange.?rate|fx_rate|currency_conversion|convertCurrency" -- packages/ ':!*.test.ts'` prints 0
99115
lines. Control: `git grep -c currencyConfig -- packages/spec/src/data/field.zod.ts` prints 13.
@@ -275,6 +291,25 @@ docs page. Under R2 it files #27194, M2-4. Those placements are the epic PM's, n
275291
R1 and R2 pull request "⛔ not released alone — pairs with …". Under Prime Directive #15, the maintainer merges
276292
Version Packages, and the epic body says that happens only when a pair is complete.
277293

294+
*Note (2026-11-04):* amended by ruling [`6539345871`](https://github.com/objectstack-ai/objectstack/issues/8345#issuecomment-6539345871) on #8345, letter A′,
295+
maintainer 「同意」 (2026-10-31T05:03Z). **18.3.0 carries R1 and the R2 core together.** Version Packages #26517
296+
waited for PR #28302 and PR #28297 to land, and its version commit `83d7e4ae` (merged 2026-11-03) consumed
297+
`sys_exchange_rate` (#27185, PR #27246 `194e66c0`), `currencyConfig.homeAmount` (#27191, PR #28096 `73b72709`), the
298+
write-time home projection (#27192, PR #28302 `6762dd91`) and the `localization.currency` lock (#27198, PR #28297
299+
`291bf44a`); each package's `## 18.3.0` section names its entry (`packages/objectql/CHANGELOG.md`,
300+
`packages/spec/CHANGELOG.md`, `packages/platform-objects/CHANGELOG.md`,
301+
`packages/services/service-settings/CHANGELOG.md`), and so does `content/docs/releases/v18/18-3.mdx`. The pairing
302+
rule above holds as written: no declaration shipped without the engine that fills it. What the ruling changes is the
303+
release gate of ruling [`6522614678`](https://github.com/objectstack-ai/objectstack/issues/8345#issuecomment-6522614678) on #8345 (letter A, 2026-10-30: R2 is
304+
built now and released once the customer confirms the consolidation terms of Context §2), in one respect: the R2 core
305+
was released ahead of that confirmation, because the three terms are configuration axes — `localization.currency`,
306+
the rate table's `source` values, the author's `rateDate` pointer — that the engine hard-codes none of, so the
307+
customer has no R2 rows to recompute. **R2.1** is #27193 (the recompute), #27190 (the import check) and #27196 (the
308+
home field's read-only display), released in a later Version Packages; none is a half-state, since the home and rate
309+
fields are `readonly` and render as such. Not chosen: A (merge #26517 before #28302, the half-state this section
310+
forbids), B (hold #26517 until the customer confirms), C (a registration-time refusal of `homeAmount` as a release
311+
switch in code).
312+
278313
## Non-goals
279314

280315
These are outside this decision, and each would need a decision of its own. The first eight are the workload rows'
@@ -306,6 +341,9 @@ D8 already excludes a switch, injected columns, fixed field names and a setting
306341
row and no home-amount declaration keeps today's behaviour.
307342
- **Every converting write adds a rate lookup.**
308343
- **The fraction-digit values may get their first reader.** That depends on [Open item](#open-items) 3.
344+
*Note (2026-11-04):* they did, in PR #28302 (`6762dd91`): the write-time home projection rounds to them, read as
345+
CLDR fraction digits rather than ISO 4217 minor units ([Open item](#open-items) 3;
346+
`packages/objectql/src/home-amount-projection.ts#projectHomeAmounts`).
309347
- **The docs start from a true baseline.** #27158 (closed) removed the two docs claims of exchange rates and a stored
310348
currency code. At `cdec7473`, `git grep -n -E "Money with exchange rates|Store amount \+ currency code" -- content/docs`
311349
prints 0 lines. The M0-5 currency page (#27189) builds on that baseline.
@@ -333,18 +371,80 @@ Where a source leans one way, the lean is quoted here, not adopted. This record
333371
the document date. The measurement round also rules out a "nearest-future rate". The design record §5 says
334372
「⛔ 不取最近一条」 (do not take the most recent row). The predicate fixes what counts as missing, and #27192 pins it
335373
with tests.
374+
*Note (2026-11-04):* settled, as workload row M2-2 read it: the row is the organization's `sys_exchange_rate` row
375+
for the record's currency with the latest `effective_date` on or before the document date; a later row never
376+
applies, and no row in force refuses `ERR_EXCHANGE_RATE_MISSING` (D5, item 9). Landed in PR #28302 (`6762dd91`):
377+
the engine reads `where: { currency, effective_date: { $lte: onOrBefore } }` with `onOrBefore` the document day,
378+
`orderBy: [{ field: 'effective_date', order: 'desc' }]`, `limit: 1`, as a system read in the row's own
379+
organization, so one organization's rate never converts another's amount
380+
(`packages/objectql/src/engine.ts#homeAmountServing`); a row whose `rate` is not a positive decimal counts
381+
as missing, and a batch asks the table once per organization, currency and day
382+
(`packages/objectql/src/home-amount-projection.ts#projectHomeAmounts`, `#memoizeHomeAmountServing`). The document
383+
date is the day of the field `homeAmount.rateDate` names: a `date` value is its own day; a `datetime` value is its
384+
calendar day in the organization's `localization.timezone`, UTC when the setting is unset or unknown to the host,
385+
never the writer's request zone, so a REST write, an import and a flow pick the same rate for the same stored
386+
instant — ruling [`6539345871`](https://github.com/objectstack-ai/objectstack/issues/8345#issuecomment-6539345871) on #8345, letter B
387+
(`packages/objectql/src/home-amount-projection.ts#documentDateOf`; the zone rides the same settings bridge as the
388+
home currency, item 4). Not chosen: A, the UTC day now and the organization's zone as a later card, a semantic
389+
change over stored rows once released. A foreign-currency amount with a blank document date is refused
390+
`VALIDATION_FAILED` with `required` on the date field (item 9). Review: contract review
391+
[`6539117549`](https://github.com/objectstack-ai/objectstack/pull/28302#issuecomment-6539117549) and its delta
392+
[`6539668963`](https://github.com/objectstack-ai/objectstack/pull/28302#issuecomment-6539668963) on PR #28302.
336393
3. **Arithmetic and rounding of the home amount** (#27192). The design record §5 and workload row M2-2 round to the
337394
minor units of the home currency's ISO 4217 code, read from `CURRENCY_FRACTION_DIGITS`. The epic body's risk note
338395
says money is computed in minor-unit integers or decimals, not floating point (「浮点算钱用最小单位整数或 decimal」).
339396
Neither point is in the ruling.
397+
*Note (2026-11-04):* settled, with one correction to the sentence above: the digit count is CLDR's, not ISO 4217's.
398+
Landed in PR #28302 (`6762dd91`). The home amount is the record amount divided by the rate (D7 parameter 3), in
399+
exact scaled-decimal arithmetic over `BigInt` with no float step, rounded **once, half away from zero** (accounting
400+
`HALF_UP`; `1.005` stores `1.01`, `-0.125` stores `-0.13`) to the home currency's fraction digits, and stored as the
401+
rounded decimal's own spelling, the bare number ADR-0104 D1 requires
402+
(`packages/objectql/src/home-amount-projection.ts#divideToPlaces`, `#divideRoundHalfAwayFromZero`, `#toScaledDecimal`).
403+
The rate is stored in `homeAmount.rateField` **unrounded**, as the rate row holds it, so the
404+
stored home amount reproduces from the stored amount and rate
405+
(`packages/objectql/src/home-amount-projection.ts#projectHomeAmounts`); a `scale` declared on that field reaches
406+
display only, since every numeric column takes the one `decimal(65, 30)` representation
407+
(`packages/spec/src/data/numeric-column-representation.ts#numericColumnFor`), and whether the spec should refuse a
408+
`rateField` scale below 12 is spec-lane work the review escalated, not decided here. The digit count is
409+
`CURRENCY_FRACTION_DIGITS[home]`, which is **CLDR's fraction digits** (`currencyData`, a checked-in snapshot), not
410+
ISO 4217 minor units: for about seventeen codes the two differ — `IQD` 0 where ISO 4217 says 3; `HUF`, `COP`, `IDR`,
411+
`PKR` and other codes whose minor unit CLDR records as unused 0 where ISO 4217 says 2 — and CLDR's count applies
412+
(JPY 0, USD and CNY 2, KWD 3; USD 10,000 at 0.1389 into CNY stores 71994.24). The table is exported from
413+
`@objectstack/spec/data` for this one reader (`packages/spec/src/data/index.ts`). The contract review flagged the
414+
"ISO 4217 minor units" wording and recommended CLDR
415+
([`6539117549`](https://github.com/objectstack-ai/objectstack/pull/28302#issuecomment-6539117549), F1 and should-fix S2); ruling `6539345871` ordered that
416+
wording corrected per S2 before #28302 merged, and this note carries the same correction.
340417
4. **A home-amount declaration or a converting write while no home currency is set** (#27192, #27198).
341418
`localization.currency` may be unset (its manifest entry says `required: false`, and there is no platform
342419
default). The ruling locks the setting once used. It does not say what happens while the setting is unset. The sources rule out a
343420
silent default (design record §9: no CNY default).
421+
*Note (2026-11-04):* settled by refusal: a converting write while `localization.currency` is unset, or holds no
422+
ISO 4217 code, is refused `ERR_HOME_CURRENCY_UNSET` / 400 before anything is written, and no default is stored or
423+
assumed (`packages/objectql/src/home-amount-projection.ts#HomeCurrencyUnsetError`, thrown by
424+
`packages/objectql/src/home-amount-projection.ts#projectHomeAmounts`). Landed in PR #28302 (`6762dd91`). The
425+
contract review judged the refusal against D5's rejected fallbacks (a null home amount is the one D5 rejects) and
426+
D8 rule 5 (no place in metadata for a default), and gave the condition its own code because its remedy, setting
427+
the setting, differs from a missing rate's, adding a row
428+
([`6539117549`](https://github.com/objectstack-ai/objectstack/pull/28302#issuecomment-6539117549), ③.1;
429+
`packages/spec/src/api/error-code-ledger.zod.ts#ERROR_CODE_LEDGER`). The engine reads the setting for the row's
430+
organization through `ObjectQL.setHomeLocalizationResolver`
431+
(`packages/objectql/src/engine.ts#setHomeLocalizationResolver`), which `ObjectQLPlugin` wires to the `settings`
432+
service's `localization.currency` and `localization.timezone` in one grouped read (`packages/objectql/src/plugin.ts`):
433+
no `settings` service, or an unregistered namespace or key, reads as unset and refuses, while any other settings
434+
error is the write's failure, never an unset setting. The refusal is at write, not at registration: a `homeAmount`
435+
declaration is admitted while the setting is unset, and the first converting write refuses. The lock (#27198, PR
436+
#28297 `291bf44a`) refuses a change to a set value once a rate row or a home-amount declaration exists (D4) and
437+
passes the first setting of an unset value: nothing has been converted into a currency that does not exist
438+
(`packages/services/service-settings/src/localization-currency-lock.ts#localizationCurrencyWritePredicate`).
344439
5. **The unit of parameter 4.** Settled, and moved to D7: scale 12, ruling `6469382727` on #27185. The entry stays so
345440
that the numbers of the items below do not change.
346441
6. **A record currency equal to the home currency** (#27192). Workload row M2-2 reads "same-currency-as-home ⇒ rate
347442
1, no lookup". The ruling does not say whether such a write needs a rate row.
443+
*Note (2026-11-04):* settled as the row reads it: a record currency equal to the home currency converts at rate `1`
444+
with no rate lookup, so no rate row is needed and none is read; the home amount is still rounded to the home
445+
currency's fraction digits (item 3), and the rate field stores `1`
446+
(`packages/objectql/src/home-amount-projection.ts#projectHomeAmounts`, the arm where the code equals the home
447+
currency). Landed in PR #28302 (`6762dd91`).
348448
7. **Uniqueness and audit of rate rows** (#27185). The design record §5 and the measurement round make rows unique per
349449
organization, currency and date. Workload row M1-1 says "unique (currency, effective_date); tenant scope;
350450
admin-only permission set; audit on". Parameter 2 fixes only who edits.
@@ -354,6 +454,26 @@ Where a source leans one way, the lean is quoted here, not adopted. This record
354454
9. **Refusal codes and messages** (#27186, #27187, #27192). The workload rows draft `ERR_CURRENCY_CODE_REQUIRED` and
355455
`ERR_EXCHANGE_RATE_MISSING`. The codes and their envelope follow ADR-0112. The missing-rate refusal names the
356456
currency and the date (design record §5, workload row M2-2).
457+
*Note (2026-11-04):* settled: three codes are registered in ADR-0112's extension ledger under
458+
`@objectstack/objectql` (`packages/spec/src/api/error-code-ledger.zod.ts#ERROR_CODE_LEDGER`), each a 400 answered
459+
before the driver is asked to write the row. `ERR_CURRENCY_CODE_REQUIRED`, a `record`-mode amount beside a blank
460+
code (#27186, PR #27339 `57c51aab`;
461+
`packages/objectql/src/validation/record-currency-code.ts#RecordCurrencyCodeRequiredError`, whose `field` names
462+
the code field to fill). `ERR_EXCHANGE_RATE_MISSING`, no rate row in force on the document date (item 2): the
463+
message names the currency, the date, the home currency and Setup → Exchange Rates, and says a missing rate is
464+
never replaced by 1, by a later rate or by an empty home amount
465+
(`packages/objectql/src/home-amount-projection.ts#ExchangeRateMissingError`). `ERR_HOME_CURRENCY_UNSET`, no usable
466+
home currency (item 4; `packages/objectql/src/home-amount-projection.ts#HomeCurrencyUnsetError`). The last two
467+
landed in PR #28302 (`6762dd91`). Two further refusals reuse the existing `VALIDATION_FAILED` vocabulary rather
468+
than a new code: `required` on the document-date field when a foreign-currency amount has no document date, and
469+
`invalid_value` when one predicate update would convert the matched rows to different home amounts, which one
470+
statement cannot store, so those rows are updated by id
471+
(`packages/objectql/src/home-amount-projection.ts#settleBulkHomeAmounts`). The analytics doors (#27187) refuse
472+
with existing codes, as the Status block says. `validate()` previews the same projection and answers each refusal
473+
as the row's finding with the same code, so a dry run refuses what the write refuses
474+
(`packages/objectql/src/engine-references-and-master-detail.ts#previewHomeAmounts`,
475+
`packages/objectql/src/engine-validate.ts`). With the third code, the Status block's "only ledger additions"
476+
sentence is corrected above.
357477
10. **Which aggregates count as "raw"** (#27186, #27187). The sources list `sum`, `avg`, `min` and `max`; `count`
358478
reads no amount. The per-row `currency` on grouped analytics results (workload row M0-3) is the contract half of
359479
#27187.

0 commit comments

Comments
 (0)