From 1e38614ed1b58623b98711dd9b0a89834dcd4781 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 31 Oct 2026 15:19:17 +0000 Subject: [PATCH 1/2] docs(skills): teach the derived-measure fixed-currency refusal beside the two fixed-currency refusals A derived sum or difference whose operands all resolve to one fixed code refuses another currency at compile (DATASET_INVALID / 400); the prescription is the shipped message's: delete the measure's currency (the column takes the operands' code), or restate it. The two skill texts and the two docs pages teach it in those words, and the docs say the derived column carries that code on the wire too, as main now does. The skills token ratchet is paid by deleting a convention already shown in the example above it (field-types.md) and a retired-key parenthetical the parse refusal and the Dataset-Bound Widgets paragraph already carry (dashboards.md). Claude-Session: https://claude.ai/code/session_01FEkQ5nqSVzKxNiy5ZH2o44 Co-authored-by: Claude --- content/docs/api/data-api.mdx | 12 +++++++++--- content/docs/data-modeling/currency.mdx | 8 ++++++-- skills/objectstack-data/rules/field-types.md | 7 ++++--- skills/objectstack-ui/rules/dashboards.md | 8 ++++---- 4 files changed, 23 insertions(+), 12 deletions(-) diff --git a/content/docs/api/data-api.mdx b/content/docs/api/data-api.mdx index 973200b4e44..8b6b89c465a 100644 --- a/content/docs/api/data-api.mdx +++ b/content/docs/api/data-api.mdx @@ -518,12 +518,18 @@ monetary measure column takes the measure's own `currency`; then, only when the field is `currencyMode: 'fixed'`, that field's `currencyConfig.defaultCurrency`; then the tenant default currency (the `localization.currency` setting), if one is set. A `dynamic` field (the default mode) never lends its `defaultCurrency` — only `fixed` does — -so its column shows the tenant default. The cube query on this page carries no column -`currency`. +so its column shows the tenant default. A derived `sum` or `difference` whose monetary +operands all resolve to one `fixed` code takes that code at the field's step; any other +derived measure (a `ratio`, a `product`, operands in the tenant's currency) is monetary +only by its own `currency`, else its column carries none. The cube query on this page +carries no column `currency`. Over a `fixed` field the measure's `currency` may only restate the field's code. A different code is refused when the dataset compiles, with `DATASET_INVALID` / 400: delete -the measure's `currency`, and the column takes the field's code. A derived measure whose +the measure's `currency`, and the column takes the field's code. A derived `sum` or +`difference` whose operands resolve to one `fixed` code is held to the same rule: a +different `currency` is refused with `DATASET_INVALID` / 400 — delete the measure's +`currency` (the column takes the operands' code), or restate it. A derived measure whose monetary operands resolve to two different `fixed` codes is refused the same way, whatever the operation: derive it from measures in one currency, or drop the operand in the other. Neither a `fixed` code against the tenant default nor a measure `currency` over a diff --git a/content/docs/data-modeling/currency.mdx b/content/docs/data-modeling/currency.mdx index 7d19c6a88e8..d7a7a9f6de6 100644 --- a/content/docs/data-modeling/currency.mdx +++ b/content/docs/data-modeling/currency.mdx @@ -178,8 +178,12 @@ that column. A `count` column never carries `currencyField`. See the - **A derived measure whose monetary operands resolve to two different `fixed` codes** is refused at compile with `DATASET_INVALID` / 400, whatever the operation. Derive it from measures in one currency, or drop the operand in the other. A derived `sum` or - `difference` over one `fixed` code carries that code, so a measure built on it is judged - the same way. + `difference` over one `fixed` code carries that code, on its result column too, so a + measure built on it is judged the same way. +- **A derived `sum` or `difference` whose `currency` differs from its operands' one `fixed` + code** is refused at compile with `DATASET_INVALID` / 400: `currency: 'USD'` over two + measures fixed to `EUR` would print euros as dollars. Delete the measure's `currency`, and + the column takes the operands' code, or restate that code. - **Not judged:** a `fixed` code against the tenant currency, which compile does not know, and a measure `currency` over a `dynamic` field, which wins as the top of the currency chain. diff --git a/skills/objectstack-data/rules/field-types.md b/skills/objectstack-data/rules/field-types.md index 9221f0c4da1..2f353791bac 100644 --- a/skills/objectstack-data/rules/field-types.md +++ b/skills/objectstack-data/rules/field-types.md @@ -170,9 +170,10 @@ Stored as JSON on the parent row — no separate table / FK: `{ value, currency }`. Shown: `'fixed'`, `defaultCurrency`; `'dynamic'` (default), the tenant `localization.currency`, else a plain number (never `$`); `'record'`, each record's code in the `text` field `currencyField` -names (by convention `currency_code`), with `valueDomain: 'iso_4217_currency'`. A -dataset measure's own `currency` wins over a `'dynamic'` field; over a `'fixed'` one -it may only restate the code, else `DATASET_INVALID` (objectstack-ui → +names, with `valueDomain: 'iso_4217_currency'`. A +dataset measure's own `currency` wins over a `'dynamic'` field; over a `'fixed'` one, +or a derived sum/difference over one `'fixed'` code, it may only restate that code, +else `DATASET_INVALID`: delete it, and the column takes the code (objectstack-ui → Dashboards). A `'fixed'` code is not judged against the tenant's. Switched a stored field to `'record'`? Backfill: `os migrate record-currency-codes`. diff --git a/skills/objectstack-ui/rules/dashboards.md b/skills/objectstack-ui/rules/dashboards.md index 5c20f5ef605..c0d19a6d032 100644 --- a/skills/objectstack-ui/rules/dashboards.md +++ b/skills/objectstack-ui/rules/dashboards.md @@ -282,8 +282,7 @@ The **dataset dimension carries the default; a widget's `options.dateGranularity` overrides it for that widget only.** Give a date dimension a `dateGranularity` and any presentation that selects it groups by that bucket server-side — without it every distinct timestamp becomes its own -category, collapsing a 12-row seed into a 12-point flat line. (The old widget-level -`categoryGranularity` was removed in the single-form cutover.) +category, collapsing a 12-row seed into a 12-point flat line. ```typescript @@ -301,8 +300,9 @@ defineDataset({ // `currencyMode: 'record'` amount refuses `currency`, and sum/avg/min/max // unless grouped by its code field; then the column carries // `currencyField` (the per-row code column); a `count` never does. A `fixed` -// field refuses a different `currency` (delete it; restating is fine), and -// a derived measure refuses operands in two `fixed` codes (derive from one +// field, or a derived sum/difference over one `fixed` code, refuses another +// `currency` (delete it, the column takes that code; restating is fine); a +// derived measure refuses operands in two `fixed` codes (derive from one // currency or drop one). Not judged: `currency` over a `dynamic` field, a // `fixed` code against the tenant's. From 02980b195a47fed090d71dfc443a0e7f588cb34b Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 31 Oct 2026 15:21:20 +0000 Subject: [PATCH 2/2] chore(changeset): @objectstack/skills patch for the derived-currency refusal texts The skills catalog ships in @objectstack/skills (its build copies skills/** into dist), so a skills text change publishes and takes a patch changeset. Claude-Session: https://claude.ai/code/session_01FEkQ5nqSVzKxNiy5ZH2o44 Co-authored-by: Claude --- .changeset/28051-skills-derived-currency-refusal.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 .changeset/28051-skills-derived-currency-refusal.md diff --git a/.changeset/28051-skills-derived-currency-refusal.md b/.changeset/28051-skills-derived-currency-refusal.md new file mode 100644 index 00000000000..d10646a456e --- /dev/null +++ b/.changeset/28051-skills-derived-currency-refusal.md @@ -0,0 +1,12 @@ +--- +'@objectstack/skills': patch +--- + +docs(skills): the skills teach the third `fixed`-currency refusal, a derived measure's own `currency` against its operands' one `fixed` code + +Clause-②: no +Metadata-protocol: none + +- `objectstack-data` `rules/field-types.md` (Currency resolution) and `objectstack-ui` `rules/dashboards.md` (the dataset currency comment) taught two `fixed` refusals and missed the third: a derived `sum` or `difference` whose monetary operands all resolve to one `fixed` code refuses another `currency` at dataset compile, with `DATASET_INVALID` / 400. Both files now teach it beside the other two, with the shipped prescription: delete the measure's `currency` (the column takes the operands' code), or restate that code. +- Paid for inside each file: `field-types.md` drops the `currency_code` naming convention its own example already shows; `dashboards.md` drops the retired `categoryGranularity` parenthetical, which the parse refusal and the Dataset-Bound Widgets paragraph already carry. Neither ratchet row moves. +- No schema, key or behaviour changes.