Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions .changeset/27994-fixed-currency-measure-refusals.md
Original file line number Diff line number Diff line change
@@ -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.
8 changes: 8 additions & 0 deletions content/docs/api/data-api.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
12 changes: 12 additions & 0 deletions content/docs/data-modeling/currency.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
20 changes: 18 additions & 2 deletions scripts/check-skills-token-ratchet.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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],
Expand Down Expand Up @@ -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.
Expand Down
9 changes: 6 additions & 3 deletions skills/objectstack-data/rules/field-types.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
};
```

Expand Down
11 changes: 7 additions & 4 deletions skills/objectstack-ui/rules/dashboards.md
Original file line number Diff line number Diff line change
Expand Up @@ -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' },
Expand Down Expand Up @@ -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',
Expand Down
Loading