Skip to content

Commit 21b2a10

Browse files
docs(skills): a dataset measure's currency no longer always wins over a fixed field; teach the two fixed-currency refusals (#28015)
<!-- claude-projects-attribution: {"github_login":"os-elon-musk"} --> _Requested by **Elon** · [project thread](https://claude.ai/code/project/chan_01WedDzV26nniwxv7jC88B9D?thread=cmsg_01WedDzV26nniwxv7jC88B9DDr1qV3x1ZEoUX6f9g6WuLJ)_ Fixes #27994 Clause-②: no Metadata-protocol: none Cross-domain exception: triage 6516910471 grants this one `domain:skills` PR the two devx pages `content/docs/data-modeling/currency.mdx` and `content/docs/api/data-api.mdx`; claim 6517255606 declares them. ## 维护者速读 - 改了什么:两份 skills 和两页文档补上 #27991 新增的两条 `fixed` 币种拒收:度量的 `currency` 与字段的 `fixed` 币种不同会被拒(删掉度量的 `currency` 即可,写同一币种照收);派生度量的金额操作数落在两个不同 `fixed` 币种会被拒(改用同一币种的度量,或去掉另一币种的操作数)。两种不判的情况也写明了。 - 为什么改:这四处原来教「度量自己的 `currency` 总是优先」,#27991 合入后这句对冲突的 `fixed` 币种已不成立,AI 按旧文写出的数据集会在编译时被拒。 - 风险与回滚:只改文字,无 schema、键或行为变化。两份 skills 的 token 天花板按常设裁决先删同文件重复、余额按实测提高(+49、+41)。回滚即撤本 PR。 - 先看:`skills/objectstack-data/rules/field-types.md` 的 Currency resolution 段,`skills/objectstack-ui/rules/dashboards.md` 的数据集币种注释。 - 两分钟验证:`node scripts/check-skills-token-ratchet.mjs` 预期全绿;对照 `packages/services/service-analytics/src/measure-currency-coherence.ts` 的两个 `fixed` 分支读四处文字。 - 需要你做的:这是 `skills/**` 受管面(Tier H),契约复审与 ACCEPT 后需要审批人批准。 ## What changes Each file states, in its own words, the two `fixed`-currency refusals PR #27991 added to dataset compile (`DATASET_INVALID` / 400), each fix, and the two cases that stay unjudged: 1. **Shape 1**: a measure `currency` other than its field's `fixed` code is refused. Fix: delete the measure's `currency`, and the column takes the field's code. Restating the code is accepted. 2. **Shape 2**: a derived measure, any op, whose monetary operands resolve to two different `fixed` codes is refused. Fix: derive from measures in one currency, or drop the operand in the other. 3. **Not judged**: a `fixed` code against the tenant currency; a measure `currency` over a `dynamic` field. - `skills/objectstack-data/rules/field-types.md` (Currency resolution): the sentence "Under `'fixed'`/`'dynamic'` a measure's own `currency` wins" is replaced by the `dynamic` / `fixed` split, the two refusals and their fixes, and the not-judged tenant case. - `skills/objectstack-ui/rules/dashboards.md` (the dataset currency comment in the date-granularity example): the two refusals, their fixes and both not-judged cases follow the record-mode sentence. - `content/docs/data-modeling/currency.mdx` (Roll-ups and datasets): three bullets after the record-mode ones, including that a derived `sum` / `difference` over one `fixed` code carries that code, so a measure built on it is judged the same way. - `content/docs/api/data-api.mdx` (the currency chain): one paragraph after the chain. - `.changeset/27994-fixed-currency-measure-refusals.md`: `@objectstack/skills` patch. ### `skills/**` token budget (standing path) Ruling quoted (maintainer, 2026-10-27, skills-lane decision card, verbatim and untranslated): 「A:先削重复,余额按实测提高」; the gate-recognised path per ruling record 6513306068 on #27847. - `field-types.md` 3158 -> 3207 (+49). Paid down first: the Formula example's `returnType` comment, which restated the Calculated table's return types, becomes a pointer to that table. - `dashboards.md` 6221 -> 6262 (+41). 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 `defineDataset` above shows the measure shape). - Both rows pinned AT the measurement, with the deletion and the ruling recorded beside each row in `scripts/check-skills-token-ratchet.mjs`. ## Why After #27991 (#27579, ruling 6486427200, ADR-0144 Open item 12 letter A) the four texts taught a rule the compiler now refuses for a contradicting `fixed` code, and they named only the record-mode refusals from #27338. Taught from the merged `measure-currency-coherence.ts` and `record-currency-analytics.test.ts`, not from the card summary. ## Verification Head `946f6b44` (commit `0776905e` plus a plain merge of `origin/main`, which touches none of these files; the derivation is identical after the merge). - `node scripts/pm/dispatch-gates.mjs --commands`: 85 derived families, all run. `--ran`: "85 derived, 84 run, 1 NOT-MEASURED, 0 UNRUN". 84 exited 0, including `check-skills-token-ratchet`, `check:skill-examples` (the edited `os:check` block in `dashboards.md`), `check:docs`, `check:doc-anchors`, `check:doc-authoring`, `check:docs-transcript-drift` and `check-docs-section-name`. NOT MEASURED: `check:dual-build-cjs-loads`, exit 3 (needs every package's dist). CI runs it. - Docs build: `pnpm turbo run build --filter=@objectstack/docs` (the `apps/docs/vercel.json` production command), exit 0. - Artifact-roster families whose roster sits under a path of this diff: `check-published-list-mirrors`, `check:changeset-fixed`, `check:engine-double-contract`, `check:i18n-stale-fill`, `check-affected-docs` exit 0; `check:published-readme-exports` exit 3 (needs built dists, not measured). - The offline lychee link check was not run locally: this diff adds no link. ## Human Minimum - [x] 1. Schemas start as Zod; `packages/spec` holds no runtime logic. (not touched) - [x] 2. Every name I added uses the casing for its kind. (no names) - [ ] 3. No formatter ran; `pnpm lint` passes. (No formatter ran; `pnpm lint` was not run locally, the diff carries no code. CI runs it.) - [x] 4. One issue, one branch off an up-to-date `main`, one pull request. - [x] 5. A change that publishes carries a changeset, and both declaration lines are filled in here and in the changeset. - [x] 6. `packages/spec` touched: its generated artifacts are regenerated and committed. (not touched) - [x] 7. No hand edits to `content/docs/references/`, `packages/*/CHANGELOG.md` or `content/docs/releases/`. - [x] 8. Title, body and comments are in English. - [x] The *Before Submitting* list in CONTRIBUTING.md is done. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_016WV4czgqyQbG9VgmyNYDNt Co-authored-by: Claude <noreply@anthropic.com>
1 parent 99fa527 commit 21b2a10

6 files changed

Lines changed: 63 additions & 9 deletions

File tree

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
'@objectstack/skills': patch
3+
---
4+
5+
docs(skills): a dataset measure's `currency` no longer "always wins" over a `fixed` field; the skills teach the two `fixed` refusals
6+
7+
Clause-②: no
8+
Metadata-protocol: none
9+
10+
- `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.
11+
- Paid for inside each file by deleting a restating code comment; the remainder raises the two token-ratchet rows on the standing path.
12+
- No schema, key or behaviour changes.

‎content/docs/api/data-api.mdx‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -502,6 +502,14 @@ tenant default currency (the `localization.currency` setting), if one is set. A
502502
so its column shows the tenant default. The cube query on this page carries no column
503503
`currency`.
504504

505+
Over a `fixed` field the measure's `currency` may only restate the field's code. A
506+
different code is refused when the dataset compiles, with `DATASET_INVALID` / 400: delete
507+
the measure's `currency`, and the column takes the field's code. A derived measure whose
508+
monetary operands resolve to two different `fixed` codes is refused the same way, whatever
509+
the operation: derive it from measures in one currency, or drop the operand in the other.
510+
Neither a `fixed` code against the tenant default nor a measure `currency` over a
511+
`dynamic` field is judged.
512+
505513
A measure column over a `currencyMode: 'record'` amount has one currency per row, so it
506514
carries `currencyField` instead of `currency`: the name of the dimension column, in the same
507515
answer, that groups by the amount's code field (`currencyConfig.currencyField`). Each row's

‎content/docs/data-modeling/currency.mdx‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,18 @@ that column. A `count` column never carries `currencyField`. See the
171171
currency** (another code field, a `fixed` code, an explicit measure code or the tenant
172172
currency) is refused at compile with `DATASET_INVALID` / 400, whatever the operation.
173173
A derived measure over a `count`, such as an amount per invoice, stays legal.
174+
- **A dataset measure whose `currency` differs from its field's `fixed` code** is refused
175+
at compile with `DATASET_INVALID` / 400: `currency: 'USD'` over a field fixed to `EUR`
176+
would label euros as dollars. Delete the measure's `currency`, and the column takes the
177+
field's code. A `currency` that restates the field's code is accepted.
178+
- **A derived measure whose monetary operands resolve to two different `fixed` codes** is
179+
refused at compile with `DATASET_INVALID` / 400, whatever the operation. Derive it from
180+
measures in one currency, or drop the operand in the other. A derived `sum` or
181+
`difference` over one `fixed` code carries that code, so a measure built on it is judged
182+
the same way.
183+
- **Not judged:** a `fixed` code against the tenant currency, which compile does not know,
184+
and a measure `currency` over a `dynamic` field, which wins as the top of the currency
185+
chain.
174186

175187
### In the Console
176188

‎scripts/check-skills-token-ratchet.mjs‎

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -679,7 +679,15 @@ export const CEILINGS = new Map([
679679
// zero headroom to absorb any of it, ceiling +126. Maintainer ruling
680680
// 2026-09-22 on decision batch #213 (item 1 carries the TOCs: 「四个超长 SKILL.md 按 500 行切,整段示例与 platform 的运维尾段下沉 references/,零删内容;同卡顺手:data 的 description 压到约 700 字符、长参考文件加目录」),
681681
// verbatim and untranslated: 「1 2 4 同意」.
682-
['skills/objectstack-data/rules/field-types.md', 3158],
682+
// 3158 -> 3207 (#27994): the Currency resolution paragraph teaches the two `fixed`
683+
// refusals #27991 added (a measure `currency` other than the field's `fixed` code; a
684+
// derived measure over two `fixed` codes), each fix, and the not-judged tenant case.
685+
// PAID DOWN FIRST by the Formula example's `returnType` comment, which restated the
686+
// Calculated table's return types. The file is 12827 bytes -> 3207 tokens, zero
687+
// headroom, ceiling +49, pinned AT the measurement. Standing ruling (2026-10-27, tap A,
688+
// verbatim and untranslated): 「A:先削重复,余额按实测提高」; recognised path per ruling
689+
// record 6513306068.
690+
['skills/objectstack-data/rules/field-types.md', 3207],
683691
['skills/objectstack-data/rules/indexing.md', 3241],
684692
['skills/objectstack-data/rules/lifecycle.md', 1590],
685693
['skills/objectstack-data/rules/naming.md', 773],
@@ -827,7 +835,15 @@ export const CEILINGS = new Map([
827835
// follows it down to the new measurement, the ratchet's ordinary direction.
828836
// 6239 -> 6221 (#26602): LOWERED. The metric-widget bullet teaches the measure's `polarity`,
829837
// paid inside the file; the ceiling follows the file down to the measurement (24881 bytes).
830-
['skills/objectstack-ui/rules/dashboards.md', 6221],
838+
// 6221 -> 6262 (#27994): the dataset currency comment teaches the two `fixed` refusals
839+
// #27991 added, each fix, and the two not-judged cases. PAID DOWN FIRST: the metric
840+
// example's `colorVariant` comment no longer restates that the measure's `format` /
841+
// `currency` drive the number (Widget `options` says it), and the measure example in the
842+
// currency comment shrinks to its `currency` key (the dataset above shows the shape).
843+
// The file is 25047 bytes -> 6262 tokens, zero headroom, ceiling +41, pinned AT the
844+
// measurement. Standing ruling (2026-10-27, tap A, verbatim and untranslated):
845+
// 「A:先削重复,余额按实测提高」; recognised path per ruling record 6513306068.
846+
['skills/objectstack-ui/rules/dashboards.md', 6262],
831847
// 3011 -> 3141 (#19715, card item 4): a 5-line table of contents at the top,
832848
// anchors to the file's existing headings, nothing else changed. +132 tokens,
833849
// of which the row's 2 tokens of headroom absorb 2; the CEILING moves 130.

‎skills/objectstack-data/rules/field-types.md‎

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -181,8 +181,11 @@ Stored as JSON on the parent row — no separate table / FK:
181181
`{ value, currency }`. Shown: `'fixed'`, `defaultCurrency`; `'dynamic'`
182182
(default), the tenant `localization.currency`, else a plain number (never
183183
`$`); `'record'`, each record's code in the `text` field `currencyField`
184-
names (by convention `currency_code`), with `valueDomain: 'iso_4217_currency'`. Under
185-
`'fixed'`/`'dynamic'` a measure's own `currency` wins.
184+
names (by convention `currency_code`), with `valueDomain: 'iso_4217_currency'`. A
185+
dataset measure's own `currency` wins over a `'dynamic'` field; over a `'fixed'` one
186+
it may only restate the code, else `DATASET_INVALID` (delete it), as is a derived
187+
measure over two different `'fixed'` codes (derive from one currency or drop an
188+
operand). A `'fixed'` code is not judged against the tenant's.
186189

187190
### Select with Default
188191

@@ -243,7 +246,7 @@ import { F } from '@objectstack/spec';
243246
const total = {
244247
type: 'formula',
245248
expression: F`record.amount * record.tax_rate`,
246-
returnType: 'number', // 'number' | 'text' | 'boolean' | 'date' | 'currency' (money; own currencyConfig)
249+
returnType: 'number', // see the Calculated table
247250
};
248251
```
249252

‎skills/objectstack-ui/rules/dashboards.md‎

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -178,7 +178,7 @@ export const SalesDashboard: Dashboard = {
178178
dataset: 'opportunity_metrics', values: ['total_amount'],
179179
filter: { stage: { $nin: ['closed_won', 'closed_lost'] } },
180180
layout: { x: 0, y: 0, w: 3, h: 2 },
181-
colorVariant: 'success', // the accent; the measure's own `format` + `currency` drive the number
181+
colorVariant: 'success', // the accent only
182182
// Period-over-period: renderer fetches the prior quarter and
183183
// surfaces a secondary value + delta arrow automatically.
184184
compareTo: { kind: 'previousPeriod' },
@@ -295,13 +295,16 @@ defineDataset({
295295
dimensions: [{ name: 'signed_date', field: 'signed_date', type: 'date', dateGranularity: 'month' }],
296296
measures: [{ name: 'signed_count', aggregate: 'count' }],
297297
});
298-
// A money measure may set `currency` (ISO 4217):
299-
// `{ name: 'revenue', aggregate: 'sum', field: 'amount', currency: 'USD' }`.
298+
// A money measure may set `currency` (ISO 4217), e.g. `currency: 'USD'`.
300299
// Chain: measure `currency` → the field's `defaultCurrency` (`fixed`
301300
// only) → tenant `localization.currency`; omit it for counts. A
302301
// `currencyMode: 'record'` amount refuses `currency` and ungrouped
303302
// sum/avg/min/max; grouped by its code field, the column carries
304-
// `currencyField` (per-row code column); a `count` never does.
303+
// `currencyField` (per-row code column); a `count` never does. A `fixed`
304+
// field refuses a different `currency` (delete it; restating is fine), and
305+
// a derived measure refuses operands in two `fixed` codes (derive from one
306+
// currency or drop one). Not judged: `currency` over a `dynamic` field, a
307+
// `fixed` code against the tenant's.
305308

306309
// The widget just selects the dimension by name:
307310
const signedByMonth: DashboardWidget = { id: 'signed_by_month', type: 'line',

0 commit comments

Comments
 (0)