diff --git a/.changeset/27994-fixed-currency-measure-refusals.md b/.changeset/27994-fixed-currency-measure-refusals.md new file mode 100644 index 00000000000..e60a2495cc7 --- /dev/null +++ b/.changeset/27994-fixed-currency-measure-refusals.md @@ -0,0 +1,12 @@ +--- +'@objectstack/skills': patch +--- + +docs(skills): a dataset measure's `currency` no longer "always wins" over a `fixed` field; the skills teach the two `fixed` refusals + +Clause-②: no +Metadata-protocol: none + +- `objectstack-data` `rules/field-types.md` (Currency resolution) and `objectstack-ui` `rules/dashboards.md` (the dataset currency comment) taught that a measure's own `currency` wins under `fixed`. Dataset compile now refuses, with `DATASET_INVALID` / 400, a measure `currency` that differs from its field's `fixed` code (delete it; restating the code is accepted) and a derived measure whose monetary operands resolve to two different `fixed` codes (derive from measures in one currency, or drop the operand in the other). Both files now say so, and keep the not-judged cases explicit: a `fixed` code against the tenant currency, and a measure `currency` over a `dynamic` field. +- Paid for inside each file by deleting a restating code comment; the remainder raises the two token-ratchet rows on the standing path. +- No schema, key or behaviour changes. diff --git a/content/docs/api/data-api.mdx b/content/docs/api/data-api.mdx index dfd4fcd4415..166c5247250 100644 --- a/content/docs/api/data-api.mdx +++ b/content/docs/api/data-api.mdx @@ -502,6 +502,14 @@ tenant default currency (the `localization.currency` setting), if one is set. A so its column shows the tenant default. 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 +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 +`dynamic` field is judged. + A measure column over a `currencyMode: 'record'` amount has one currency per row, so it carries `currencyField` instead of `currency`: the name of the dimension column, in the same answer, that groups by the amount's code field (`currencyConfig.currencyField`). Each row's diff --git a/content/docs/data-modeling/currency.mdx b/content/docs/data-modeling/currency.mdx index ebd68eb13df..c09ce58a7a4 100644 --- a/content/docs/data-modeling/currency.mdx +++ b/content/docs/data-modeling/currency.mdx @@ -171,6 +171,18 @@ that column. A `count` column never carries `currencyField`. See the currency** (another code field, a `fixed` code, an explicit measure code or the tenant currency) is refused at compile with `DATASET_INVALID` / 400, whatever the operation. A derived measure over a `count`, such as an amount per invoice, stays legal. +- **A dataset measure whose `currency` differs from its field's `fixed` code** is refused + at compile with `DATASET_INVALID` / 400: `currency: 'USD'` over a field fixed to `EUR` + would label euros as dollars. Delete the measure's `currency`, and the column takes the + field's code. A `currency` that restates the field's code is accepted. +- **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. +- **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. ### In the Console diff --git a/scripts/check-skills-token-ratchet.mjs b/scripts/check-skills-token-ratchet.mjs index 289e5263252..829b116a619 100644 --- a/scripts/check-skills-token-ratchet.mjs +++ b/scripts/check-skills-token-ratchet.mjs @@ -679,7 +679,15 @@ export const CEILINGS = new Map([ // zero headroom to absorb any of it, ceiling +126. Maintainer ruling // 2026-09-22 on decision batch #213 (item 1 carries the TOCs: 「四个超长 SKILL.md 按 500 行切,整段示例与 platform 的运维尾段下沉 references/,零删内容;同卡顺手:data 的 description 压到约 700 字符、长参考文件加目录」), // verbatim and untranslated: 「1 2 4 同意」. - ['skills/objectstack-data/rules/field-types.md', 3158], + // 3158 -> 3207 (#27994): the Currency resolution paragraph teaches the two `fixed` + // refusals #27991 added (a measure `currency` other than the field's `fixed` code; a + // derived measure over two `fixed` codes), each fix, and the not-judged tenant case. + // PAID DOWN FIRST by the Formula example's `returnType` comment, which restated the + // Calculated table's return types. The file is 12827 bytes -> 3207 tokens, zero + // headroom, ceiling +49, pinned AT the measurement. Standing ruling (2026-10-27, tap A, + // verbatim and untranslated): 「A:先削重复,余额按实测提高」; recognised path per ruling + // record 6513306068. + ['skills/objectstack-data/rules/field-types.md', 3207], ['skills/objectstack-data/rules/indexing.md', 3241], ['skills/objectstack-data/rules/lifecycle.md', 1590], ['skills/objectstack-data/rules/naming.md', 773], @@ -827,7 +835,15 @@ export const CEILINGS = new Map([ // follows it down to the new measurement, the ratchet's ordinary direction. // 6239 -> 6221 (#26602): LOWERED. The metric-widget bullet teaches the measure's `polarity`, // paid inside the file; the ceiling follows the file down to the measurement (24881 bytes). - ['skills/objectstack-ui/rules/dashboards.md', 6221], + // 6221 -> 6262 (#27994): the dataset currency comment teaches the two `fixed` refusals + // #27991 added, each fix, and the two not-judged cases. PAID DOWN FIRST: the metric + // example's `colorVariant` comment no longer restates that the measure's `format` / + // `currency` drive the number (Widget `options` says it), and the measure example in the + // currency comment shrinks to its `currency` key (the dataset above shows the shape). + // The file is 25047 bytes -> 6262 tokens, zero headroom, ceiling +41, pinned AT the + // measurement. Standing ruling (2026-10-27, tap A, verbatim and untranslated): + // 「A:先削重复,余额按实测提高」; recognised path per ruling record 6513306068. + ['skills/objectstack-ui/rules/dashboards.md', 6262], // 3011 -> 3141 (#19715, card item 4): a 5-line table of contents at the top, // anchors to the file's existing headings, nothing else changed. +132 tokens, // of which the row's 2 tokens of headroom absorb 2; the CEILING moves 130. diff --git a/skills/objectstack-data/rules/field-types.md b/skills/objectstack-data/rules/field-types.md index efef55ea9fb..019b2a657d4 100644 --- a/skills/objectstack-data/rules/field-types.md +++ b/skills/objectstack-data/rules/field-types.md @@ -181,8 +181,11 @@ 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'`. Under -`'fixed'`/`'dynamic'` a measure's own `currency` wins. +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` (delete it), as is a derived +measure over two different `'fixed'` codes (derive from one currency or drop an +operand). A `'fixed'` code is not judged against the tenant's. ### Select with Default @@ -243,7 +246,7 @@ import { F } from '@objectstack/spec'; const total = { type: 'formula', expression: F`record.amount * record.tax_rate`, - returnType: 'number', // 'number' | 'text' | 'boolean' | 'date' | 'currency' (money; own currencyConfig) + returnType: 'number', // see the Calculated table }; ``` diff --git a/skills/objectstack-ui/rules/dashboards.md b/skills/objectstack-ui/rules/dashboards.md index d12325d0fea..0e937873769 100644 --- a/skills/objectstack-ui/rules/dashboards.md +++ b/skills/objectstack-ui/rules/dashboards.md @@ -178,7 +178,7 @@ export const SalesDashboard: Dashboard = { dataset: 'opportunity_metrics', values: ['total_amount'], filter: { stage: { $nin: ['closed_won', 'closed_lost'] } }, layout: { x: 0, y: 0, w: 3, h: 2 }, - colorVariant: 'success', // the accent; the measure's own `format` + `currency` drive the number + colorVariant: 'success', // the accent only // Period-over-period: renderer fetches the prior quarter and // surfaces a secondary value + delta arrow automatically. compareTo: { kind: 'previousPeriod' }, @@ -295,13 +295,16 @@ defineDataset({ dimensions: [{ name: 'signed_date', field: 'signed_date', type: 'date', dateGranularity: 'month' }], measures: [{ name: 'signed_count', aggregate: 'count' }], }); -// A money measure may set `currency` (ISO 4217): -// `{ name: 'revenue', aggregate: 'sum', field: 'amount', currency: 'USD' }`. +// A money measure may set `currency` (ISO 4217), e.g. `currency: 'USD'`. // Chain: measure `currency` → the field's `defaultCurrency` (`fixed` // only) → tenant `localization.currency`; omit it for counts. A // `currencyMode: 'record'` amount refuses `currency` and ungrouped // sum/avg/min/max; grouped by its code field, the column carries -// `currencyField` (per-row code column); a `count` never does. +// `currencyField` (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 +// currency or drop one). Not judged: `currency` over a `dynamic` field, a +// `fixed` code against the tenant's. // The widget just selects the dimension by name: const signedByMonth: DashboardWidget = { id: 'signed_by_month', type: 'line',