Repository navigation
Commit 96367f1
fix(service-analytics): a derived sum or difference over one fixed currency code carries that code on its column (#28122)
<!-- claude-projects-attribution: {"github_login":"os-elon-musk"} -->
_Requested by **Elon** · [project
thread](https://claude.ai/code/project/chan_01WedDzV26nniwxv7jC88B9D?thread=cmsg_01WedDzV26nniwxv7jC88B9DF7znmBWA8q35pxUyjMRykb)_
Fixes #28049
domain:server#3 · os-elon-musk · session_016WV4d1NUiRegpdMBrWq7FZ
Clause-②: no (the response comes to say what
`content/docs/data-modeling/currency.mdx` L180–181 already declares: "A
derived `sum` or `difference` over one `fixed` code carries that code")
Metadata-protocol: none
Skills-docs: none. `git grep -n -i currency skills/` hits
`skills/objectstack-ui/rules/dashboards.md` L298–306 and
`skills/objectstack-data/rules/field-types.md` L180–188; both state the
measure / field chain and the refusals, and neither says a derived
measure carries no code, so neither becomes false.
`content/docs/api/data-api.mdx` L497–509 (the currency chain) likewise
says nothing about derived measures and stays true.
## 维护者速读
- 改了什么:数据集里一个衍生度量(`derived`),如果是 `sum` /
`difference`,且其所有金额操作数都固定为同一个币种(例如两个固定 EUR
的度量相加),查询结果的这一列现在带上该币种(`currency: 'EUR'`)。线上查询和 Studio 草稿预览一致。
-
为什么改:文档早已写明“同一固定币种上的衍生求和/差带该币种”,但实测修复前该列在线上是不带币种的裸数字,前端只能不带货币符号显示欧元。编译期守卫早已算出这个币种,本次让结果列直接读同一个解析结果,不再另算一遍。
-
风险与回滚:只给原来缺币种的这类衍生列补上标签;比率、乘积、混合租户币种、记录币种、动态字段上的显式币种等其余衍生列都保持原答案(测试逐一钉住)。不改
spec、不改作者能写什么。回滚即 revert 本 PR。
-
先看:`packages/services/service-analytics/src/measure-currency-coherence.ts`(解析抽成
`measureCurrencyResolver`),`analytics-service.ts` 的
`enrichResultColumns` 币种链(`derivedCode`)。
- 两分钟验证:`pnpm --filter @objectstack/service-analytics exec vitest run
src/__tests__/record-currency-analytics.test.ts`,预期 52 passed(新增 3
条:两种策略与草稿预览下 `eur_twice` / `eur_net` 列为 `EUR`,6 个对照列无币种)。
- 需要你做的:确认本 PR 对 #28019 尚未发布的 changeset 的更正(见 Acceptance notes
第一条;`check-empty-changeset` 因此按设计保持红,等人工确认)。
## What changes
- **`measure-currency-coherence.ts`.** The compile guard's per-measure
currency resolution (`resolvedOf`, `monetaryOperands`) moves out of
`assertMeasureCurrenciesCoherent` into `measureCurrencyResolver(dataset,
declaredCurrency)`, unchanged, with one reader added: `fixedCodeOf(m)`,
the one `fixed` code a measure resolves to, else `undefined`. The guard
now destructures the same resolver. Module-internal; not exported from
the package entry.
- **The refusal of a derived `currency` contradicting its operands'
`fixed` code** (PR #28043) said "restate the operands' EUR as the
measure's `currency`, or delete it (the column then carries no currency
code)". After this change that parenthetical is false, so it now reads
"delete the measure's `currency` (the column takes the operands' EUR),
or restate EUR." The field-level refusal beside it already spells its
fix this way.
- **`analytics-service.ts`.** The `declaredCurrency` adapter over
`sourceFieldMeta` that `compile()` handed the compiler becomes one
private member, and `enrichResultColumns` builds a
`measureCurrencyResolver` over the same adapter. A derived measure's
column is monetary when `fixedCodeOf` answers, and the chain is explicit
measure `currency` → field `fixed` code → derived `fixed` code → tenant.
The guard already refuses a derived `currency` that contradicts the
resolved code, so the first and third links cannot disagree.
- **`record-currency-analytics.test.ts`.** New block "a derived measure
column takes the currency its operands resolve to": native and ObjectQL
strategies plus the draft preview. Pins `EUR` on a `sum` over a
field-fixed and a restated EUR measure, a `difference`, and a restated
derived; pins no `currency` and no `currencyField` on a fixed+tenant
sum, a tenant+tenant sum, a sum of explicit JPY over a `dynamic` field,
a `ratio`, a `product`, and a record-mode sum. Values checked too (`{
USD: 20, EUR: 180 }`). The existing message assertion moves to the new
wording.
- **`.changeset/28049-derived-sum-fixed-currency-column.md`** (new,
patch) and **`.changeset/28019-derived-measure-fixed-currency.md`** (one
sentence corrected, see below).
## Tests
<details><summary>Red on main, green after, ablation</summary>
- New block on the unfixed tree (`origin/main` `8cb8c7f4`, test only): 3
failed / 49 skipped (filtered). The draft preview failed
`AssertionError: eur_twice: expected undefined to be 'EUR'`. That first
draft also selected a derived-over-derived measure, which failed both
live strategies earlier, in the executor (second acceptance note), so it
was dropped from the block. The ablation below is the clean red for the
final block on all three paths.
- Fixed head `670a7790`: `record-currency-analytics.test.ts` 52 passed.
- Ablation on the committed head, `scripts/ablation-replace.mjs` on
`analytics-service.ts` (`const derivedCode = m.derived ?
currencies.fixedCodeOf(m) : undefined;` → `const derivedCode = undefined
as string | undefined; // ABLATION`; anchor 1 → 0, blob `98e0c551` →
`885a2a1f`): 3 failed / 49 passed. Restored: blob back to `98e0c551` =
HEAD, `git diff HEAD` empty. The suite imports the service from `src`
(relative imports), so no dist rebuild was in the path.
- `pnpm --filter @objectstack/service-analytics test`: 196 files passed,
4762 passed / 282 skipped. `pnpm --filter @objectstack/service-analytics
typecheck` (includes `src/__tests__`): exit 0.
</details>
## Verification
<details><summary>Gates at head 670a779</summary>
- `dispatch-gates --commands` derived 62 families; all run with exit
codes recorded, after `turbo run build
--filter='@objectstack/dogfood^...'
--filter='@objectstack/runtime^...'`.
- 60 exit 0. `check:dual-build-cjs-loads` exit 3 (no dist for seven
unrelated packages, e.g. `@objectstack/studio`): NOT MEASURED locally,
CI runs it. `check-empty-changeset --base origin/main` exit 1: the
deliberate correction below, red by design.
- `--ran`: 62 derived, 61 run, 1 NOT-MEASURED (derived from the recorded
exit 3), 0 unrun.
- `pnpm lint`: exit 0.
- The head is 6 commits behind `origin/main`; none touches
`service-analytics` or the corrected changeset.
</details>
## Acceptance notes
- **A pending release note corrected (needs a person's confirmation).**
`.changeset/28019-derived-measure-fixed-currency.md` (PR #28043, not yet
released) told readers the refusal's fix was "restate the operands' code
as the measure's `currency`, or delete it (the column then carries no
currency code; a derived column takes no code from its operands on the
wire)". This PR makes that parenthetical false, so the sentence now
reads "delete the measure's `currency` (the column takes the operands'
code), or restate that code." Nothing else in that note changed.
`check-empty-changeset` refuses any edit to another PR's changeset and
stays red until a person confirms; restoring the old sentence would
publish a false statement in the same release as this fix.
- **Derived-over-derived on the live query.** The compile guard resolves
a derived measure built on another derived one, and the currency docs
describe that shape. A live `queryDataset` selecting such a measure
fails on both strategies: "Measure 'eur_twice' on cube … aggregates
field 'eur_twice', which object … does not have". The draft preview
serves it. That is the executor, outside this card, and is reported to
the seat. The new block does not use the shape.
- The column `type` stays `number`, as for a base `sum` over a currency
field. `currency` is what makes a column monetary on the wire.
## 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.
- [x] 3. No formatter ran; `pnpm lint` passes.
- [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: 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 (the 维护者速读 block is the
repo's own Chinese reader section).
- [x] The *Before Submitting* list in CONTRIBUTING.md is done.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_016WV4d1NUiRegpdMBrWq7FZ
---
_Generated by [Claude
Code](https://claude.ai/code/session_016WV4d1NUiRegpdMBrWq7FZ)_
Co-authored-by: Claude <noreply@anthropic.com>1 parent 2695de7 commit 96367f1
5 files changed
Lines changed: 133 additions & 20 deletions
File tree
- .changeset
- packages/services/service-analytics/src
- __tests__
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
9 | 9 | | |
10 | 10 | | |
11 | 11 | | |
12 | | - | |
| 12 | + | |
13 | 13 | | |
14 | 14 | | |
15 | 15 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
279 | 279 | | |
280 | 280 | | |
281 | 281 | | |
| 282 | + | |
| 283 | + | |
| 284 | + | |
| 285 | + | |
| 286 | + | |
| 287 | + | |
| 288 | + | |
| 289 | + | |
| 290 | + | |
| 291 | + | |
| 292 | + | |
| 293 | + | |
| 294 | + | |
| 295 | + | |
| 296 | + | |
| 297 | + | |
| 298 | + | |
| 299 | + | |
| 300 | + | |
| 301 | + | |
| 302 | + | |
| 303 | + | |
| 304 | + | |
| 305 | + | |
| 306 | + | |
| 307 | + | |
| 308 | + | |
| 309 | + | |
| 310 | + | |
| 311 | + | |
| 312 | + | |
| 313 | + | |
| 314 | + | |
| 315 | + | |
| 316 | + | |
| 317 | + | |
| 318 | + | |
| 319 | + | |
| 320 | + | |
| 321 | + | |
| 322 | + | |
| 323 | + | |
| 324 | + | |
| 325 | + | |
| 326 | + | |
| 327 | + | |
| 328 | + | |
| 329 | + | |
| 330 | + | |
| 331 | + | |
| 332 | + | |
| 333 | + | |
| 334 | + | |
| 335 | + | |
| 336 | + | |
| 337 | + | |
| 338 | + | |
| 339 | + | |
| 340 | + | |
| 341 | + | |
| 342 | + | |
| 343 | + | |
| 344 | + | |
| 345 | + | |
| 346 | + | |
| 347 | + | |
| 348 | + | |
| 349 | + | |
282 | 350 | | |
283 | 351 | | |
284 | 352 | | |
| |||
384 | 452 | | |
385 | 453 | | |
386 | 454 | | |
387 | | - | |
| 455 | + | |
388 | 456 | | |
389 | 457 | | |
390 | 458 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
99 | 99 | | |
100 | 100 | | |
101 | 101 | | |
| 102 | + | |
102 | 103 | | |
103 | 104 | | |
104 | 105 | | |
| |||
2563 | 2564 | | |
2564 | 2565 | | |
2565 | 2566 | | |
2566 | | - | |
2567 | | - | |
2568 | | - | |
2569 | | - | |
2570 | | - | |
| 2567 | + | |
2571 | 2568 | | |
2572 | 2569 | | |
2573 | 2570 | | |
| 2571 | + | |
| 2572 | + | |
| 2573 | + | |
| 2574 | + | |
| 2575 | + | |
| 2576 | + | |
| 2577 | + | |
| 2578 | + | |
| 2579 | + | |
| 2580 | + | |
| 2581 | + | |
2574 | 2582 | | |
2575 | 2583 | | |
2576 | 2584 | | |
| |||
3131 | 3139 | | |
3132 | 3140 | | |
3133 | 3141 | | |
| 3142 | + | |
3134 | 3143 | | |
3135 | 3144 | | |
3136 | 3145 | | |
| |||
3192 | 3201 | | |
3193 | 3202 | | |
3194 | 3203 | | |
| 3204 | + | |
| 3205 | + | |
| 3206 | + | |
| 3207 | + | |
| 3208 | + | |
| 3209 | + | |
| 3210 | + | |
| 3211 | + | |
3195 | 3212 | | |
3196 | 3213 | | |
3197 | 3214 | | |
| 3215 | + | |
3198 | 3216 | | |
3199 | 3217 | | |
3200 | 3218 | | |
3201 | 3219 | | |
3202 | 3220 | | |
3203 | 3221 | | |
3204 | | - | |
| 3222 | + | |
3205 | 3223 | | |
3206 | | - | |
| 3224 | + | |
3207 | 3225 | | |
3208 | 3226 | | |
3209 | 3227 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
46 | 46 | | |
47 | 47 | | |
48 | 48 | | |
49 | | - | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
50 | 52 | | |
51 | 53 | | |
52 | 54 | | |
| |||
97 | 99 | | |
98 | 100 | | |
99 | 101 | | |
| 102 | + | |
| 103 | + | |
100 | 104 | | |
101 | | - | |
102 | | - | |
103 | | - | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
104 | 109 | | |
105 | | - | |
106 | | - | |
107 | | - | |
108 | | - | |
| 110 | + | |
109 | 111 | | |
110 | 112 | | |
111 | 113 | | |
112 | 114 | | |
113 | | - | |
114 | 115 | | |
115 | 116 | | |
116 | 117 | | |
| |||
148 | 149 | | |
149 | 150 | | |
150 | 151 | | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
151 | 166 | | |
152 | 167 | | |
153 | 168 | | |
| |||
185 | 200 | | |
186 | 201 | | |
187 | 202 | | |
188 | | - | |
189 | | - | |
| 203 | + | |
| 204 | + | |
190 | 205 | | |
191 | 206 | | |
192 | 207 | | |
| |||
0 commit comments