From 44927ce881d6a4443c3e85e6c430326531eca24f Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 28 Oct 2026 11:29:36 +0000 Subject: [PATCH 01/10] =?UTF-8?q?docs(adr):=20ADR-0144=20multi-currency=20?= =?UTF-8?q?=E2=80=94=20record=20currency,=20dated=20rates,=20write-time=20?= =?UTF-8?q?home=20projection;=20dated=20note=20on=20ADR-0104?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Records ruling 6465431966 on #27119 (shape C′, sub-questions Q0–Q4, parameters 1–4, the five design rules, the R1/R2/R3 release pairing) as an Accepted decision record. Details found only in the analysis the ruling read are listed as open items for the implementing cards. ADR-0104 gains a dated note under D1's "Reality wins" paragraph: the CurrencyValueSchema deletion it records is still pending (#27182), and ADR-0144 keeps the bare-number value. The ADR index is regenerated by scripts/adr-index.mjs. Claude-Session: https://claude.ai/code/session_01Utp6CbTZrFqRYNgjSaSbN5 Co-authored-by: Claude --- ...0104-field-runtime-value-shape-contract.md | 11 + ...-dated-rates-write-time-home-projection.md | 343 ++++++++++++++++++ docs/adr/README.md | 7 +- 3 files changed, 358 insertions(+), 3 deletions(-) create mode 100644 docs/adr/0144-multi-currency-record-currency-dated-rates-write-time-home-projection.md diff --git a/docs/adr/0104-field-runtime-value-shape-contract.md b/docs/adr/0104-field-runtime-value-shape-contract.md index 906c53e1e06..a5f762ddd9f 100644 --- a/docs/adr/0104-field-runtime-value-shape-contract.md +++ b/docs/adr/0104-field-runtime-value-shape-contract.md @@ -170,6 +170,17 @@ either adopted by the contract and enforced, or deleted — it does not remain exported-but-dead. An exported-but-unconsumed value schema is exactly the inert metadata ADR-0078 forbids. +> **Note 2026-10-28 (#27183; [ADR-0144](./0144-multi-currency-record-currency-dated-rates-write-time-home-projection.md)).** +> This paragraph's currency sentence needs two corrections. +> First, the deletion it records has not happened yet. At `cdec7473`, +> `CurrencyValueSchema` is still exported and marked `@deprecated` +> (`packages/spec/src/data/field.zod.ts:581`, its type at `:3062`). The +> retirement is carried by #27182, which records its landing here. +> Second, multi-currency keeps this decision. ADR-0144 (ruling `6465431966` +> on #27119) puts a record's currency in a sibling ISO 4217 code field that +> the amount names through `currencyField`, never in the value, so a +> `currency` value stays a bare number. + **Consumers converge on the contract** (each keeps its role, loses its private type lists): 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 new file mode 100644 index 00000000000..6a11e69d856 --- /dev/null +++ b/docs/adr/0144-multi-currency-record-currency-dated-rates-write-time-home-projection.md @@ -0,0 +1,343 @@ +# ADR-0144: Multi-currency — a record currency in a sibling code field, dated exchange rates, and a home-currency amount computed and stored at write + +**Status**: Accepted — 2026-10-28, ruled on objectstack#27119 (ruling [`6465431966`](https://github.com/objectstack-ai/objectstack/issues/27119#issuecomment-6465431966), decision batch #421 item 2, #27119 Q1 = 乙-现在). `docs/adr/**` is Tier H (Prime Directive #14): this record lands by an authorised approval and the maintainer's merge. **This record declares; it implements nothing** — no schema, no code, no changeset. The children of epic #8345 implement it. + +- **Decided by**: the maintainer, 2026-10-28T09:29Z, on #27119, verbatim and untranslated: 「同意立专题卡」 — the epic + form the director seat proposed for 乙-现在. Per the ruling, the maintainer had read the design record + [`6464438764`](https://github.com/objectstack-ai/objectstack/issues/27119#issuecomment-6464438764), the workload list + [`6464936321`](https://github.com/objectstack-ai/objectstack/issues/27119#issuecomment-6464936321) and the seat's answers + in the summon chat. The ruling's own text is the authority this record executes; where the two differ, the ruling + wins and this record is corrected. +- **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) (the codes and envelope of every refusal this + record names). +- **Amends**: no decision. ADR-0104 gains a dated note under D1's "Reality wins" paragraph. The note points here and at + #27182, the card that retires `CurrencyValueSchema`. +- **Evidence**: epic [#8345](https://github.com/objectstack-ai/objectstack/issues/8345), with its measurement round + [`6041378776`](https://github.com/objectstack-ai/objectstack/issues/8345#issuecomment-6041378776) and the spec seat's + 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 and line on `origin/main` at `cdec7473`. + +--- + +## TL;DR + +- An amount field in the new `record` mode names, through `currencyField`, a text field on the same object with + `valueDomain: 'iso_4217_currency'`. That field holds each record's currency. The amount stays a bare number + (ADR-0104 D1). +- `sys_exchange_rate { currency, rate, effective_date, source }` holds dated rates against the organization's home + currency, which is `localization.currency`. In v1, rows are entered by hand or imported. A provider feed is an app flow + that writes rows. +- At write, the engine converts the amount at the document date's rate. It stores the result in a declared home field + (`homeAmount`) together with the rate it used. A missing rate refuses the write. A corrected rate reaches stored + rows only through an explicit recompute. +- A raw aggregate over a record-mode field refuses at build unless it groups by the code field. Consolidated totals + read the stored home field. +- There is no CEL conversion function, no platform or tenant switch, no injected column and no fixed field name. + 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. + +## Provenance — what is ruled here, and what is not + +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. [Decision](#decision) carries only this. It quotes the ruling wherever the + wording matters. +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 and line. + +**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 +the answer was X, in its first record `6464202770`. The shape's own sub-questions Q0–Q4 come from the measurement +round: Q0 asks where a record's currency lives, Q1 covers rates, Q2 the home currency, Q3 conversion timing and Q4 +aggregation. The ruling names them by topic ("Q0 A", "rates A", "home currency A", "conversion timing A", +"aggregation A"). In this record, **Q0–Q4 always mean the sub-questions**. The batch items are spelled "#27119 Q1" and +"#27119 Q2 X". + +## Context + +### 1. What the platform has today (read at `cdec7473`) + +- **A currency value is a bare number** (ADR-0104 D1). `currencyConfig` has two modes + (`packages/spec/src/data/field.zod.ts:558`). In `fixed` mode the field has one currency, `defaultCurrency`. In + `dynamic` mode (the default) amounts display in the tenant default currency. The describe says "Neither mode is a + per-record choice: the value is a bare number either way." No key, column or value carries a record's currency. +- **A `fixed` field with no code becomes a yuan field.** `defaultCurrency` defaults to `'CNY'` (`field.zod.ts:559`). + #27157 (#27119 Q2 X) requires the code and drops the default. It is a member of R1 and a blocker of M0-1. +- **The tenant currency is the `localization.currency` setting** + (`packages/services/service-settings/src/manifests/localization.manifest.ts:105`). It has tenant scope, no platform + default and `valueDomain: 'iso_4217_currency'` (`:124`). It may be unset. It is mutable and keeps no history: probe B + of the measurement round saw a change relabel every stored `dynamic` amount. +- **The ISO 4217 value domain exists** (`packages/spec/src/shared/value-domain.zod.ts:159`). It checks membership in + `CURRENCY_FRACTION_DIGITS` (`packages/spec/src/data/currency-fraction-digits.ts:59`). Only that table's key set is + read. Its fraction-digit values have no reader. +- **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. +- **The CEL standard library is pure.** Its header says: "Nothing registered here may be handed a resolver, a lazy + getter or any other callback into the host" (`packages/formula/src/stdlib.ts:6`). That purity keeps + `objectstack build` byte-stable, so a formula function cannot read a rate table. +- **`CurrencyValueSchema` is still exported**, marked `@deprecated` (`field.zod.ts:581`, its type at `:3062`), although + ADR-0104 D1 says it "is deleted". #27182 retires it. +- **Today's numbers are silently wrong** (from the measurement round's probes over a real ObjectQL engine; the scratch + files were run and deleted and are not re-run for this record). The probes model a record currency the way authors + do today, as a sibling text field. `sum` over a USD row of 100 and an EUR row of 100 answers `200 "USD"`. A `summary` + roll-up over USD 100 and JPY 15000 answers `15100`. A derived measure across a `fixed` USD field and a `fixed` EUR + field answers 200 with no currency. Grouping by the code gives correct per-group values, but the column carries one + currency, so every group gets the same label. + +### 2. The pull + +One named overseas port and terminal customer (2026-08) needs three things: + +1. invoice in the customer's currency; +2. reconcile at the rate of the document's date; +3. consolidate in one home currency. + +In-tree author pull is zero. The measurement round counted 17 currency fields in `examples/**` at `3d9188502e`, and +none has a record currency. The hold of +2026-10-08 ended with this ruling (乙-现在: now, as an epic). The commitment, as ruled: "R1 (about 4–5 agent-days) is +the delivery the customer can rely on; R2 starts after the customer confirms the consolidation terms (home currency, +rate source, which date drives the rate)." + +### 3. The shapes considered + +The spec seat's verdict `6041482631` put four shapes to the maintainer. The design record `6464438764` argued them +again against how Dynamics 365, Odoo, NetSuite, SAP, Salesforce and Airtable model currency. + +| Shape | What it is | Outcome | +|:--|:--|:--| +| A | Guard rails only: refuse today's silent cross-currency arithmetic | Rejected. It meets none of the three needs, and it cannot see the sibling-text workaround it would push authors into. | +| A′ | A, plus a declared record currency (`record` mode and a code field) and the grouped-aggregate refusal | Rejected as the end state. Invoicing works but consolidation does not. The design record advised against stopping here because the contract would have to change again after M0. | +| B | Dated rates and a CEL `convertCurrency()` | Rejected. It conflicts with the stdlib purity invariant. A formula is virtual and aggregates refuse it, so the converted amount can never be totalled. | +| **C′** | A′, plus `sys_exchange_rate` and a home amount computed at write and stored with its rate. No formula function. | **Ruled.** It meets all three needs. Existing aggregate readers total the stored home field without any change to the reader. | + +Q0, where the record currency lives, had four options of its own: + +- **A, a sibling code field.** Ruled. +- B, a hidden column injected by the platform. It is implicit and invisible to AI authors and to build validation. +- C, a value object `{amount, currency}`. It overturns ADR-0104 D1, migrates every stored currency column and revives + `CurrencyValueSchema`. +- D, the status quo of one `fixed` field per currency. It cannot serve an open set of currencies. + +## Decision + +Every item in this section comes from the ruling. Its words are quoted wherever the reading matters. + +### D1 — Shape C′ + +> "a record-mode amount field points at an ISO 4217 code field on the same object; a platform rate table by effective +> date; the home-currency amount computed AT WRITE from the document date's rate and STORED with the rate used; a +> missing rate refuses the write; a raw aggregate over a record-mode field refuses at build unless grouped by its code +> field; consolidation reads the stored home field. No CEL conversion function." + +D2–D6 answer its sub-questions. D7 covers its parameters, D8 the design rules from the summon and D9 the release +pairing. + +### D2 — Q0: the record currency lives in a sibling code field (letter A) + +- An amount field declares `currencyMode: 'record'`. Its `currencyField` names a text field on the same object with + `valueDomain: 'iso_4217_currency'`. That field's value is the record's currency. +- The amount's value stays a bare number, so ADR-0104 D1 is kept, not superseded. `record` is a third mode alongside + `dynamic` and `fixed`. + +### D3 — Q1: dated rates live in `sys_exchange_rate` (letter A) + +- `sys_exchange_rate { currency, rate, effective_date, source }` is a platform system object. +- In v1, rows come from manual entry and import. A provider feed is "by an app flow" that writes rows. The platform + ships no feed. +- A missing rate refuses (D5). + +### D4 — Q2: the home currency is `localization.currency`, locked once used (letter A) + +- The organization's home currency is the existing `localization.currency` setting. There is no second currency + setting. +- It is locked "once a rate row or a home-amount declaration exists". M1-3 (#27198) builds the lock. `service-settings` + has no write-side predicate hook today (workload row M1-3). + +### D5 — Q3: the conversion runs at write and is stored with its rate (letter A) + +- At write, the engine computes the home-currency amount "from the document date's rate". It stores the result in the + declared home field "with the rate used". +- "A missing rate refuses the write." Because the write refuses, no fallback value is stored in place of the missing + one. The design record (§5) lists the fallbacks it rejected: a 1:1 rate, 「最近一条」, and a null home amount. What + counts as missing in the first place is [Open item](#open-items) 2. +- A corrected rate reaches stored rows only through "an explicit recompute operation". Changing a rate row does not, on + its own, rewrite stored home amounts. +- The declaration key is `homeAmount`. The ruling: "the `homeAmount` key never ships ahead of the engine". Workload + row M2-1 drafts its shape as `homeAmount: { field, rateDate, rateField? }`. #27191 fixes the shape under contract + review; see [Open items](#open-items) 1 and 2. + +### D6 — Q4: raw aggregates over a record-mode field refuse, and consolidation reads the home field (letter A) + +- A raw aggregate over a record-mode amount refuses at build unless it groups by that amount's code field. +- Consolidated totals aggregate the stored home field. The home field is an ordinary stored currency field, so the + aggregate readers that exist today total it without any change to the reader (design record §7). Those readers are + the ObjectQL aggregate, `summary` roll-ups, analytics datasets and cubes, grid footers and metric tiles. +- The workload rows place the refusals in existing doors: the ObjectQL aggregate-by-field-type door and `summary` + registration (M0-2, #27186), and the analytics cube measure door (M0-3, #27187). + +### D7 — Parameters 1–4 (the workload's defaults, uncontested) + +| # | Parameter | Ruled value | +|:--|:--|:--| +| 1 | Backfill when a historical rate is missing | Report and skip the row, and apply the rest | +| 2 | Who edits `sys_exchange_rate` | Organization admins only | +| 3 | Rate direction | "1 home unit = `rate` units of the foreign currency" | +| 4 | Precision of `rate` | "12 significant decimals" | + +- **Parameter 3 as arithmetic.** An amount `A` in currency `X`, whose rate row says `r`, is `A ÷ r` home units. The + home amount is the record amount **divided by** `rate`. Workload row M2-2 writes the projection as + "amount × rate", which is the inverse convention. #27192 follows parameter 3. +- **Parameter 1 governs the recompute and backfill** (M2-3, #27193). It does not govern the write path, where D5's + refusal holds. +- The wording of parameter 4 leaves its unit open ([Open item](#open-items) 5). + +### D8 — Design rules recorded from the summon + +1. **No platform or tenant switch.** "the declaration `currencyMode: 'record'` is the switch". Nothing turns + multi-currency on for an organization or a deployment. +2. **No injected columns and no fixed field names.** "every currency-related field is declared by the app author and + referenced by pointer". The naming convention (`currency_code`, `AMOUNT_home`, `AMOUNT_rate`) is guidance in + `skills/**`, "never a contract". +3. **"Enabled currencies" are derived, not a setting.** They are the home currency, plus every currency with a rate + row, plus the codes already used. +4. **The form pre-fills the organization's currency at the renderer, and the stored value is always explicit.** The + organization's currency reaches a new record only as a pre-filled value the user submits. The server never fills + in the code. +5. **Metadata never names the home currency.** "the home field is a `dynamic`-mode currency field", so it displays in + `localization.currency`, as every `dynamic` field does. + + The measurement round and the design record (§9, §10) drafted the home field as `fixed` to the home currency. Rule + 5 replaces that with `dynamic`, and workload row M2-1 already reads `dynamic`. #27191 builds on the ruled `dynamic`. + +### D9 — Release pairing (the only half-state risk) + +As ruled: + +- **R1** = #27157 + this ADR + M0-1..M0-4 "in ONE Version Packages release (a published `record` mode without the + refusals and the per-row display is worse than today)". +- **R2** = M1 + M2-1..M2-3 (+ M3-2's read-only display) "in ONE release (the rate table's only reader is the + write-time projection; the `homeAmount` key never ships ahead of the engine)". M1 "is therefore developed in parallel + but released with M2". +- **R3** = "the rest of M3". + +The cards that carry each release are listed in the epic #8345 body: + +| Release | Ruled members | Cards | +|:--|:--|:--| +| R1 | #27157, the ADR, M0-1..M0-4 | #27157; #27183 (this record, numbered M3-4 in the workload rows); #27184 M0-1; #27186 M0-2; #27187 M0-3; #27188 M0-4 | +| R2 | M1, M2-1..M2-3, M3-2's read-only display | #27185 M1-1; #27190 M1-2; #27198 M1-3; #27191 M2-1; #27192 M2-2; #27193 M2-3; #27196 M3-2 | +| R3 | the rest of M3 | #27195 M3-1; #27197 M3-3 | + +The epic body also files three cards that the ruling's pairing does not list. Under R1 it files #27182, the +`CurrencyValueSchema` retirement (the ruling calls it a prerequisite, and M0-1 is blocked by it), and #27189, the M0-5 +docs page. Under R2 it files #27194, M2-4. Those placements are the epic PM's, not the ruling's. The epic PM marks each +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. + +## Non-goals + +These are outside this decision, and each would need a decision of its own. The first eight are the workload rows' +"What is not in this list", each "a capability with no pull in tree". The last comes from the design record §13. + +- A CEL `convertCurrency()`. The shape rules it out (D1), and it conflicts with `stdlib.ts:6`. +- Read-time conversion. +- A built-in provider feed (ECB or similar). An app flow writes rows instead (D3). +- N×N pair rates. Every rate is against the home currency. +- A per-user display currency. +- Period-end revaluation and unrealised FX gains. +- More than one home currency per organization (multi-entity consolidation). +- Locale rounding rules. +- Rates shared across organizations at platform level. The design record §13 calls this "another decision, not this + phase". + +D8 already excludes a switch, injected columns, fixed field names and a setting for enabled currencies. + +## Consequences + +- **The change is additive.** `record` is a new mode, `dynamic` and `fixed` keep their meaning and stored rows are not + touched. An app that never writes `record` sees no change. The two spec cards that add keys (#27184, #27191) declare + `Metadata-protocol: added`. +- **Two steps cannot be undone once used.** The home-currency lock is one. The other: an object with record-mode + amounts and a home field returns to a single currency only through a data migration (design record §13). +- **Today's relabel hazard closes where the feature is used.** Once the lock holds, a change to `localization.currency` + can no longer relabel stored amounts. An organization with no rate 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. +- **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. + +## Open items + +The ruling does not decide the items below. Each belongs to the card named, and is settled under that card's review. +Where a source leans one way, the lean is quoted here, not adopted. This record is amended with each answer. + +1. **The document-date pointer and the stored-rate pointer: spelling and whether they are required** (#27191). The + ruling fixes that the rate is the document date's and that the rate used is stored. Workload row M2-1 drafts + `homeAmount: { field, rateDate, rateField? }`. In that draft, `rateDate` is "a `date`/`datetime` field on the same + object (⛔ no default)" and `rateField` is optional. The design record §6 says the document date must be declared + explicitly and must not default to `created_at` (「⛔ 不默认 created_at」). An optional `rateField` sits against the + ruling's "STORED with the rate used", and #27191 reconciles the two. +2. **Which row counts as "the document date's rate"** (#27192). The ruling says "a platform rate table by effective + date". Workload row M2-2 and the measurement round read this as the row with the latest `effective_date` on or before + 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. +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. +4. **A home-amount declaration or a converting write while no home currency is set** (#27192, #27198). + `localization.currency` may be unset (`localization.manifest.ts:105`: `required: false`, 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). +5. **The unit of parameter 4** (#27185). "12 significant decimals" can mean 12 significant digits or 12 digits after + the decimal point. The precision and scale of the `rate` column follow from the choice. +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. +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. +8. **A row that a backfill skips** (#27193). Parameter 1 skips and reports a row whose historical rate is missing, + which leaves that row's home field unconverted. The design record §5 warns that `sum` silently ignores an empty home + amount. #27193 decides how the gap stays visible after the report. +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). +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. + +## Verification notes + +Each reading below was taken at `cdec7473`. To re-check one, run its command on a newer `origin/main` and compare. A +changed line number means the file moved, not that the fact changed. Re-read the line before citing it. + +```bash +git grep -n "Neither mode is a per-record choice" -- packages/spec/src/data/field.zod.ts # :558 +git grep -n "default('CNY')" -- packages/spec/src/data/field.zod.ts # :559, until #27157 +git grep -n "export const CurrencyValueSchema" -- packages/spec/src/data/field.zod.ts # :581, until #27182 +git grep -n "may be handed a res" -- packages/formula/src/stdlib.ts # :6 +git grep -n -i -E "exchange.?rate|fx_rate|currency_conversion|convertCurrency" -- packages/ ':!*.test.ts' # 0 lines +git grep -n "valueDomain: 'iso_4217_currency'" -- packages/services/service-settings/src/manifests/localization.manifest.ts # :124 +``` + +## References + +- Ruling: [#27119 `6465431966`](https://github.com/objectstack-ai/objectstack/issues/27119#issuecomment-6465431966). +- Design record: [#27119 `6464438764`](https://github.com/objectstack-ai/objectstack/issues/27119#issuecomment-6464438764). +- Workload rows and parameters: [#27119 `6464936321`](https://github.com/objectstack-ai/objectstack/issues/27119#issuecomment-6464936321). +- Measurement round and shape verdict: [#8345 `6041378776`](https://github.com/objectstack-ai/objectstack/issues/8345#issuecomment-6041378776), + [#8345 `6041482631`](https://github.com/objectstack-ai/objectstack/issues/8345#issuecomment-6041482631). +- Epic and children: [#8345](https://github.com/objectstack-ai/objectstack/issues/8345). Prerequisites outside the + epic (#27119 Q2 X): #27156, #27157, #27158, #26939. diff --git a/docs/adr/README.md b/docs/adr/README.md index d439464da7f..e56cd9279f6 100644 --- a/docs/adr/README.md +++ b/docs/adr/README.md @@ -13,11 +13,11 @@ ## Summary -144 records under `docs/adr/` (139 numbers), 11 under `ui/docs/adr/` (listed separately below). +145 records under `docs/adr/` (140 numbers), 11 under `ui/docs/adr/` (listed separately below). | Status | Records | |---|---| -| Accepted | 125 | +| Accepted | 126 | | Proposed | 5 | | Superseded | 4 | | Draft | 3 | @@ -26,7 +26,7 @@ | Amended | 1 | | Partially implemented | 1 | | Revised | 1 | -| **Total** | **144** | +| **Total** | **145** | ## Shared numbers @@ -211,6 +211,7 @@ A bare `ADR-0006` names the decision; its current text is [0006-project-environm | [ADR-0141](./0141-inter-app-dependency-declared-checked-before-install.md) | An app may depend on another installed app — declared in `manifest.dependencies`, checked once before install… | Proposed — 2026-10-25; first draft 2026-10-24. `docs/adr/` is Tier H (Prim… | 2026-10-25 | — | — | | [ADR-0142](./0142-ownership-queue-membership-claim-and-assignment-rules.md) | An ownership queue is a platform primitive — a queue entity, membership through the existing recipient kinds,… | Proposed — 2026-10-25 · Amended (2026-10-28, ruling `6457475657` on #25769… | 2026-10-25 | — | — | | [ADR-0143](./0143-outbound-connector-auth-is-open-tier.md) | Outbound connector auth is open tier — OAuth2 grants, vendor app-credential exchange and request signing in o… | Proposed — 2026-10-27. `docs/adr/` is Tier H (Prime Directive #14): the ma… | 2026-10-27 | — | — | +| [ADR-0144](./0144-multi-currency-record-currency-dated-rates-write-time-home-projection.md) | Multi-currency — a record currency in a sibling code field, dated exchange rates, and a home-currency amount… | Accepted — 2026-10-28, ruled on objectstack#27119 (ruling `6465431966`, de… | 2026-10-28 | — | — | ## ObjectUI registry — `ui/docs/adr/` (`ui-` prefix) From f14a732a4c9612275484bd136e6c670e0b968565 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 28 Oct 2026 11:39:53 +0000 Subject: [PATCH 02/10] docs(adr): ADR-0144 and the ADR-0104 note cite symbols, not line numbers check:adr-symbol-anchors refuses a line number as an anchor form; the readings now anchor to the declaring symbol or the file. Claude-Session: https://claude.ai/code/session_01Utp6CbTZrFqRYNgjSaSbN5 Co-authored-by: Claude --- ...0104-field-runtime-value-shape-contract.md | 5 ++- ...-dated-rates-write-time-home-projection.md | 38 +++++++++---------- 2 files changed, 22 insertions(+), 21 deletions(-) diff --git a/docs/adr/0104-field-runtime-value-shape-contract.md b/docs/adr/0104-field-runtime-value-shape-contract.md index a5f762ddd9f..d0d2ec74bff 100644 --- a/docs/adr/0104-field-runtime-value-shape-contract.md +++ b/docs/adr/0104-field-runtime-value-shape-contract.md @@ -174,8 +174,9 @@ inert metadata ADR-0078 forbids. > This paragraph's currency sentence needs two corrections. > First, the deletion it records has not happened yet. At `cdec7473`, > `CurrencyValueSchema` is still exported and marked `@deprecated` -> (`packages/spec/src/data/field.zod.ts:581`, its type at `:3062`). The -> retirement is carried by #27182, which records its landing here. +> (`packages/spec/src/data/field.zod.ts#CurrencyValueSchema`, its type +> `#CurrencyValue`). The retirement is carried by #27182, which records its +> landing here. > Second, multi-currency keeps this decision. ADR-0144 (ruling `6465431966` > on #27119) puts a record's currency in a sibling ISO 4217 code field that > the amount names through `currencyField`, never in the value, so a 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 6a11e69d856..7187b899e61 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 @@ -18,7 +18,7 @@ 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 and line on `origin/main` at `cdec7473`. + behaviour cites a file or symbol as read on `origin/main` at `cdec7473`. --- @@ -51,7 +51,7 @@ Three kinds of text feed this record. Each section says which kind it carries. 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 and line. +3. **Readings.** Current behaviour, measured at `cdec7473` and cited by file or symbol. **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 @@ -66,25 +66,25 @@ aggregation. The ruling names them by topic ("Q0 A", "rates A", "home currency A ### 1. What the platform has today (read at `cdec7473`) - **A currency value is a bare number** (ADR-0104 D1). `currencyConfig` has two modes - (`packages/spec/src/data/field.zod.ts:558`). In `fixed` mode the field has one currency, `defaultCurrency`. In + (`packages/spec/src/data/field.zod.ts#CurrencyConfigSchema`). In `fixed` mode the field has one currency, `defaultCurrency`. In `dynamic` mode (the default) amounts display in the tenant default currency. The describe says "Neither mode is a per-record choice: the value is a bare number either way." No key, column or value carries a record's currency. -- **A `fixed` field with no code becomes a yuan field.** `defaultCurrency` defaults to `'CNY'` (`field.zod.ts:559`). +- **A `fixed` field with no code becomes a yuan field.** `defaultCurrency` defaults to `'CNY'` (same schema). #27157 (#27119 Q2 X) requires the code and drops the default. It is a member of R1 and a blocker of M0-1. - **The tenant currency is the `localization.currency` setting** - (`packages/services/service-settings/src/manifests/localization.manifest.ts:105`). It has tenant scope, no platform - default and `valueDomain: 'iso_4217_currency'` (`:124`). It may be unset. It is mutable and keeps no history: probe B + (`packages/services/service-settings/src/manifests/localization.manifest.ts#localizationSettingsManifest`, key + `currency`). It has tenant scope, no platform default and `valueDomain: 'iso_4217_currency'`. It may be unset. It is mutable and keeps no history: probe B of the measurement round saw a change relabel every stored `dynamic` amount. -- **The ISO 4217 value domain exists** (`packages/spec/src/shared/value-domain.zod.ts:159`). It checks membership in - `CURRENCY_FRACTION_DIGITS` (`packages/spec/src/data/currency-fraction-digits.ts:59`). Only that table's key set is +- **The ISO 4217 value domain exists** (`packages/spec/src/shared/value-domain.zod.ts#isValueDomainMember`). It checks + 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. - **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. - **The CEL standard library is pure.** Its header says: "Nothing registered here may be handed a resolver, a lazy - getter or any other callback into the host" (`packages/formula/src/stdlib.ts:6`). That purity keeps + getter or any other callback into the host" (`packages/formula/src/stdlib.ts`). That purity keeps `objectstack build` byte-stable, so a formula function cannot read a rate table. -- **`CurrencyValueSchema` is still exported**, marked `@deprecated` (`field.zod.ts:581`, its type at `:3062`), although +- **`CurrencyValueSchema` is still exported**, marked `@deprecated` (`packages/spec/src/data/field.zod.ts#CurrencyValueSchema`, its type `#CurrencyValue`), although ADR-0104 D1 says it "is deleted". #27182 retires it. - **Today's numbers are silently wrong** (from the measurement round's probes over a real ObjectQL engine; the scratch files were run and deleted and are not re-run for this record). The probes model a record currency the way authors @@ -248,7 +248,7 @@ Version Packages, and the epic body says that happens only when a pair is comple These are outside this decision, and each would need a decision of its own. The first eight are the workload rows' "What is not in this list", each "a capability with no pull in tree". The last comes from the design record §13. -- A CEL `convertCurrency()`. The shape rules it out (D1), and it conflicts with `stdlib.ts:6`. +- A CEL `convertCurrency()`. The shape rules it out (D1), and it conflicts with the purity rule in the header of `packages/formula/src/stdlib.ts`. - Read-time conversion. - A built-in provider feed (ECB or similar). An app flow writes rows instead (D3). - N×N pair rates. Every rate is against the home currency. @@ -298,7 +298,7 @@ Where a source leans one way, the lean is quoted here, not adopted. This record says money is computed in minor-unit integers or decimals, not floating point (「浮点算钱用最小单位整数或 decimal」). Neither point is in the ruling. 4. **A home-amount declaration or a converting write while no home currency is set** (#27192, #27198). - `localization.currency` may be unset (`localization.manifest.ts:105`: `required: false`, no platform default). The + `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). 5. **The unit of parameter 4** (#27185). "12 significant decimals" can mean 12 significant digits or 12 digits after @@ -320,16 +320,16 @@ Where a source leans one way, the lean is quoted here, not adopted. This record ## Verification notes -Each reading below was taken at `cdec7473`. To re-check one, run its command on a newer `origin/main` and compare. A -changed line number means the file moved, not that the fact changed. Re-read the line before citing it. +Each reading below was taken at `cdec7473`. To re-check one, run its command on a newer `origin/main`. The comment +says what the command printed at `cdec7473`. ```bash -git grep -n "Neither mode is a per-record choice" -- packages/spec/src/data/field.zod.ts # :558 -git grep -n "default('CNY')" -- packages/spec/src/data/field.zod.ts # :559, until #27157 -git grep -n "export const CurrencyValueSchema" -- packages/spec/src/data/field.zod.ts # :581, until #27182 -git grep -n "may be handed a res" -- packages/formula/src/stdlib.ts # :6 +git grep -n "Neither mode is a per-record choice" -- packages/spec/src/data/field.zod.ts # 1 line +git grep -n "default('CNY')" -- packages/spec/src/data/field.zod.ts # 1 line, until #27157 +git grep -n "export const CurrencyValueSchema" -- packages/spec/src/data/field.zod.ts # 1 line, until #27182 +git grep -n "may be handed a res" -- packages/formula/src/stdlib.ts # 1 line git grep -n -i -E "exchange.?rate|fx_rate|currency_conversion|convertCurrency" -- packages/ ':!*.test.ts' # 0 lines -git grep -n "valueDomain: 'iso_4217_currency'" -- packages/services/service-settings/src/manifests/localization.manifest.ts # :124 +git grep -n "valueDomain: 'iso_4217_currency'" -- packages/services/service-settings/src/manifests/localization.manifest.ts # 1 line ``` ## References From f5d5bf4920c875c5d02b027a3b26c7504b0a9362 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 28 Oct 2026 11:53:04 +0000 Subject: [PATCH 03/10] docs(adr): ADR-0144 D7 names the projection-formula mismatch in its implementing card Parameter 3 makes the home amount the record amount divided by rate; the implementing card's body writes the product while its own check is the division. D7 now says so. Claude-Session: https://claude.ai/code/session_01Utp6CbTZrFqRYNgjSaSbN5 Co-authored-by: Claude --- ...y-record-currency-dated-rates-write-time-home-projection.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 7187b899e61..9a5d1785b36 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 @@ -195,7 +195,8 @@ pairing. - **Parameter 3 as arithmetic.** An amount `A` in currency `X`, whose rate row says `r`, is `A ÷ r` home units. The home amount is the record amount **divided by** `rate`. Workload row M2-2 writes the projection as - "amount × rate", which is the inverse convention. #27192 follows parameter 3. + "amount × rate", which is the inverse convention, and #27192's card body repeats the product. That card's own + two-minute check is the division: USD 10,000 at 0.1389 gives ¥71,994.24. #27192 follows parameter 3. - **Parameter 1 governs the recompute and backfill** (M2-3, #27193). It does not govern the write path, where D5's refusal holds. - The wording of parameter 4 leaves its unit open ([Open item](#open-items) 5). From 12cc227fa0591ec4588210bec88a2c983bc41505 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 28 Oct 2026 11:54:09 +0000 Subject: [PATCH 04/10] docs(adr): ADR-0144 separates ruled decisions from cited context, and re-wraps The Decision intro now says a line citing the design record, a workload row or a card is context and names its source; the shapes table says its reasons are the analysis'; consequences carry their sources. Claude-Session: https://claude.ai/code/session_01Utp6CbTZrFqRYNgjSaSbN5 Co-authored-by: Claude --- ...-dated-rates-write-time-home-projection.md | 64 ++++++++++--------- 1 file changed, 35 insertions(+), 29 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 9a5d1785b36..b8956565582 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,26 +66,29 @@ aggregation. The ruling names them by topic ("Q0 A", "rates A", "home currency A ### 1. What the platform has today (read at `cdec7473`) - **A currency value is a bare number** (ADR-0104 D1). `currencyConfig` has two modes - (`packages/spec/src/data/field.zod.ts#CurrencyConfigSchema`). In `fixed` mode the field has one currency, `defaultCurrency`. In - `dynamic` mode (the default) amounts display in the tenant default currency. The describe says "Neither mode is a - per-record choice: the value is a bare number either way." No key, column or value carries a record's currency. + (`packages/spec/src/data/field.zod.ts#CurrencyConfigSchema`). In `fixed` mode the field has one currency, + `defaultCurrency`. In `dynamic` mode (the default) amounts display in the tenant default currency. The describe says + "Neither mode is a per-record choice: the value is a bare number either way." No key, column or value carries a + record's currency. - **A `fixed` field with no code becomes a yuan field.** `defaultCurrency` defaults to `'CNY'` (same schema). #27157 (#27119 Q2 X) requires the code and drops the default. It is a member of R1 and a blocker of M0-1. - **The tenant currency is the `localization.currency` setting** (`packages/services/service-settings/src/manifests/localization.manifest.ts#localizationSettingsManifest`, key - `currency`). It has tenant scope, no platform default and `valueDomain: 'iso_4217_currency'`. It may be unset. It is mutable and keeps no history: probe B - of the measurement round saw a change relabel every stored `dynamic` amount. + `currency`). It has tenant scope, no platform default and `valueDomain: 'iso_4217_currency'`. It may be unset. It is + mutable and keeps no history: probe B of the measurement round saw a change relabel every stored `dynamic` amount. - **The ISO 4217 value domain exists** (`packages/spec/src/shared/value-domain.zod.ts#isValueDomainMember`). It checks - 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. + 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. - **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. - **The CEL standard library is pure.** Its header says: "Nothing registered here may be handed a resolver, a lazy getter or any other callback into the host" (`packages/formula/src/stdlib.ts`). That purity keeps `objectstack build` byte-stable, so a formula function cannot read a rate table. -- **`CurrencyValueSchema` is still exported**, marked `@deprecated` (`packages/spec/src/data/field.zod.ts#CurrencyValueSchema`, its type `#CurrencyValue`), although - ADR-0104 D1 says it "is deleted". #27182 retires it. +- **`CurrencyValueSchema` is still exported**, marked `@deprecated` + (`packages/spec/src/data/field.zod.ts#CurrencyValueSchema`, its type `#CurrencyValue`), although ADR-0104 D1 says it + "is deleted". #27182 retires it. - **Today's numbers are silently wrong** (from the measurement round's probes over a real ObjectQL engine; the scratch files were run and deleted and are not re-run for this record). The probes model a record currency the way authors do today, as a sibling text field. `sum` over a USD row of 100 and an EUR row of 100 answers `200 "USD"`. A `summary` @@ -102,10 +105,9 @@ One named overseas port and terminal customer (2026-08) needs three things: 3. consolidate in one home currency. In-tree author pull is zero. The measurement round counted 17 currency fields in `examples/**` at `3d9188502e`, and -none has a record currency. The hold of -2026-10-08 ended with this ruling (乙-现在: now, as an epic). The commitment, as ruled: "R1 (about 4–5 agent-days) is -the delivery the customer can rely on; R2 starts after the customer confirms the consolidation terms (home currency, -rate source, which date drives the rate)." +none has a record currency. The hold of 2026-10-08 ended with this ruling (乙-现在: now, as an epic). The commitment, +as ruled: "R1 (about 4–5 agent-days) is the delivery the customer can rely on; R2 starts after the customer confirms +the consolidation terms (home currency, rate source, which date drives the rate)." ### 3. The shapes considered @@ -119,6 +121,8 @@ again against how Dynamics 365, Odoo, NetSuite, SAP, Salesforce and Airtable mod | B | Dated rates and a CEL `convertCurrency()` | Rejected. It conflicts with the stdlib purity invariant. A formula is virtual and aggregates refuse it, so the converted amount can never be totalled. | | **C′** | A′, plus `sys_exchange_rate` and a home amount computed at write and stored with its rate. No formula function. | **Ruled.** It meets all three needs. Existing aggregate readers total the stored home field without any change to the reader. | +The ruling chose C′. The reasons in the Outcome column are the analysis', not the ruling's. + Q0, where the record currency lives, had four options of its own: - **A, a sibling code field.** Ruled. @@ -129,7 +133,8 @@ Q0, where the record currency lives, had four options of its own: ## Decision -Every item in this section comes from the ruling. Its words are quoted wherever the reading matters. +Every decision in this section comes from the ruling, and its words are quoted wherever the reading matters. A line +that cites the design record, a workload row or a card is context for the decision above it, and names its source. ### D1 — Shape C′ @@ -151,8 +156,8 @@ pairing. ### D3 — Q1: dated rates live in `sys_exchange_rate` (letter A) - `sys_exchange_rate { currency, rate, effective_date, source }` is a platform system object. -- In v1, rows come from manual entry and import. A provider feed is "by an app flow" that writes rows. The platform - ships no feed. +- In v1, rows come from manual entry and import. Provider feeds come "by an app flow", which writes rows; the + platform ships no feed. - A missing rate refuses (D5). ### D4 — Q2: the home currency is `localization.currency`, locked once used (letter A) @@ -210,9 +215,9 @@ pairing. `skills/**`, "never a contract". 3. **"Enabled currencies" are derived, not a setting.** They are the home currency, plus every currency with a rate row, plus the codes already used. -4. **The form pre-fills the organization's currency at the renderer, and the stored value is always explicit.** The - organization's currency reaches a new record only as a pre-filled value the user submits. The server never fills - in the code. +4. **The form pre-fills the organization's currency at the renderer, and the stored value is always explicit.** Read + plainly, the organization's currency reaches a new record only as a pre-filled value the user submits, and the + server never fills in the code. 5. **Metadata never names the home currency.** "the home field is a `dynamic`-mode currency field", so it displays in `localization.currency`, as every `dynamic` field does. @@ -249,7 +254,8 @@ Version Packages, and the epic body says that happens only when a pair is comple These are outside this decision, and each would need a decision of its own. The first eight are the workload rows' "What is not in this list", each "a capability with no pull in tree". The last comes from the design record §13. -- A CEL `convertCurrency()`. The shape rules it out (D1), and it conflicts with the purity rule in the header of `packages/formula/src/stdlib.ts`. +- A CEL `convertCurrency()`. The shape rules it out (D1), and it conflicts with the purity rule in the header of + `packages/formula/src/stdlib.ts`. - Read-time conversion. - A built-in provider feed (ECB or similar). An app flow writes rows instead (D3). - N×N pair rates. Every rate is against the home currency. @@ -267,11 +273,11 @@ D8 already excludes a switch, injected columns, fixed field names and a setting - **The change is additive.** `record` is a new mode, `dynamic` and `fixed` keep their meaning and stored rows are not touched. An app that never writes `record` sees no change. The two spec cards that add keys (#27184, #27191) declare `Metadata-protocol: added`. -- **Two steps cannot be undone once used.** The home-currency lock is one. The other: an object with record-mode - amounts and a home field returns to a single currency only through a data migration (design record §13). -- **Today's relabel hazard closes where the feature is used.** Once the lock holds, a change to `localization.currency` - can no longer relabel stored amounts. An organization with no rate row and no home-amount declaration keeps today's - behaviour. +- **Two steps cannot be undone once used** (design record §13). One is the home-currency lock. The other: an object + with record-mode amounts and a home field returns to a single currency only through a data migration. +- **Today's relabel hazard closes where the feature is used** (the measurement round's Q2 recommendation). Once the + lock holds, a change to `localization.currency` can no longer relabel stored amounts. An organization with no rate + 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. - **The docs start from a true baseline.** #27158 (closed) removed the two docs claims of exchange rates and a stored @@ -299,8 +305,8 @@ Where a source leans one way, the lean is quoted here, not adopted. This record says money is computed in minor-unit integers or decimals, not floating point (「浮点算钱用最小单位整数或 decimal」). Neither point is in the ruling. 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 + `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). 5. **The unit of parameter 4** (#27185). "12 significant decimals" can mean 12 significant digits or 12 digits after the decimal point. The precision and scale of the `rate` column follow from the choice. @@ -340,5 +346,5 @@ git grep -n "valueDomain: 'iso_4217_currency'" -- packages/services/service-sett - Workload rows and parameters: [#27119 `6464936321`](https://github.com/objectstack-ai/objectstack/issues/27119#issuecomment-6464936321). - Measurement round and shape verdict: [#8345 `6041378776`](https://github.com/objectstack-ai/objectstack/issues/8345#issuecomment-6041378776), [#8345 `6041482631`](https://github.com/objectstack-ai/objectstack/issues/8345#issuecomment-6041482631). -- Epic and children: [#8345](https://github.com/objectstack-ai/objectstack/issues/8345). Prerequisites outside the - epic (#27119 Q2 X): #27156, #27157, #27158, #26939. +- Epic and children: [#8345](https://github.com/objectstack-ai/objectstack/issues/8345). The cards executing + #27119 Q2 X outside the epic: #27156, #27157 (also a member of R1), #27158, #26939. From 5fa54eaa46b866781eeb364ad27ef06b2b4125db Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 28 Oct 2026 12:26:54 +0000 Subject: [PATCH 05/10] =?UTF-8?q?docs(adr):=20ADR-0144=20review=20fixes=20?= =?UTF-8?q?=E2=80=94=20required=20pointers,=20scoped=20citations,=20ADR-01?= =?UTF-8?q?04=20note=20moved=20clear=20of=20the=20retirement=20note?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit M2: both homeAmount pointers are required (D1 + D8 rule 2); only their spelling is the spec card's. S1/S2: the ADR-0104 note keeps only the multi-currency-keeps-D1 sentences, sits at the end of D1, and leaves the retirement note to its own card. S3: ADR-0112 scoped to runtime refusals. S4: ADR-0028/ADR-0029 cited for the rate table's owner. S5-S8: TL;DR, Metadata-protocol cards, D8 rule 4 reading, settings-guard wording. Claude-Session: https://claude.ai/code/session_01Utp6CbTZrFqRYNgjSaSbN5 Co-authored-by: Claude --- ...0104-field-runtime-value-shape-contract.md | 20 +++--- ...-dated-rates-write-time-home-projection.md | 68 ++++++++++++------- 2 files changed, 53 insertions(+), 35 deletions(-) diff --git a/docs/adr/0104-field-runtime-value-shape-contract.md b/docs/adr/0104-field-runtime-value-shape-contract.md index d0d2ec74bff..8a9d870f6c9 100644 --- a/docs/adr/0104-field-runtime-value-shape-contract.md +++ b/docs/adr/0104-field-runtime-value-shape-contract.md @@ -170,18 +170,6 @@ either adopted by the contract and enforced, or deleted — it does not remain exported-but-dead. An exported-but-unconsumed value schema is exactly the inert metadata ADR-0078 forbids. -> **Note 2026-10-28 (#27183; [ADR-0144](./0144-multi-currency-record-currency-dated-rates-write-time-home-projection.md)).** -> This paragraph's currency sentence needs two corrections. -> First, the deletion it records has not happened yet. At `cdec7473`, -> `CurrencyValueSchema` is still exported and marked `@deprecated` -> (`packages/spec/src/data/field.zod.ts#CurrencyValueSchema`, its type -> `#CurrencyValue`). The retirement is carried by #27182, which records its -> landing here. -> Second, multi-currency keeps this decision. ADR-0144 (ruling `6465431966` -> on #27119) puts a record's currency in a sibling ISO 4217 code field that -> the amount names through `currencyField`, never in the value, so a -> `currency` value stays a bare number. - **Consumers converge on the contract** (each keeps its role, loses its private type lists): @@ -208,6 +196,14 @@ assertion that every MATRIX entry `parse`s under `valueSchemaFor` — so the contract and the executable oracle cannot drift apart, and a contract change that would break the wire fails a test instead of shipping silently. +> **Note 2026-10-28 (#27183; [ADR-0144](./0144-multi-currency-record-currency-dated-rates-write-time-home-projection.md)) — multi-currency keeps D1's currency value.** +> ADR-0144 (ruling `6465431966` on #27119) puts a record's currency in a +> sibling ISO 4217 code field that the amount names through `currencyField`, +> never in the value. A `currency` value therefore stays a bare number, as +> the "Reality wins" paragraph above decides. The `CurrencyValueSchema` +> retirement that paragraph records belongs to #27182, whose own dated note +> (PR #27233) goes under that paragraph. + ### D2 — Typed action handlers: declared params become the enforced, typed handler input Action params are already fields-lite (`ActionParamSchema` mirrors `FieldType`, 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 b8956565582..8317c0df08a 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 @@ -9,10 +9,17 @@ in the summon chat. The ruling's own text is the authority this record executes; where the two differ, the ruling wins and this record is corrected. - **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) (the codes and envelope of every refusal this - record names). -- **Amends**: no decision. ADR-0104 gains a dated note under D1's "Reality wins" paragraph. The note points here and at - #27182, the card that retires `CurrencyValueSchema`. + kept here). [ADR-0112](./0112-error-code-vocabulary-and-ledger.md) governs the runtime refusals only: a blank record + currency code and a missing rate ([Open item](#open-items) 9). It sets their codes and envelope on the wire, and any + new code is registered in ADR-0112's extension ledger. The build-time refusals this record names are authoring + diagnostics, not wire errors. + [ADR-0029](./0029-kernel-object-ownership-and-platform-objects-decomposition.md) D1, D3 and D6 (a first-party + capability owns its `sys_*` objects, one owner per object, and `platform-objects` is being decomposed) and + [ADR-0028](./0028-metadata-naming-and-namespace-isolation.md) (Deferred; the `sys` namespace design it records) + govern where `sys_exchange_rate` may live. #27185 settles its owner against them (D3). +- **Amends**: no decision. ADR-0104 gains a dated note at the end of its D1 section: multi-currency keeps D1's + bare-number value. For the `CurrencyValueSchema` retirement, the note points to #27182 (PR #27233), which carries its + own note under D1's "Reality wins" paragraph. - **Evidence**: epic [#8345](https://github.com/objectstack-ai/objectstack/issues/8345), with its measurement round [`6041378776`](https://github.com/objectstack-ai/objectstack/issues/8345#issuecomment-6041378776) and the spec seat's shape verdict [`6041482631`](https://github.com/objectstack-ai/objectstack/issues/8345#issuecomment-6041482631); @@ -30,8 +37,9 @@ - `sys_exchange_rate { currency, rate, effective_date, source }` holds dated rates against the organization's home currency, which is `localization.currency`. In v1, rows are entered by hand or imported. A provider feed is an app flow that writes rows. -- At write, the engine converts the amount at the document date's rate. It stores the result in a declared home field - (`homeAmount`) together with the rate it used. A missing rate refuses the write. A corrected rate reaches stored +- At write, the engine converts the amount at the document date's rate. The amount field's `homeAmount` declaration + names the stored home field (`homeAmount.field`) that receives the result, and the author-declared field that stores + the rate used. A missing rate refuses the write. A corrected rate reaches stored rows only through an explicit recompute. - A raw aggregate over a record-mode field refuses at build unless it groups by the code field. Consolidated totals read the stored home field. @@ -88,7 +96,7 @@ aggregation. The ruling names them by topic ("Q0 A", "rates A", "home currency A `objectstack build` byte-stable, so a formula function cannot read a rate table. - **`CurrencyValueSchema` is still exported**, marked `@deprecated` (`packages/spec/src/data/field.zod.ts#CurrencyValueSchema`, its type `#CurrencyValue`), although ADR-0104 D1 says it - "is deleted". #27182 retires it. + "is deleted". #27182 retires it, in PR #27233. - **Today's numbers are silently wrong** (from the measurement round's probes over a real ObjectQL engine; the scratch files were run and deleted and are not re-run for this record). The probes model a record currency the way authors do today, as a sibling text field. `sum` over a USD row of 100 and an EUR row of 100 answers `200 "USD"`. A `summary` @@ -159,13 +167,18 @@ pairing. - In v1, rows come from manual entry and import. Provider feeds come "by an app flow", which writes rows; the platform ships no feed. - A missing rate refuses (D5). +- Which package owns the object is #27185's to settle against ADR-0029 D1, D3 and D6 and ADR-0028 + ([Open item](#open-items) 11). Workload row M1-1 and the epic's territory name `packages/platform-objects/src`, + which ADR-0029 D6 is decomposing. ### D4 — Q2: the home currency is `localization.currency`, locked once used (letter A) - The organization's home currency is the existing `localization.currency` setting. There is no second currency setting. -- It is locked "once a rate row or a home-amount declaration exists". M1-3 (#27198) builds the lock. `service-settings` - has no write-side predicate hook today (workload row M1-3). +- It is locked "once a rate row or a home-amount declaration exists". M1-3 (#27198) builds the lock. + `service-settings` has no write-side guard today that can refuse a value change on engine state (workload row M1-3). + `config-change-audit.ts` runs after the fact, and the write-path predicate `providesConfidentiality` concerns secret + posture, not a value change. ### D5 — Q3: the conversion runs at write and is stored with its rate (letter A) @@ -176,9 +189,14 @@ pairing. counts as missing in the first place is [Open item](#open-items) 2. - A corrected rate reaches stored rows only through "an explicit recompute operation". Changing a rate row does not, on its own, rewrite stored home amounts. -- The declaration key is `homeAmount`. The ruling: "the `homeAmount` key never ships ahead of the engine". Workload - row M2-1 drafts its shape as `homeAmount: { field, rateDate, rateField? }`. #27191 fixes the shape under contract - review; see [Open items](#open-items) 1 and 2. +- The declaration key is `homeAmount`. The ruling: "the `homeAmount` key never ships ahead of the engine". + `homeAmount` is the declaration on the amount field, and `homeAmount.field` names the stored home field. +- **Whenever `homeAmount` is declared, both its document-date pointer and its rate pointer are required.** D1 stores + the amount "with the rate used", and D8 rule 2 lets that rate live only in a field "declared by the app author and + referenced by pointer". The conversion runs "from the document date's rate", and under the same rule the document + date is an author-declared field named by a pointer, with no default for the engine to guess. Workload row M2-1 and + #27191's card body draft the rate pointer as optional (`rateField?`). This record corrects that to required. Only the + spelling of the two pointers is #27191's ([Open item](#open-items) 1). ### D6 — Q4: raw aggregates over a record-mode field refuse, and consolidation reads the home field (letter A) @@ -215,9 +233,10 @@ pairing. `skills/**`, "never a contract". 3. **"Enabled currencies" are derived, not a setting.** They are the home currency, plus every currency with a rate row, plus the codes already used. -4. **The form pre-fills the organization's currency at the renderer, and the stored value is always explicit.** Read - plainly, the organization's currency reaches a new record only as a pre-filled value the user submits, and the - server never fills in the code. +4. **The form pre-fills the organization's currency at the renderer, and the stored value is always explicit.** This + record reads the rule to mean that the organization's currency reaches a new record only as a pre-filled value the + user submits, and that the server never fills in the code. #27186's refusal of a blank code (drafted as + `ERR_CURRENCY_CODE_REQUIRED`, workload row M0-2) is the enforcement that makes the reading true. 5. **Metadata never names the home currency.** "the home field is a `dynamic`-mode currency field", so it displays in `localization.currency`, as every `dynamic` field does. @@ -271,8 +290,8 @@ D8 already excludes a switch, injected columns, fixed field names and a setting ## Consequences - **The change is additive.** `record` is a new mode, `dynamic` and `fixed` keep their meaning and stored rows are not - touched. An app that never writes `record` sees no change. The two spec cards that add keys (#27184, #27191) declare - `Metadata-protocol: added`. + touched. An app that never writes `record` sees no change. Three cards declare `Metadata-protocol: added`: the two that add + authorable keys (#27184, #27191) and the seam card that adds the analytics wire key (#27187). - **Two steps cannot be undone once used** (design record §13). One is the home-currency lock. The other: an object with record-mode amounts and a home field returns to a single currency only through a data migration. - **Today's relabel hazard closes where the feature is used** (the measurement round's Q2 recommendation). Once the @@ -289,12 +308,11 @@ D8 already excludes a switch, injected columns, fixed field names and a setting The ruling does not decide the items below. Each belongs to the card named, and is settled under that card's review. Where a source leans one way, the lean is quoted here, not adopted. This record is amended with each answer. -1. **The document-date pointer and the stored-rate pointer: spelling and whether they are required** (#27191). The - ruling fixes that the rate is the document date's and that the rate used is stored. Workload row M2-1 drafts - `homeAmount: { field, rateDate, rateField? }`. In that draft, `rateDate` is "a `date`/`datetime` field on the same - object (⛔ no default)" and `rateField` is optional. The design record §6 says the document date must be declared - explicitly and must not default to `created_at` (「⛔ 不默认 created_at」). An optional `rateField` sits against the - ruling's "STORED with the rate used", and #27191 reconciles the two. +1. **The spelling of the document-date pointer and the rate pointer** (#27191). Only the names are open. Both + pointers are required whenever `homeAmount` is declared (D5, from D1 and D8 rule 2). The optional `rateField?` of + workload row M2-1 and #27191 is corrected to required. Workload row M2-1 drafts the names `rateDate` and + `rateField`; in that draft, `rateDate` is "a `date`/`datetime` field on the same object (⛔ no default)". The design + record §6 says the document date must not default to `created_at` (「⛔ 不默认 created_at」). 2. **Which row counts as "the document date's rate"** (#27192). The ruling says "a platform rate table by effective date". Workload row M2-2 and the measurement round read this as the row with the latest `effective_date` on or before the document date. The measurement round also rules out a "nearest-future rate". The design record §5 says @@ -324,6 +342,10 @@ Where a source leans one way, the lean is quoted here, not adopted. This record 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. +11. **The owner package of `sys_exchange_rate`** (#27185). Workload row M1-1 and the epic's territory name + `packages/platform-objects/src`. ADR-0029 D1, D3 and D6 give a first-party capability ownership of its `sys_*` + objects and decompose `platform-objects`, and ADR-0028 (Deferred) records the `sys` namespace design. #27185 + settles the owner against them. ## Verification notes From ada4aaad3ce9cc36964d4c6e498e3c908299194f Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 28 Oct 2026 12:50:01 +0000 Subject: [PATCH 06/10] docs(adr): ADR-0144 D7 records parameter 4 as scale 12 (ruling 6469382727) The maintainer ruled parameter 4 on #27185: scale 12, total precision 24, decimal arithmetic, never float. D7 carries it; Open item 5 stays as a settled pointer so the numbering of items 6-11 does not move. Claude-Session: https://claude.ai/code/session_01Utp6CbTZrFqRYNgjSaSbN5 Co-authored-by: Claude --- ...cy-dated-rates-write-time-home-projection.md | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 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 8317c0df08a..41d67690b34 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 @@ -7,7 +7,8 @@ [`6464438764`](https://github.com/objectstack-ai/objectstack/issues/27119#issuecomment-6464438764), the workload list [`6464936321`](https://github.com/objectstack-ai/objectstack/issues/27119#issuecomment-6464936321) and the seat's answers in the summon chat. The ruling's own text is the authority this record executes; where the two differ, the ruling - wins and this record is corrected. + wins and this record is corrected. The unit of parameter 4 was ruled afterwards, on 2026-10-28, in ruling + [`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 the runtime refusals only: a blank record currency code and a missing rate ([Open item](#open-items) 9). It sets their codes and envelope on the wire, and any @@ -53,8 +54,8 @@ 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. [Decision](#decision) carries only this. It quotes the ruling wherever the - wording matters. + 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. 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 @@ -214,7 +215,7 @@ pairing. | 1 | Backfill when a historical rate is missing | Report and skip the row, and apply the rest | | 2 | Who edits `sys_exchange_rate` | Organization admins only | | 3 | Rate direction | "1 home unit = `rate` units of the foreign currency" | -| 4 | Precision of `rate` | "12 significant decimals" | +| 4 | Precision of `rate` | "12 significant decimals", ruled as **scale 12** (ruling `6469382727`) | - **Parameter 3 as arithmetic.** An amount `A` in currency `X`, whose rate row says `r`, is `A ÷ r` home units. The home amount is the record amount **divided by** `rate`. Workload row M2-2 writes the projection as @@ -222,7 +223,9 @@ pairing. two-minute check is the division: USD 10,000 at 0.1389 gives ¥71,994.24. #27192 follows parameter 3. - **Parameter 1 governs the recompute and backfill** (M2-3, #27193). It does not govern the write path, where D5's refusal holds. -- The wording of parameter 4 leaves its unit open ([Open item](#open-items) 5). +- **Parameter 4's unit** comes from ruling [`6469382727`](https://github.com/objectstack-ai/objectstack/issues/27185#issuecomment-6469382727) on #27185: "scale 12 — twelve digits after the + decimal point, total precision 24, decimal arithmetic, never float". The `rate` column is a decimal with precision 24 + and scale 12. ### D8 — Design rules recorded from the summon @@ -326,8 +329,8 @@ Where a source leans one way, the lean is quoted here, not adopted. This record `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). -5. **The unit of parameter 4** (#27185). "12 significant decimals" can mean 12 significant digits or 12 digits after - the decimal point. The precision and scale of the `rate` column follow from the choice. +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. 7. **Uniqueness and audit of rate rows** (#27185). The design record §5 and the measurement round make rows unique per From 21ad0228d17f156144568d4bd910f994df806df5 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 28 Oct 2026 13:23:36 +0000 Subject: [PATCH 07/10] docs(adr): ADR-0144 cites the CurrencyValueSchema file, not its symbol The retirement PR deletes CurrencyValueSchema and CurrencyValue, so a symbol anchor to either would red check:adr-symbol-anchors whichever of the two PRs lands second. The Context bullet now cites the file. Claude-Session: https://claude.ai/code/session_01Utp6CbTZrFqRYNgjSaSbN5 Co-authored-by: Claude --- ...record-currency-dated-rates-write-time-home-projection.md | 5 ++--- 1 file changed, 2 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 41d67690b34..d15d9f40450 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 @@ -95,9 +95,8 @@ aggregation. The ruling names them by topic ("Q0 A", "rates A", "home currency A - **The CEL standard library is pure.** Its header says: "Nothing registered here may be handed a resolver, a lazy getter or any other callback into the host" (`packages/formula/src/stdlib.ts`). That purity keeps `objectstack build` byte-stable, so a formula function cannot read a rate table. -- **`CurrencyValueSchema` is still exported**, marked `@deprecated` - (`packages/spec/src/data/field.zod.ts#CurrencyValueSchema`, its type `#CurrencyValue`), although ADR-0104 D1 says it - "is deleted". #27182 retires it, in PR #27233. +- **`CurrencyValueSchema` is still exported** from `packages/spec/src/data/field.zod.ts`, marked `@deprecated`, + together with its `CurrencyValue` type, although ADR-0104 D1 says it "is deleted". #27182 retires it, in PR #27233. - **Today's numbers are silently wrong** (from the measurement round's probes over a real ObjectQL engine; the scratch files were run and deleted and are not re-run for this record). The probes model a record currency the way authors do today, as a sibling text field. `sum` over a USD row of 100 and an EUR row of 100 answers `200 "USD"`. A `summary` From 775d8c09666de79e0ece0c2d8dbb3c58d08c8fba Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 28 Oct 2026 14:12:57 +0000 Subject: [PATCH 08/10] docs(adr): ADR-0144 dates the #27157 landing in Context and the verification notes PR #27218 (0cd578f7) landed #27157 on 2026-10-28: defaultCurrency has no default and a fixed config without a code is refused. The readings stay dated at cdec7473; a one-line note records the landing. Claude-Session: https://claude.ai/code/session_01Utp6CbTZrFqRYNgjSaSbN5 Co-authored-by: Claude --- ...-record-currency-dated-rates-write-time-home-projection.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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 d15d9f40450..4a6b2938363 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 @@ -81,6 +81,8 @@ aggregation. The ruling names them by topic ("Q0 A", "rates A", "home currency A record's currency. - **A `fixed` field with no code becomes a yuan field.** `defaultCurrency` defaults to `'CNY'` (same schema). #27157 (#27119 Q2 X) requires the code and drops the default. It is a member of R1 and a blocker of M0-1. + *Note (2026-10-28):* #27157 landed in PR #27218 (`0cd578f7`). On `main`, `defaultCurrency` has no default, and a + `fixed` config without a code is refused. - **The tenant currency is the `localization.currency` setting** (`packages/services/service-settings/src/manifests/localization.manifest.ts#localizationSettingsManifest`, key `currency`). It has tenant scope, no platform default and `valueDomain: 'iso_4217_currency'`. It may be unset. It is @@ -356,7 +358,7 @@ says what the command printed at `cdec7473`. ```bash git grep -n "Neither mode is a per-record choice" -- packages/spec/src/data/field.zod.ts # 1 line -git grep -n "default('CNY')" -- packages/spec/src/data/field.zod.ts # 1 line, until #27157 +git grep -n "default('CNY')" -- packages/spec/src/data/field.zod.ts # 1 line; #27157 landed 2026-10-28 in PR #27218 (0cd578f7), after which the one match is a docblock line recording the old default git grep -n "export const CurrencyValueSchema" -- packages/spec/src/data/field.zod.ts # 1 line, until #27182 git grep -n "may be handed a res" -- packages/formula/src/stdlib.ts # 1 line git grep -n -i -E "exchange.?rate|fx_rate|currency_conversion|convertCurrency" -- packages/ ':!*.test.ts' # 0 lines From e9bb1901e2308d28673756d516e4ca3ada404ea1 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 29 Oct 2026 01:17:16 +0000 Subject: [PATCH 09/10] docs(adr): ADR-0144 takes the three should-fixes of contract review 6485013063 The review on 775d8c09 (PASS, no must-fix) found three places where the record said less, or other, than its own Decision: - Builds-on scoped ADR-0112 to two runtime refusals and called every build-time refusal an authoring diagnostic, while D6 places the ungrouped-aggregate refusal in two request-time doors. The paragraph now says ADR-0112 governs every refusal that answers on the wire, that "at build" covers the declared aggregates (summary roll-ups at registration, dataset measures at compile), and that an ad-hoc aggregate meets the same rule at the request door with the existing catalog code INVALID_FIELD; the two new codes stay the only ledger additions. - Consequences predicted the consumers' Metadata-protocol declarations. It now states the record's own claim: record and homeAmount are additive, the one narrowing of what an author can write is #27157's, and each consumer's semver is its own changeset's to declare. - Open item 12 records what the ruling on #27119 did not decide: whether cross-currency arithmetic between fixed fields of different codes is refused. The epic PM seat ruled that #27187 (PR #27338) confines its compile guards to record-mode amounts; the general guard waits on the maintainer and epic #8345 carries the question. Items 1-11 keep their numbers. Claude-Session: https://claude.ai/code/session_01Utp6CbTZrFqRYNgjSaSbN5 Co-authored-by: Claude --- ...-dated-rates-write-time-home-projection.md | 23 +++++++++++++------ 1 file changed, 16 insertions(+), 7 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 4a6b2938363..78c1d4af703 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 @@ -10,10 +10,13 @@ wins and this record is corrected. The unit of parameter 4 was ruled afterwards, on 2026-10-28, in ruling [`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 the runtime refusals only: a blank record - currency code and a missing rate ([Open item](#open-items) 9). It sets their codes and envelope on the wire, and any - new code is registered in ADR-0112's extension ledger. The build-time refusals this record names are authoring - diagnostics, not wire errors. + 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. + "At build" in D1 and D6 covers the declared aggregates: a `summary` roll-up refuses at registration and a dataset + measure at compile, as authoring diagnostics, not wire errors. An ad-hoc aggregate is never built, so it meets the + same rule at the request door (the ObjectQL aggregate-by-field-type door and the analytics cube measure door, D6) + with an existing ADR-0112 catalog code, `INVALID_FIELD`. [ADR-0029](./0029-kernel-object-ownership-and-platform-objects-decomposition.md) D1, D3 and D6 (a first-party capability owns its `sys_*` objects, one owner per object, and `platform-objects` is being decomposed) and [ADR-0028](./0028-metadata-naming-and-namespace-isolation.md) (Deferred; the `sys` namespace design it records) @@ -293,9 +296,9 @@ D8 already excludes a switch, injected columns, fixed field names and a setting ## Consequences -- **The change is additive.** `record` is a new mode, `dynamic` and `fixed` keep their meaning and stored rows are not - touched. An app that never writes `record` sees no change. Three cards declare `Metadata-protocol: added`: the two that add - authorable keys (#27184, #27191) and the seam card that adds the analytics wire key (#27187). +- **`record` and `homeAmount` are additive.** `record` is a new mode, `dynamic` and `fixed` keep their meaning and + stored rows are not touched. This epic's one narrowing of what an author can write is #27157's: a `fixed` field + must name its code. Each consumer's semver is its own changeset's to declare, not this record's. - **Two steps cannot be undone once used** (design record §13). One is the home-currency lock. The other: an object with record-mode amounts and a home field returns to a single currency only through a data migration. - **Today's relabel hazard closes where the feature is used** (the measurement round's Q2 recommendation). Once the @@ -350,6 +353,12 @@ Where a source leans one way, the lean is quoted here, not adopted. This record `packages/platform-objects/src`. ADR-0029 D1, D3 and D6 give a first-party capability ownership of its `sys_*` objects and decompose `platform-objects`, and ADR-0028 (Deferred) records the `sys` namespace design. #27185 settles the owner against them. +12. **Whether today's cross-currency arithmetic between `fixed` fields of different codes is refused** (epic #8345). + Two shapes: a measure `currency` that contradicts a `fixed` field's code, and a derived measure over two different + `fixed` codes, which Context §1 counts among today's silently wrong numbers. The ruling on #27119 did not decide + this: the ruled D1 sentence names only record-mode fields. The epic PM seat ruled that the R1 analytics card #27187 + (PR #27338) confines its compile guards to record-mode amounts. The general `fixed`-versus-`fixed` guard stays + undecided. The authority is the maintainer's, and epic #8345 carries the question. ## Verification notes From ad89eb22e650a4fad61cd9c0402b68b3dc077555 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 29 Oct 2026 01:34:48 +0000 Subject: [PATCH 10/10] docs(adr): ADR-0144 takes the three should-fixes of delta review 6485356516 The delta review on e9bb1901 (PASS, no must-fix) found three residues in the text the previous commit added: - Builds-on called the declared-aggregate refusals "authoring diagnostics, not wire errors", which is wrong for the dataset-compile half (the compile runs in the request path and DATASET_INVALID / 400 reaches the caller) and loose for the registration half (INVALID_METADATA / 422 when a metadata write triggers it). The clause now names those existing codes; "They are the only ledger additions" stays the operative sentence. - Consequences attributed #27157's narrowing to the epic; the record's own References line places #27157 outside the epic, as a member of R1. It now reads "R1's one narrowing of what an author can write". - Open item 12 said the epic PM seat "ruled", a word this record reserves for the maintainer's text, and cited nothing a reader can open. It now says "decided", cites the decision comment on #27187 and PR #27338's commit 507ed8f9 as the tree evidence, and links the question as posted on epic #8345. Claude-Session: https://claude.ai/code/session_01Utp6CbTZrFqRYNgjSaSbN5 Co-authored-by: Claude --- ...-dated-rates-write-time-home-projection.md | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 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 78c1d4af703..25b89fed82e 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 @@ -14,9 +14,9 @@ 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. "At build" in D1 and D6 covers the declared aggregates: a `summary` roll-up refuses at registration and a dataset - measure at compile, as authoring diagnostics, not wire errors. An ad-hoc aggregate is never built, so it meets the - same rule at the request door (the ObjectQL aggregate-by-field-type door and the analytics cube measure door, D6) - with an existing ADR-0112 catalog code, `INVALID_FIELD`. + 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 + and the analytics cube measure door, D6) with an existing ADR-0112 catalog code, `INVALID_FIELD`. [ADR-0029](./0029-kernel-object-ownership-and-platform-objects-decomposition.md) D1, D3 and D6 (a first-party capability owns its `sys_*` objects, one owner per object, and `platform-objects` is being decomposed) and [ADR-0028](./0028-metadata-naming-and-namespace-isolation.md) (Deferred; the `sys` namespace design it records) @@ -297,8 +297,8 @@ D8 already excludes a switch, injected columns, fixed field names and a setting ## Consequences - **`record` and `homeAmount` are additive.** `record` is a new mode, `dynamic` and `fixed` keep their meaning and - stored rows are not touched. This epic's one narrowing of what an author can write is #27157's: a `fixed` field - must name its code. Each consumer's semver is its own changeset's to declare, not this record's. + stored rows are not touched. R1's one narrowing of what an author can write is #27157's: a `fixed` field must name + its code. Each consumer's semver is its own changeset's to declare, not this record's. - **Two steps cannot be undone once used** (design record §13). One is the home-currency lock. The other: an object with record-mode amounts and a home field returns to a single currency only through a data migration. - **Today's relabel hazard closes where the feature is used** (the measurement round's Q2 recommendation). Once the @@ -356,9 +356,12 @@ Where a source leans one way, the lean is quoted here, not adopted. This record 12. **Whether today's cross-currency arithmetic between `fixed` fields of different codes is refused** (epic #8345). Two shapes: a measure `currency` that contradicts a `fixed` field's code, and a derived measure over two different `fixed` codes, which Context §1 counts among today's silently wrong numbers. The ruling on #27119 did not decide - this: the ruled D1 sentence names only record-mode fields. The epic PM seat ruled that the R1 analytics card #27187 - (PR #27338) confines its compile guards to record-mode amounts. The general `fixed`-versus-`fixed` guard stays - undecided. The authority is the maintainer's, and epic #8345 carries the question. + this: the ruled D1 sentence names only record-mode fields. The epic PM seat decided, in + [`6485365183`](https://github.com/objectstack-ai/objectstack/issues/27187#issuecomment-6485365183) on #27187, that + the R1 analytics card #27187 confines its compile guards to record-mode amounts; PR #27338's commit `507ed8f9` is + the tree evidence. The general `fixed`-versus-`fixed` guard stays undecided. The authority is the maintainer's, and + epic #8345 carries the question + ([`6485374827`](https://github.com/objectstack-ai/objectstack/issues/8345#issuecomment-6485374827)). ## Verification notes