Skip to content

Commit 12cc227

Browse files
committed
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 <noreply@anthropic.com>
1 parent f5d5bf4 commit 12cc227

1 file changed

Lines changed: 35 additions & 29 deletions

File tree

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

Lines changed: 35 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -66,26 +66,29 @@ aggregation. The ruling names them by topic ("Q0 A", "rates A", "home currency A
6666
### 1. What the platform has today (read at `cdec7473`)
6767

6868
- **A currency value is a bare number** (ADR-0104 D1). `currencyConfig` has two modes
69-
(`packages/spec/src/data/field.zod.ts#CurrencyConfigSchema`). In `fixed` mode the field has one currency, `defaultCurrency`. In
70-
`dynamic` mode (the default) amounts display in the tenant default currency. The describe says "Neither mode is a
71-
per-record choice: the value is a bare number either way." No key, column or value carries a record's currency.
69+
(`packages/spec/src/data/field.zod.ts#CurrencyConfigSchema`). In `fixed` mode the field has one currency,
70+
`defaultCurrency`. In `dynamic` mode (the default) amounts display in the tenant default currency. The describe says
71+
"Neither mode is a per-record choice: the value is a bare number either way." No key, column or value carries a
72+
record's currency.
7273
- **A `fixed` field with no code becomes a yuan field.** `defaultCurrency` defaults to `'CNY'` (same schema).
7374
#27157 (#27119 Q2 X) requires the code and drops the default. It is a member of R1 and a blocker of M0-1.
7475
- **The tenant currency is the `localization.currency` setting**
7576
(`packages/services/service-settings/src/manifests/localization.manifest.ts#localizationSettingsManifest`, key
76-
`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
77-
of the measurement round saw a change relabel every stored `dynamic` amount.
77+
`currency`). It has tenant scope, no platform default and `valueDomain: 'iso_4217_currency'`. It may be unset. It is
78+
mutable and keeps no history: probe B of the measurement round saw a change relabel every stored `dynamic` amount.
7879
- **The ISO 4217 value domain exists** (`packages/spec/src/shared/value-domain.zod.ts#isValueDomainMember`). It checks
79-
membership in `CURRENCY_FRACTION_DIGITS` (`packages/spec/src/data/currency-fraction-digits.ts#CURRENCY_FRACTION_DIGITS`). Only that table's key set is
80-
read. Its fraction-digit values have no reader.
80+
membership in `CURRENCY_FRACTION_DIGITS`
81+
(`packages/spec/src/data/currency-fraction-digits.ts#CURRENCY_FRACTION_DIGITS`). Only that table's key set is read.
82+
Its fraction-digit values have no reader.
8183
- **No conversion is implemented.**
8284
`git grep -n -i -E "exchange.?rate|fx_rate|currency_conversion|convertCurrency" -- packages/ ':!*.test.ts'` prints 0
8385
lines. Control: `git grep -c currencyConfig -- packages/spec/src/data/field.zod.ts` prints 13.
8486
- **The CEL standard library is pure.** Its header says: "Nothing registered here may be handed a resolver, a lazy
8587
getter or any other callback into the host" (`packages/formula/src/stdlib.ts`). That purity keeps
8688
`objectstack build` byte-stable, so a formula function cannot read a rate table.
87-
- **`CurrencyValueSchema` is still exported**, marked `@deprecated` (`packages/spec/src/data/field.zod.ts#CurrencyValueSchema`, its type `#CurrencyValue`), although
88-
ADR-0104 D1 says it "is deleted". #27182 retires it.
89+
- **`CurrencyValueSchema` is still exported**, marked `@deprecated`
90+
(`packages/spec/src/data/field.zod.ts#CurrencyValueSchema`, its type `#CurrencyValue`), although ADR-0104 D1 says it
91+
"is deleted". #27182 retires it.
8992
- **Today's numbers are silently wrong** (from the measurement round's probes over a real ObjectQL engine; the scratch
9093
files were run and deleted and are not re-run for this record). The probes model a record currency the way authors
9194
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:
102105
3. consolidate in one home currency.
103106

104107
In-tree author pull is zero. The measurement round counted 17 currency fields in `examples/**` at `3d9188502e`, and
105-
none has a record currency. The hold of
106-
2026-10-08 ended with this ruling (乙-现在: now, as an epic). The commitment, as ruled: "R1 (about 4–5 agent-days) is
107-
the delivery the customer can rely on; R2 starts after the customer confirms the consolidation terms (home currency,
108-
rate source, which date drives the rate)."
108+
none has a record currency. The hold of 2026-10-08 ended with this ruling (乙-现在: now, as an epic). The commitment,
109+
as ruled: "R1 (about 4–5 agent-days) is the delivery the customer can rely on; R2 starts after the customer confirms
110+
the consolidation terms (home currency, rate source, which date drives the rate)."
109111

110112
### 3. The shapes considered
111113

@@ -119,6 +121,8 @@ again against how Dynamics 365, Odoo, NetSuite, SAP, Salesforce and Airtable mod
119121
| 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. |
120122
| **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. |
121123

124+
The ruling chose C′. The reasons in the Outcome column are the analysis', not the ruling's.
125+
122126
Q0, where the record currency lives, had four options of its own:
123127

124128
- **A, a sibling code field.** Ruled.
@@ -129,7 +133,8 @@ Q0, where the record currency lives, had four options of its own:
129133

130134
## Decision
131135

132-
Every item in this section comes from the ruling. Its words are quoted wherever the reading matters.
136+
Every decision in this section comes from the ruling, and its words are quoted wherever the reading matters. A line
137+
that cites the design record, a workload row or a card is context for the decision above it, and names its source.
133138

134139
### D1 — Shape C′
135140

@@ -151,8 +156,8 @@ pairing.
151156
### D3 — Q1: dated rates live in `sys_exchange_rate` (letter A)
152157

153158
- `sys_exchange_rate { currency, rate, effective_date, source }` is a platform system object.
154-
- In v1, rows come from manual entry and import. A provider feed is "by an app flow" that writes rows. The platform
155-
ships no feed.
159+
- In v1, rows come from manual entry and import. Provider feeds come "by an app flow", which writes rows; the
160+
platform ships no feed.
156161
- A missing rate refuses (D5).
157162

158163
### D4 — Q2: the home currency is `localization.currency`, locked once used (letter A)
@@ -210,9 +215,9 @@ pairing.
210215
`skills/**`, "never a contract".
211216
3. **"Enabled currencies" are derived, not a setting.** They are the home currency, plus every currency with a rate
212217
row, plus the codes already used.
213-
4. **The form pre-fills the organization's currency at the renderer, and the stored value is always explicit.** The
214-
organization's currency reaches a new record only as a pre-filled value the user submits. The server never fills
215-
in the code.
218+
4. **The form pre-fills the organization's currency at the renderer, and the stored value is always explicit.** Read
219+
plainly, the organization's currency reaches a new record only as a pre-filled value the user submits, and the
220+
server never fills in the code.
216221
5. **Metadata never names the home currency.** "the home field is a `dynamic`-mode currency field", so it displays in
217222
`localization.currency`, as every `dynamic` field does.
218223

@@ -249,7 +254,8 @@ Version Packages, and the epic body says that happens only when a pair is comple
249254
These are outside this decision, and each would need a decision of its own. The first eight are the workload rows'
250255
"What is not in this list", each "a capability with no pull in tree". The last comes from the design record §13.
251256

252-
- 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`.
257+
- A CEL `convertCurrency()`. The shape rules it out (D1), and it conflicts with the purity rule in the header of
258+
`packages/formula/src/stdlib.ts`.
253259
- Read-time conversion.
254260
- A built-in provider feed (ECB or similar). An app flow writes rows instead (D3).
255261
- 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
267273
- **The change is additive.** `record` is a new mode, `dynamic` and `fixed` keep their meaning and stored rows are not
268274
touched. An app that never writes `record` sees no change. The two spec cards that add keys (#27184, #27191) declare
269275
`Metadata-protocol: added`.
270-
- **Two steps cannot be undone once used.** The home-currency lock is one. The other: an object with record-mode
271-
amounts and a home field returns to a single currency only through a data migration (design record §13).
272-
- **Today's relabel hazard closes where the feature is used.** Once the lock holds, a change to `localization.currency`
273-
can no longer relabel stored amounts. An organization with no rate row and no home-amount declaration keeps today's
274-
behaviour.
276+
- **Two steps cannot be undone once used** (design record §13). One is the home-currency lock. The other: an object
277+
with record-mode amounts and a home field returns to a single currency only through a data migration.
278+
- **Today's relabel hazard closes where the feature is used** (the measurement round's Q2 recommendation). Once the
279+
lock holds, a change to `localization.currency` can no longer relabel stored amounts. An organization with no rate
280+
row and no home-amount declaration keeps today's behaviour.
275281
- **Every converting write adds a rate lookup.**
276282
- **The fraction-digit values may get their first reader.** That depends on [Open item](#open-items) 3.
277283
- **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
299305
says money is computed in minor-unit integers or decimals, not floating point (「浮点算钱用最小单位整数或 decimal」).
300306
Neither point is in the ruling.
301307
4. **A home-amount declaration or a converting write while no home currency is set** (#27192, #27198).
302-
`localization.currency` may be unset (its manifest entry says `required: false`, and there is no platform default). The
303-
ruling locks the setting once used. It does not say what happens while the setting is unset. The sources rule out a
308+
`localization.currency` may be unset (its manifest entry says `required: false`, and there is no platform
309+
default). The ruling locks the setting once used. It does not say what happens while the setting is unset. The sources rule out a
304310
silent default (design record §9: no CNY default).
305311
5. **The unit of parameter 4** (#27185). "12 significant decimals" can mean 12 significant digits or 12 digits after
306312
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
340346
- Workload rows and parameters: [#27119 `6464936321`](https://github.com/objectstack-ai/objectstack/issues/27119#issuecomment-6464936321).
341347
- Measurement round and shape verdict: [#8345 `6041378776`](https://github.com/objectstack-ai/objectstack/issues/8345#issuecomment-6041378776),
342348
[#8345 `6041482631`](https://github.com/objectstack-ai/objectstack/issues/8345#issuecomment-6041482631).
343-
- Epic and children: [#8345](https://github.com/objectstack-ai/objectstack/issues/8345). Prerequisites outside the
344-
epic (#27119 Q2 X): #27156, #27157, #27158, #26939.
349+
- Epic and children: [#8345](https://github.com/objectstack-ai/objectstack/issues/8345). The cards executing
350+
#27119 Q2 X outside the epic: #27156, #27157 (also a member of R1), #27158, #26939.

0 commit comments

Comments
 (0)