From 244f2ff0593874d699d0fb7dc41db3e1850bf661 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 4 Nov 2026 20:19:37 +0000 Subject: [PATCH 1/2] =?UTF-8?q?docs(adr):=20ADR-0144=20settles=20Open=20it?= =?UTF-8?q?ems=202,=203,=204,=206=20and=209,=20records=20ruling=2065393458?= =?UTF-8?q?71=20(A=E2=80=B2=20and=20B),=20the=20rounding=20mode=20and=20CL?= =?UTF-8?q?DR=20fraction=20digits?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Each settled Open item gets a dated note in the form PR #28181 used for items 11 to 13, citing the tree on main at f68b47ca by file#symbol: the rate row is the latest effective_date on or before the document date (item 2), the home amount is amount ÷ rate in exact decimal, rounded once, half away from zero, to the home currency's CLDR fraction digits with the rate stored unrounded (item 3), an unset home currency refuses ERR_HOME_CURRENCY_UNSET (item 4), the same currency converts at 1 with no lookup (item 6), and three codes are in the ledger (item 9). D9 and the TL;DR record ruling 6539345871 letter A′ (18.3.0 shipped R1 and the R2 core together; #27193, #27190, #27196 are R2.1), and Open item 2 records letter B (a datetime document date is its calendar day in the organization's localization.timezone). The Context and Consequences lines on the fraction-digit table say CLDR fraction digits, not ISO 4217 minor units, and the "only ledger additions" sentence is corrected to three codes. Open item 13 is untouched. Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_01Utp6CaFs7UbZV5xCiu37Kk --- ...-dated-rates-write-time-home-projection.md | 122 +++++++++++++++++- 1 file changed, 119 insertions(+), 3 deletions(-) diff --git a/docs/adr/0144-multi-currency-record-currency-dated-rates-write-time-home-projection.md b/docs/adr/0144-multi-currency-record-currency-dated-rates-write-time-home-projection.md index de0307382f6..a9089b417e6 100644 --- a/docs/adr/0144-multi-currency-record-currency-dated-rates-write-time-home-projection.md +++ b/docs/adr/0144-multi-currency-record-currency-dated-rates-write-time-home-projection.md @@ -11,8 +11,12 @@ [`6469382727`](https://github.com/objectstack-ai/objectstack/issues/27185#issuecomment-6469382727) on #27185. The maintainer chose the option 「小数点后 12 位(推荐)」. - **Builds on**: [ADR-0104](./0104-field-runtime-value-shape-contract.md) D1 (a `currency` value is a bare number, kept here). [ADR-0112](./0112-error-code-vocabulary-and-ledger.md) governs every refusal that answers on the wire. - Two of them need new codes, a blank record currency code and a missing rate ([Open item](#open-items) 9): ADR-0112 - sets their codes and envelope, and both are registered in its extension ledger. They are the only ledger additions. + Three of them need new codes, a blank record currency code, a missing rate and an unset home currency + ([Open items](#open-items) 4 and 9): ADR-0112 sets their codes and envelope, and all three are registered in its + extension ledger (`packages/spec/src/api/error-code-ledger.zod.ts#ERROR_CODE_LEDGER`, under `@objectstack/objectql`). + *Note (2026-11-04):* until Open item 4 settled by refusal (`ERR_HOME_CURRENCY_UNSET`, PR #28302, `6762dd91`) this + sentence named two codes and called them "the only ledger additions"; the third code is that settlement's, and the + "only" claim is dropped. "At build" in D1 and D6 covers the declared aggregates: a `summary` roll-up refuses at registration and a dataset measure at compile, with existing codes (`INVALID_METADATA` at registration, `DATASET_INVALID` at compile). An ad-hoc 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 @@ shape verdict [`6041482631`](https://github.com/objectstack-ai/objectstack/issues/8345#issuecomment-6041482631); [#27119](https://github.com/objectstack-ai/objectstack/issues/27119) (design record, workload rows, ruling); this record's card [#27183](https://github.com/objectstack-ai/objectstack/issues/27183). Every claim below about current - behaviour cites a file or symbol as read on `origin/main` at `cdec7473`. + behaviour cites a file or symbol as read on `origin/main` at `cdec7473`; the dated notes of 2026-11-04 (Open items + 2, 3, 4, 6 and 9, D9) read `origin/main` at `f68b47ca`, and each names the pull request that landed its answer. --- @@ -51,6 +56,9 @@ Metadata never names the home currency. - Delivery comes in three releases. R1 ships the record currency, the refusals and the per-row display. R2 ships the rates, the projection and the recompute. R3 is proof and polish. R1 and R2 each ship as one Version Packages release. + *Note (2026-11-04):* 18.3.0 shipped R1 and the R2 core (the rate table, the `homeAmount` key, the write-time + projection and the home-currency lock) in one Version Packages release, by ruling `6539345871`, letter A′; the + recompute, the import check and the home field's display follow as R2.1. See D9. ## Provenance — what is ruled here, and what is not @@ -94,6 +102,11 @@ aggregation. The ruling names them by topic ("Q0 A", "rates A", "home currency A membership in `CURRENCY_FRACTION_DIGITS` (`packages/spec/src/data/currency-fraction-digits.ts#CURRENCY_FRACTION_DIGITS`). Only that table's key set is read. Its fraction-digit values have no reader. + *Note (2026-11-04):* the values have their reader since PR #28302 (`6762dd91`): the write-time home projection + rounds the home amount to them ([Open item](#open-items) 3), through the table's export from `@objectstack/spec/data` + (`packages/spec/src/data/index.ts`). They are CLDR fraction digits, a checked-in `currencyData` snapshot as the + table's own header records, not ISO 4217 minor units: for about seventeen codes the two differ (`IQD` 0 against 3; + `HUF`, `COP`, `IDR`, `PKR` and other codes whose minor unit CLDR records as unused, 0 against 2). - **No conversion is implemented.** `git grep -n -i -E "exchange.?rate|fx_rate|currency_conversion|convertCurrency" -- packages/ ':!*.test.ts'` prints 0 lines. Control: `git grep -c currencyConfig -- packages/spec/src/data/field.zod.ts` prints 13. @@ -275,6 +288,25 @@ docs page. Under R2 it files #27194, M2-4. Those placements are the epic PM's, n R1 and R2 pull request "⛔ not released alone — pairs with …". Under Prime Directive #15, the maintainer merges Version Packages, and the epic body says that happens only when a pair is complete. +*Note (2026-11-04):* amended by ruling [`6539345871`](https://github.com/objectstack-ai/objectstack/issues/8345#issuecomment-6539345871) on #8345, letter A′, +maintainer 「同意」 (2026-10-31T05:03Z). **18.3.0 carries R1 and the R2 core together.** Version Packages #26517 +waited for PR #28302 and PR #28297 to land, and its version commit `83d7e4ae` (merged 2026-11-03) consumed +`sys_exchange_rate` (#27185, PR #27246 `194e66c0`), `currencyConfig.homeAmount` (#27191, PR #28096 `73b72709`), the +write-time home projection (#27192, PR #28302 `6762dd91`) and the `localization.currency` lock (#27198, PR #28297 +`291bf44a`); each package's `## 18.3.0` section names its entry (`packages/objectql/CHANGELOG.md`, +`packages/spec/CHANGELOG.md`, `packages/platform-objects/CHANGELOG.md`, +`packages/services/service-settings/CHANGELOG.md`), and so does `content/docs/releases/v18/18-3.mdx`. The pairing +rule above holds as written: no declaration shipped without the engine that fills it. What the ruling changes is the +release gate of ruling [`6522614678`](https://github.com/objectstack-ai/objectstack/issues/8345#issuecomment-6522614678) on #8345 (letter A, 2026-10-30: R2 is +built now and released once the customer confirms the consolidation terms of Context §2), in one respect: the R2 core +was released ahead of that confirmation, because the three terms are configuration axes — `localization.currency`, +the rate table's `source` values, the author's `rateDate` pointer — that the engine hard-codes none of, so the +customer has no R2 rows to recompute. **R2.1** is #27193 (the recompute), #27190 (the import check) and #27196 (the +home field's read-only display), released in a later Version Packages; none is a half-state, since the home and rate +fields are `readonly` and render as such. Not chosen: A (merge #26517 before #28302, the half-state this section +forbids), B (hold #26517 until the customer confirms), C (a registration-time refusal of `homeAmount` as a release +switch in code). + ## Non-goals These are outside this decision, and each would need a decision of its own. The first eight are the workload rows' @@ -306,6 +338,9 @@ D8 already excludes a switch, injected columns, fixed field names and a setting row and no home-amount declaration keeps today's behaviour. - **Every converting write adds a rate lookup.** - **The fraction-digit values may get their first reader.** That depends on [Open item](#open-items) 3. + *Note (2026-11-04):* they did, in PR #28302 (`6762dd91`): the write-time home projection rounds to them, read as + CLDR fraction digits rather than ISO 4217 minor units ([Open item](#open-items) 3; + `packages/objectql/src/home-amount-projection.ts#projectHomeAmounts`). - **The docs start from a true baseline.** #27158 (closed) removed the two docs claims of exchange rates and a stored currency code. At `cdec7473`, `git grep -n -E "Money with exchange rates|Store amount \+ currency code" -- content/docs` prints 0 lines. The M0-5 currency page (#27189) builds on that baseline. @@ -333,18 +368,79 @@ Where a source leans one way, the lean is quoted here, not adopted. This record the document date. The measurement round also rules out a "nearest-future rate". The design record §5 says 「⛔ 不取最近一条」 (do not take the most recent row). The predicate fixes what counts as missing, and #27192 pins it with tests. + *Note (2026-11-04):* settled, as workload row M2-2 read it: the row is 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, and no row in force refuses `ERR_EXCHANGE_RATE_MISSING` (D5, item 9). Landed in PR #28302 (`6762dd91`): + the engine reads `where: { currency, effective_date: { $lte: day } }`, ordered by `effective_date` descending, + `limit: 1`, as a system read in the row's own organization, so one organization's rate never converts another's + amount (`packages/objectql/src/engine.ts#homeAmountServing`); a row whose `rate` is not a positive decimal counts + as missing, and a batch asks the table once per organization, currency and day + (`packages/objectql/src/home-amount-projection.ts#projectHomeAmounts`, `#memoizeHomeAmountServing`). The document + date is the day of the field `homeAmount.rateDate` names: a `date` value is its own day; a `datetime` value is its + calendar day in the organization's `localization.timezone`, UTC when the setting is unset or unknown to the host, + never the writer's request zone, so a REST write, an import and a flow pick the same rate for the same stored + instant — ruling [`6539345871`](https://github.com/objectstack-ai/objectstack/issues/8345#issuecomment-6539345871) on #8345, letter B + (`packages/objectql/src/home-amount-projection.ts#documentDateOf`; the zone rides the same settings bridge as the + home currency, item 4). Not chosen: A, the UTC day now and the organization's zone as a later card, a semantic + change over stored rows once released. A foreign-currency amount with a blank document date is refused + `VALIDATION_FAILED` with `required` on the date field (item 9). Review: contract review + [`6539117549`](https://github.com/objectstack-ai/objectstack/pull/28302#issuecomment-6539117549) and its delta + [`6539668963`](https://github.com/objectstack-ai/objectstack/pull/28302#issuecomment-6539668963) on PR #28302. 3. **Arithmetic and rounding of the home amount** (#27192). The design record §5 and workload row M2-2 round to the minor units of the home currency's ISO 4217 code, read from `CURRENCY_FRACTION_DIGITS`. The epic body's risk note says money is computed in minor-unit integers or decimals, not floating point (「浮点算钱用最小单位整数或 decimal」). Neither point is in the ruling. + *Note (2026-11-04):* settled, with one correction to the sentence above: the digit count is CLDR's, not ISO 4217's. + Landed in PR #28302 (`6762dd91`). The home amount is the record amount divided by the rate (D7 parameter 3), in + exact scaled-decimal arithmetic over `BigInt` with no float step, rounded **once, half away from zero** (accounting + `HALF_UP`; `1.005` stores `1.01`, `-0.125` stores `-0.13`) to the home currency's fraction digits, and stored as the + rounded decimal's own spelling, the bare number ADR-0104 D1 requires + (`packages/objectql/src/home-amount-projection.ts#divideToPlaces`, `#divideRoundHalfAwayFromZero`, `#toScaledDecimal`). + The rate is stored in `homeAmount.rateField` **unrounded**, as the rate row holds it, so the + stored home amount reproduces from the stored amount and rate + (`packages/objectql/src/home-amount-projection.ts#projectHomeAmounts`); a `scale` declared on that field reaches + display only, since every numeric column takes the one `decimal(65, 30)` representation + (`packages/spec/src/data/numeric-column-representation.ts#numericColumnFor`), and whether the spec should refuse a + `rateField` scale below 12 is spec-lane work the review escalated, not decided here. The digit count is + `CURRENCY_FRACTION_DIGITS[home]`, which is **CLDR's fraction digits** (`currencyData`, a checked-in snapshot), not + ISO 4217 minor units: for about seventeen codes the two differ — `IQD` 0 where ISO 4217 says 3; `HUF`, `COP`, `IDR`, + `PKR` and other codes whose minor unit CLDR records as unused 0 where ISO 4217 says 2 — and CLDR's count applies + (JPY 0, USD and CNY 2, KWD 3; USD 10,000 at 0.1389 into CNY stores 71994.24). The table is exported from + `@objectstack/spec/data` for this one reader (`packages/spec/src/data/index.ts`). The contract review flagged the + "ISO 4217 minor units" wording and recommended CLDR + ([`6539117549`](https://github.com/objectstack-ai/objectstack/pull/28302#issuecomment-6539117549), F1 and should-fix S2); ruling `6539345871` ordered that + wording corrected per S2 before #28302 merged, and this note carries the same correction. 4. **A home-amount declaration or a converting write while no home currency is set** (#27192, #27198). `localization.currency` may be unset (its manifest entry says `required: false`, and there is no platform default). The ruling locks the setting once used. It does not say what happens while the setting is unset. The sources rule out a silent default (design record §9: no CNY default). + *Note (2026-11-04):* settled by refusal: a converting write while `localization.currency` is unset, or holds no + ISO 4217 code, is refused `ERR_HOME_CURRENCY_UNSET` / 400 before anything is written, and no default is stored or + assumed (`packages/objectql/src/home-amount-projection.ts#HomeCurrencyUnsetError`, thrown by + `packages/objectql/src/home-amount-projection.ts#projectHomeAmounts`). Landed in PR #28302 (`6762dd91`). The + contract review judged the refusal against D5's rejected fallbacks (a null home amount is the one D5 rejects) and + D8 rule 5 (no place in metadata for a default), and gave the condition its own code because its remedy, setting + the setting, differs from a missing rate's, adding a row + ([`6539117549`](https://github.com/objectstack-ai/objectstack/pull/28302#issuecomment-6539117549), ③.1; + `packages/spec/src/api/error-code-ledger.zod.ts#ERROR_CODE_LEDGER`). The engine reads the setting for the row's + organization through `ObjectQL.setHomeLocalizationResolver` + (`packages/objectql/src/engine.ts#setHomeLocalizationResolver`), which `ObjectQLPlugin` wires to the `settings` + service's `localization.currency` and `localization.timezone` in one grouped read (`packages/objectql/src/plugin.ts`): + no `settings` service, or an unregistered namespace or key, reads as unset and refuses, while any other settings + error is the write's failure, never an unset setting. The refusal is at write, not at registration: a `homeAmount` + declaration is admitted while the setting is unset, and the first converting write refuses. The lock (#27198, PR + #28297 `291bf44a`) refuses a change to a set value once a rate row or a home-amount declaration exists (D4) and + passes the first setting of an unset value: nothing has been converted into a currency that does not exist + (`packages/services/service-settings/src/localization-currency-lock.ts#localizationCurrencyWritePredicate`). 5. **The unit of parameter 4.** Settled, and moved to D7: scale 12, ruling `6469382727` on #27185. The entry stays so that the numbers of the items below do not change. 6. **A record currency equal to the home currency** (#27192). Workload row M2-2 reads "same-currency-as-home ⇒ rate 1, no lookup". The ruling does not say whether such a write needs a rate row. + *Note (2026-11-04):* settled as the row reads it: a record currency equal to the home currency converts at rate `1` + with no rate lookup, so no rate row is needed and none is read; the home amount is still rounded to the home + currency's fraction digits (item 3), and the rate field stores `1` + (`packages/objectql/src/home-amount-projection.ts#projectHomeAmounts`, the arm where the code equals the home + currency). Landed in PR #28302 (`6762dd91`). 7. **Uniqueness and audit of rate rows** (#27185). The design record §5 and the measurement round make rows unique per organization, currency and date. Workload row M1-1 says "unique (currency, effective_date); tenant scope; admin-only permission set; audit on". Parameter 2 fixes only who edits. @@ -354,6 +450,26 @@ Where a source leans one way, the lean is quoted here, not adopted. This record 9. **Refusal codes and messages** (#27186, #27187, #27192). The workload rows draft `ERR_CURRENCY_CODE_REQUIRED` and `ERR_EXCHANGE_RATE_MISSING`. The codes and their envelope follow ADR-0112. The missing-rate refusal names the currency and the date (design record §5, workload row M2-2). + *Note (2026-11-04):* settled: three codes are registered in ADR-0112's extension ledger under + `@objectstack/objectql` (`packages/spec/src/api/error-code-ledger.zod.ts#ERROR_CODE_LEDGER`), each a 400 answered + before the driver is asked to write the row. `ERR_CURRENCY_CODE_REQUIRED`, a `record`-mode amount beside a blank + code (#27186, PR #27339 `57c51aab`; + `packages/objectql/src/validation/record-currency-code.ts#RecordCurrencyCodeRequiredError`, whose `field` names + the code field to fill). `ERR_EXCHANGE_RATE_MISSING`, no rate row in force on the document date (item 2): the + message names the currency, the date, the home currency and Setup → Exchange Rates, and says a missing rate is + never replaced by 1, by a later rate or by an empty home amount + (`packages/objectql/src/home-amount-projection.ts#ExchangeRateMissingError`). `ERR_HOME_CURRENCY_UNSET`, no usable + home currency (item 4; `packages/objectql/src/home-amount-projection.ts#HomeCurrencyUnsetError`). The last two + landed in PR #28302 (`6762dd91`). Two further refusals reuse the existing `VALIDATION_FAILED` vocabulary rather + than a new code: `required` on the document-date field when a foreign-currency amount has no document date, and + `invalid_value` when one predicate update would convert the matched rows to different home amounts, which one + statement cannot store, so those rows are updated by id + (`packages/objectql/src/home-amount-projection.ts#settleBulkHomeAmounts`). The analytics doors (#27187) refuse + with existing codes, as the Status block says. `validate()` previews the same projection and answers each refusal + as the row's finding with the same code, so a dry run refuses what the write refuses + (`packages/objectql/src/engine-references-and-master-detail.ts#previewHomeAmounts`, + `packages/objectql/src/engine-validate.ts`). With the third code, the Status block's "only ledger additions" + sentence is corrected above. 10. **Which aggregates count as "raw"** (#27186, #27187). The sources list `sum`, `avg`, `min` and `max`; `count` reads no amount. The per-row `currency` on grouped analytics results (workload row M0-3) is the contract half of #27187. From a0c8d7ca9451da2185b167731d1004108512f05b Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 4 Nov 2026 20:38:31 +0000 Subject: [PATCH 2/2] docs(adr): ADR-0144 Provenance names ruling 6539345871 and the f68b47ca reading; Open item 2 quotes the rate predicate verbatim MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two should-fixes from the contract review of the amendment, prose only. Provenance §1 says that ruling 6539345871 amends D9's release gate (A′) and settles Open item 2's datetime day (B), carried as dated notes, and §3 says the 2026-11-04 notes read origin/main at f68b47ca. The Open item 2 note quotes the engine's rate predicate as written in homeAmountServing (onOrBefore, the orderBy clause, limit 1) instead of a paraphrase in code spans. Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_01Utp6CaFs7UbZV5xCiu37Kk --- ...rency-dated-rates-write-time-home-projection.md | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/docs/adr/0144-multi-currency-record-currency-dated-rates-write-time-home-projection.md b/docs/adr/0144-multi-currency-record-currency-dated-rates-write-time-home-projection.md index a9089b417e6..dbd24c54d60 100644 --- a/docs/adr/0144-multi-currency-record-currency-dated-rates-write-time-home-projection.md +++ b/docs/adr/0144-multi-currency-record-currency-dated-rates-write-time-home-projection.md @@ -66,12 +66,15 @@ Three kinds of text feed this record. Each section says which kind it carries. 1. **Ruled.** Ruling `6465431966` fixes shape C′, sub-questions Q0–Q4, parameters 1–4, the five design rules, the release pairing and the commitment. Ruling `6469382727` on #27185 fixes parameter 4's unit. [Decision](#decision) - carries only these two. It quotes them wherever the wording matters. + carries only these two. It quotes them wherever the wording matters. Ruling `6539345871` on #8345 (2026-10-31) + amends D9's release gate (letter A′) and settles [Open item](#open-items) 2's `datetime` day (letter B); D9 and + Open item 2 carry it as dated notes, and nothing else in [Decision](#decision) rests on it. 2. **Analysis the ruling read.** This is the design record `6464438764`, the workload rows `6464936321`, the measurement round `6041378776` and the shape verdict `6041482631`. They supply the context, the shapes considered and the per-card plan. A detail that appears only there is not ruled. This record lists such details under [Open items](#open-items) or as a card's draft, never as a decision. -3. **Readings.** Current behaviour, measured at `cdec7473` and cited by file or symbol. +3. **Readings.** Current behaviour, measured at `cdec7473` and cited by file or symbol. The dated notes of + 2026-11-04 read `origin/main` at `f68b47ca` the same way. **Two numberings meet here.** #27119 is a decision batch, and two of its items are also called Q1 and Q2. #27119 Q1 asks when multi-currency starts, and the answer was 乙-现在. #27119 Q2 asks for one rule for an unknown currency, and @@ -371,9 +374,10 @@ Where a source leans one way, the lean is quoted here, not adopted. This record *Note (2026-11-04):* settled, as workload row M2-2 read it: the row is 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, and no row in force refuses `ERR_EXCHANGE_RATE_MISSING` (D5, item 9). Landed in PR #28302 (`6762dd91`): - the engine reads `where: { currency, effective_date: { $lte: day } }`, ordered by `effective_date` descending, - `limit: 1`, as a system read in the row's own organization, so one organization's rate never converts another's - amount (`packages/objectql/src/engine.ts#homeAmountServing`); a row whose `rate` is not a positive decimal counts + the engine reads `where: { currency, effective_date: { $lte: onOrBefore } }` with `onOrBefore` the document day, + `orderBy: [{ field: 'effective_date', order: 'desc' }]`, `limit: 1`, as a system read in the row's own + organization, so one organization's rate never converts another's amount + (`packages/objectql/src/engine.ts#homeAmountServing`); a row whose `rate` is not a positive decimal counts as missing, and a batch asks the table once per organization, currency and day (`packages/objectql/src/home-amount-projection.ts#projectHomeAmounts`, `#memoizeHomeAmountServing`). The document date is the day of the field `homeAmount.rateDate` names: a `date` value is its own day; a `datetime` value is its