Skip to content

Commit 36436fa

Browse files
docs(skills): teach the derived-measure fixed-currency refusal beside the two fixed-currency refusals (#28431)
<!-- claude-projects-attribution: {"github_login":"os-project-manager"} --> _Requested by **Leehom** · [project thread](https://claude.ai/code/project/chan_01TenzsRopZj4jMbvjX8uz1X?thread=cmsg_01TenzsRopZj4jMbvjX8uz1XCYhrp146Mk9BxnoRcGGQcD)_ Fixes #28051 Clause-②: no Metadata-protocol: none Cross-lane dispatch: `domain:skills` card dispatched and reviewed by `domain:server#2` under the maintainer's 「座位之间可以协调,你有空你就可以派」 (claim 6545242148); the two devx pages ride as the sibling PR #28015 (#27994) carried them. ## 维护者速读 - **改了什么**:两份 skills 和两页文档补上第三条 `fixed` 币种拒收:派生 `sum` / `difference` 的操作数全部落在同一个 `fixed` 币种时,度量自己声明另一个 `currency` 会在数据集编译时被拒(`DATASET_INVALID` / 400)。改法照运行时的原话:删掉度量的 `currency`(列取操作数的币种),或写同一币种。两页文档同时写明:这样的派生列在线上答复里也带那个币种(PR #28122 落地的行为,本任在 `main` 上实测)。 - **为什么改**:PR #28043 加了这条拒收,四处文字只教前两条;AI 按旧文给 EUR 固定币种的派生求和写上 `currency: 'USD'`,数据集会被拒而文档没说。 - **风险与回滚**:只改文字,无 schema、键或行为变化。两份 skills 的 token 天花板不动:每处新增都在同文件内删重复付账(`field-types.md` 删掉示例已给出的 `currency_code` 命名惯例;`dashboards.md` 删掉已退役的 `categoryGranularity` 括注,解析拒收与 Dataset-Bound Widgets 段已有它)。回滚即撤本 PR。 - **先看**:`skills/objectstack-ui/rules/dashboards.md` 的数据集币种注释(L300-306),`skills/objectstack-data/rules/field-types.md` 的 Currency resolution 段(L174-177)。 - **两分钟验证**:`node scripts/check-skills-token-ratchet.mjs` 预期全绿(`field-types.md` 3207/3207,`dashboards.md` 6259/6262);对照 `packages/services/service-analytics/src/measure-currency-coherence.ts` Guard 1 末尾的 `resolved?.fixed` 分支读四处文字。 - **需要你做的**:`skills/**` 是 Tier H 受管面:PR 留 draft,等你批准;契约复审由席位在本 head 上做。 ## What changes The shipped refusal, read from `measure-currency-coherence.ts` Guard 1 on `main` (and pinned in `record-currency-analytics.test.ts`): a derived measure that declares a `currency` over measures whose amounts are fixed to one code, with a different code, is refused `DATASET_INVALID` / 400; the message ends `delete the measure's \`currency\` (the column takes the operands' CODE), or restate CODE.` Each of the four texts now teaches that case and that prescription in its own words, beside the two `fixed` refusals PR #28015 taught: - `skills/objectstack-data/rules/field-types.md` (Currency resolution): "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". Reader: the agent writing a `currency` field and then a dataset measure over it; the mistake prevented is `currency: 'USD'` on a derived sum of EUR-fixed measures. Paid for by deleting "(by convention `currency_code`)", which the example two lines above already shows (`currencyField: 'currency_code'`). - `skills/objectstack-ui/rules/dashboards.md` (the dataset currency comment): "A `fixed` field, or a derived sum/difference over one `fixed` code, refuses another `currency` (delete it, the column takes that code; restating is fine)". Reader: the agent writing a dataset measure for a dashboard; the same mistake. Paid for by deleting "(The old widget-level `categoryGranularity` was removed in the single-form cutover.)": the parse refusal names that key with its prescription (`LEGACY_WIDGET_ANALYTICS_KEYS` in `dashboard.zod.ts`) and the Dataset-Bound Widgets paragraph already says the closed schema rejects the legacy keys by name, so the sentence had two homes. - `content/docs/data-modeling/currency.mdx` (Roll-ups and datasets): a new bullet for the derived case with the prescription, and the existing sentence "a derived `sum` or `difference` over one `fixed` code carries that code" now says "on its result column too": true on the wire since PR #28122 (#28049), measured below. - `content/docs/api/data-api.mdx` (the currency chain): the chain paragraph gains the derived step (a derived `sum` / `difference` whose operands all resolve to one `fixed` code takes that code at the field's step; any other derived measure is monetary only by its own `currency`, else its column carries none), and the refusal paragraph gains the derived case with the prescription. - `.changeset/28051-skills-derived-currency-refusal.md`: `@objectstack/skills` patch. The order ruled `skip-changeset`; os-dev.md's published-surface criterion wins over the order on a conflict, and it is measured: `packages/skills/scripts/sync-catalog.mjs` copies `skills/**` into that package's `dist/`, which is in its `files[]`, and the sibling PR #28015 carried the same patch changeset. Recorded in the report's `open_questions`. ### `skills/**` readings (lines, and tokens as the ratchet counts them) | File | Before | After | |:--|:--|:--| | `skills/objectstack-data/rules/field-types.md` | 304 lines / 3191 tokens | 305 lines / 3207 tokens (ceiling 3207, headroom 0) | | `skills/objectstack-ui/rules/dashboards.md` | 464 lines / 6261 tokens | 464 lines / 6259 tokens (ceiling 6262, headroom 3) | | all `skills/*/SKILL.md` | 4607 lines | 4607 lines (untouched) | | whole catalog `skills/**/*.md` | 17850 lines / 782492 bytes | 17851 lines / 782547 bytes | No ceiling row moves; `node scripts/check-skills-token-ratchet.mjs` exits 0 on the head. ## Tests No code changes. The wire behaviour the docs now teach was measured on `origin/main` `5da3a8ce` in this worktree, after `pnpm --filter '@objectstack/service-analytics^...' build` (lock VERDICT command-exit 0): ``` pnpm --filter @objectstack/service-analytics exec vitest run --maxWorkers=2 --reporter=verbose src/__tests__/record-currency-analytics.test.ts ✓ … > a derived measure column takes the currency its operands resolve to > native strategy: a sum or difference over one fixed code carries it; no other derived column gains one ✓ … > objectql strategy: a sum or difference over one fixed code carries it; no other derived column gains one ✓ … > the draft-preview path labels the same columns ✓ … > compile-time guards on measure currencies > a derived sum declaring a currency other than its operands' one fixed code is refused, naming the fix ✓ … > a derived difference declaring a currency other than its operands' one fixed code is refused, naming the fix ✓ … > CONTROL: a derived measure restating its operands' one fixed code is accepted Test Files 1 passed (1) Tests 60 passed (60) ``` The refusal pin asserts the message tail `delete the measure's \`currency\` (the column takes the operands' EUR), or restate EUR.`; the wire pin asserts `fields[].currency === 'EUR'` on `eur_twice` (sum) and `eur_net` (difference) with no `currencyField`, and `currency` undefined on every derived column whose operands resolve to no one `fixed` code (`usd_and_tenant`, `tenant_twice`, `jpy_twice`, `eur_per_invoice`, `eur_product`, `total_twice`). ## Acceptance notes - The card's Wanted quoted the message as it stood in PR #28043 ("or delete it, in which case the column carries no currency code"). PR #28122 (#28049) changed both the wire and that sentence before this card was dispatched; the order says to teach what landed, so the texts teach "the column takes the operands' code". - `content/docs/**` is not a governed surface, but it rides this Tier H PR, so the whole PR waits for the maintainer's approval. - No out-of-scope findings. ## Verification Head `02980b19`. - `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` on the final diff (5 files): 67 derived families, every one run locally with its exit code captured before any pipe (`cmd > log 2>&1 < /dev/null; EXIT=$?`). `--ran`: "67 derived, 66 run, 1 NOT-MEASURED, 0 UNRUN". - 66 exit 0, including `check-skills-token-ratchet` (+ `--self-test`), `check:skill-examples` (the edited `os:check` block in `dashboards.md`; after `pnpm --filter '@objectstack/client-react...' build`, the prerequisite it names), `check:docs`, `check:doc-anchors`, `check:doc-authoring`, `check:docs-transcript-drift`, `check-docs-section-name`, `check:pm-skill-id-lint`, `check:skill-identifier-liveness`, `check:skill-frame-sync`, `check:dts-closure`, `check:pm-governed-merges`, `check-empty-changeset --base origin/main`, `check-adr-0087-registration --base origin/main`, `check:published-files`, `check-changeset-no-major`. - NOT MEASURED: `check:dual-build-cjs-loads`, exit 3 (needs every package's `dist`; 68 packages unbuilt here). CI runs it. - `node scripts/pm/check-governed-merges.mjs --branch claude/project-thread-4snjmh`: "GOVERNED — landing tier: H(人合)", `skills/**` ×2 (`field-types.md`, `dashboards.md`); `content/docs/**` ×2 and the changeset are off the register; size 47 changed lines, under the human-merge threshold. Exit 3 (its GOVERNED verdict). - eslint `--no-inline-config` on the four edited files: exit 0. The repo-wide `pnpm lint` is CI's. - Full dogfood battery: n/a (no `!` in the title, no `(narrowing)` arm). - Head repo: internal (this branch is in `objectstack-ai/objectstack`). ## 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) - [x] 3. No formatter ran; `pnpm lint` passes. (eslint `--no-inline-config` on the four files: exit 0; the repo-wide run is CI's.) - [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_01FEkQ5nqSVzKxNiy5ZH2o44 --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 22663ca commit 36436fa

5 files changed

Lines changed: 35 additions & 12 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): the skills teach the third `fixed`-currency refusal, a derived measure's own `currency` against its operands' one `fixed` code
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 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.
11+
- 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.
12+
- No schema, key or behaviour changes.

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

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -518,12 +518,18 @@ monetary measure column takes the measure's own `currency`; then, only when the
518518
field is `currencyMode: 'fixed'`, that field's `currencyConfig.defaultCurrency`; then the
519519
tenant default currency (the `localization.currency` setting), if one is set. A
520520
`dynamic` field (the default mode) never lends its `defaultCurrency` — only `fixed` does —
521-
so its column shows the tenant default. The cube query on this page carries no column
522-
`currency`.
521+
so its column shows the tenant default. A derived `sum` or `difference` whose monetary
522+
operands all resolve to one `fixed` code takes that code at the field's step; any other
523+
derived measure (a `ratio`, a `product`, operands in the tenant's currency) is monetary
524+
only by its own `currency`, else its column carries none. The cube query on this page
525+
carries no column `currency`.
523526

524527
Over a `fixed` field the measure's `currency` may only restate the field's code. A
525528
different code is refused when the dataset compiles, with `DATASET_INVALID` / 400: delete
526-
the measure's `currency`, and the column takes the field's code. A derived measure whose
529+
the measure's `currency`, and the column takes the field's code. A derived `sum` or
530+
`difference` whose operands resolve to one `fixed` code is held to the same rule: a
531+
different `currency` is refused with `DATASET_INVALID` / 400 — delete the measure's
532+
`currency` (the column takes the operands' code), or restate it. A derived measure whose
527533
monetary operands resolve to two different `fixed` codes is refused the same way, whatever
528534
the operation: derive it from measures in one currency, or drop the operand in the other.
529535
Neither a `fixed` code against the tenant default nor a measure `currency` over a

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

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -178,8 +178,12 @@ that column. A `count` column never carries `currencyField`. See the
178178
- **A derived measure whose monetary operands resolve to two different `fixed` codes** is
179179
refused at compile with `DATASET_INVALID` / 400, whatever the operation. Derive it from
180180
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.
181+
`difference` over one `fixed` code carries that code, on its result column too, so a
182+
measure built on it is judged the same way.
183+
- **A derived `sum` or `difference` whose `currency` differs from its operands' one `fixed`
184+
code** is refused at compile with `DATASET_INVALID` / 400: `currency: 'USD'` over two
185+
measures fixed to `EUR` would print euros as dollars. Delete the measure's `currency`, and
186+
the column takes the operands' code, or restate that code.
183187
- **Not judged:** a `fixed` code against the tenant currency, which compile does not know,
184188
and a measure `currency` over a `dynamic` field, which wins as the top of the currency
185189
chain.

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

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -170,9 +170,10 @@ Stored as JSON on the parent row — no separate table / FK:
170170
`{ value, currency }`. Shown: `'fixed'`, `defaultCurrency`; `'dynamic'`
171171
(default), the tenant `localization.currency`, else a plain number (never
172172
`$`); `'record'`, each record's code in the `text` field `currencyField`
173-
names (by convention `currency_code`), with `valueDomain: 'iso_4217_currency'`. A
174-
dataset measure's own `currency` wins over a `'dynamic'` field; over a `'fixed'` one
175-
it may only restate the code, else `DATASET_INVALID` (objectstack-ui →
173+
names, with `valueDomain: 'iso_4217_currency'`. A
174+
dataset measure's own `currency` wins over a `'dynamic'` field; over a `'fixed'` one,
175+
or a derived sum/difference over one `'fixed'` code, it may only restate that code,
176+
else `DATASET_INVALID`: delete it, and the column takes the code (objectstack-ui →
176177
Dashboards). A `'fixed'` code is not judged against the tenant's. Switched a
177178
stored field to `'record'`? Backfill: `os migrate record-currency-codes`.
178179

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

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -282,8 +282,7 @@ The **dataset dimension carries the default; a widget's
282282
`options.dateGranularity` overrides it for that widget only.** Give a date
283283
dimension a `dateGranularity` and any presentation that selects it groups by that
284284
bucket server-side — without it every distinct timestamp becomes its own
285-
category, collapsing a 12-row seed into a 12-point flat line. (The old widget-level
286-
`categoryGranularity` was removed in the single-form cutover.)
285+
category, collapsing a 12-row seed into a 12-point flat line.
287286

288287
<!-- os:check -->
289288
```typescript
@@ -301,8 +300,9 @@ defineDataset({
301300
// `currencyMode: 'record'` amount refuses `currency`, and sum/avg/min/max
302301
// unless grouped by its code field; then the column carries
303302
// `currencyField` (the 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
303+
// field, or a derived sum/difference over one `fixed` code, refuses another
304+
// `currency` (delete it, the column takes that code; restating is fine); a
305+
// derived measure refuses operands in two `fixed` codes (derive from one
306306
// currency or drop one). Not judged: `currency` over a `dynamic` field, a
307307
// `fixed` code against the tenant's.
308308

0 commit comments

Comments
 (0)