Repository navigation
Commit 3604371
feat(spec,service-analytics): record-currency analytics — grouped answers name a per-row currency, the cube door refuses an ungrouped amount aggregate, compile guards on measure currencies (#27338)
epic:#8345#1 · os-bill · session_01Utp6CbTZrFqRYNgjSaSbN5
<!-- claude-projects-attribution: {"github_login":"os-bill"} -->
_Requested by **Bill** · [project
thread](https://claude.ai/code/project/chan_016iMXWSZDzqWRWvWzdNo8wg?thread=cmsg_016iMXWSZDzqWRWvWzdNo8wg9oTskgBVZmtxXLAEh251Cb)_
Fixes #27187
⛔ not released alone — R1: pairs with #27184, #27186, #27187, #27188
(one Version Packages release)
Clause-②: yes (widening)
Metadata-protocol: added
Skills-docs: follow-up owed — #27189
## 维护者速读
- **改了什么:** 分析结果里,record 币种金额的度量列新增
`currencyField`,指向按币种分组的那一列,每行的币种就是该行这列的值;cube 门拒收不按币种字段分组的
sum/avg/min/max(所有路径:数据集与已注册 cube、按对象名的临时查询、草稿预览);数据集编译期新增两条只针对 record
币种的护栏:record 金额上写死度量 `currency` 拒收(派生度量结果是 record 币种的也拒收),派生度量把 record
金额与别的币种混算拒收。
- **为什么改:** 测量轮探针 C:按币种分组的结果整列只标一个币种,每组都标错;探针 A:跨币种相加得出一个没有币种的数。
- **风险与回滚:** 只新增,不收窄:护栏只管随本期发布的 record 模式,以前能编译的写法一律不受影响;回滚即还原本 PR。契约键走
CONTRACT_REVIEW_TIER 复核。
- **先看:**
`packages/services/service-analytics/src/cube-record-currency-door.ts`(拦截判定)、`measure-currency-coherence.ts`
的 `recordPairDiffers`(何时算"与 record
金额币种不同")、`packages/spec/src/contracts/analytics-service.ts` 新键说明。
- **两分钟验证:** `pnpm --filter @objectstack/service-analytics exec vitest
run src/__tests__/record-currency-analytics.test.ts`,预期 29 passed(两种策略 +
临时查询 + 预览路径 + 护栏)。
- **需要你做的:** 无,席位落地。
## What changes
The record currency mode (#27268) has landed on `main`, and `main` is
brought in by the merge commit `48cdeb43` (no rebase) through
`scripts/pm/os-regen-merge.sh`. The script took main's side of
`packages/spec/authorable-surface/data.json` (`f79f10d0`), and
`check:generated` then read all 14 generated artifacts up to date, so no
regeneration commit was needed. The diff against `main` is this card's
own 10 files, +824 / -8. The contract-review fixes are `0d2291da`.
**Contract (spec).** `AnalyticsResult.fields[].currencyField?: string`
(`contracts/analytics-service.ts`), and the same key on
`AnalyticsResultResponseSchema` (`api/analytics.zod.ts`), which the
existing type-identity pin in `api/analytics.test.ts` holds equal to the
contract. On a measure column over a `currencyMode: 'record'` amount, it
names the dimension column of the same answer that groups by the
amount's code field. Each row's currency is that row's value of that
column, and the column carries no `currency`.
**Consumer (service-analytics).**
- `plugin.ts` relays `currencyField` through `sourceFieldMeta` only
under `currencyMode: 'record'`, by the same rule that relays
`defaultCurrency` only under `fixed`.
- `enrichResultColumns` sets the pointer from the selected dimensions.
It reads the dataset and the declaration, not the rows, so the live path
and the draft preview path agree. A record-mode column with no code
dimension gets neither key, and a `count` / `count_distinct` over the
amount gets neither key, grouped or not.
- `cube-record-currency-door.ts` runs on all three exits of `ensureCube`
(the inferred cube that an ad-hoc `query` over the object mints, the
augmented cube and the declared one), after the #21044 field-type door
and ahead of both strategies. The ADR-0037 draft-preview evaluator never
reaches `ensureCube`, so its per-query gate (`previewService.query`)
calls the same judgment. A `sum` / `avg` / `min` / `max` over a
record-mode amount whose `dimensions` do not include the code field (on
the same object) is refused `INVALID_FIELD` / 400 with `member`,
`param`, `cube`, `field` and `object`. Dataset `totals` groupings reach
it one by one, so a grand total over the amount is refused too.
- `measure-currency-coherence.ts`, called from `compileDataset`
(`DATASET_INVALID` / 400):
- a measure `currency` over a record-mode amount, whose currency is per
row, and a measure `currency` on a derived measure whose resolved
currency is the record mode;
- a derived measure (any op) that combines a record-mode amount with a
monetary operand in a provably different currency: another code field, a
`fixed` code, an explicit code or the tenant's.
- Both judge the record mode alone, so nothing that compiled before is
refused. The same shapes over `fixed` fields only (a measure `currency`
contradicting a `fixed` field, a derived measure over two different
`fixed` codes) are not judged; see Acceptance notes.
**Docs.** `content/docs/api/data-api.mdx` lists `currencyField` among
the optional `fields[]` members, which that page says mirror
`AnalyticsResultResponseSchema` member for member. It also states the
record-mode column and the ungrouped-aggregate refusal next to the
currency chain (`d24c889d`, prompted by the docs-drift comment).
## Why
ADR-0144 D2/D6 and workload row M0-3: the per-row currency on grouped
results, the cube door's refusal, and the guards on the record mode.
Measurement round on #8345: probe C (grouped values right, one label per
column), probe A (`m_mixed_sum` 200 with no currency).
## Tests
<details><summary>New suite:
src/__tests__/record-currency-analytics.test.ts — 29/29</summary>
Real ObjectQL engine over `SqlDriver` (better-sqlite3), the plugin's own
composition, run on two faces: `native` (plugin capabilities,
NativeSQLStrategy) and `objectql` (narrowed to the engine aggregate).
Per face: grouped by code gives `{ USD: 150, EUR: 100 }` and
`currencyField: 'currency'` with no `currency`; a totals grouping that
keeps the code is served; three refusals (grouped by another dimension,
no grouping, a grand total) answer `INVALID_FIELD` / 400 with field
`amount` and the object, and the first two read nothing; a `count` is
served ungrouped with neither key, and a `count` whose `field` is the
amount gets neither key when grouped; the cube face refuses the same
thing; the inferred cube (no registered dataset, `amount_sum`) refuses
the ungrouped amount before anything is read and serves it grouped by
`currency_code`. The draft preview path carries the pointer and refuses
the ungrouped amount. Compile guards: a measure `currency` over the
record amount is refused, and so is one on a derived sum of record
amounts; a record amount derived with a fixed amount (all four ops) or
with a tenant amount is refused, including one level up through a
derived sum. The controls compile: a measure `currency` over a fixed
field (contradicting or restating it), a derived sum of two different
fixed codes, an amount per invoice, an amount with itself, and a fixed
amount with the tenant one.
</details>
<details><summary>Existing pins unchanged</summary>
`currency-mode-relay.test.ts` (`m_explicit_fixed`: JPY over a fixed-EUR
field still answers JPY) and `query-dataset.test.ts` are byte-identical
to `main` at f79f10d. They were moved under the earlier narrowing
ruling and have since been restored.
</details>
<details><summary>Package runs</summary>
At f79f10d, with `main` merged in, under the verify lock: `pnpm install
--frozen-lockfile`, `pnpm --filter '@objectstack/service-analytics...'
build`, then service-analytics `typecheck && test`, gave `VERDICT
command-exit 0`, Test Files 196 passed (196), Tests 4734 passed / 282
skipped. The same counts as at 0d2291d. Spec: `tsc --noEmit -p
tsconfig.json` 0 errors; `vitest run src/api/analytics.test.ts` 34/34;
`check:generated` "All 14 generated artifacts are up to date", taken
against the dist that build produced. Nothing needed regenerating after
the merge.
</details>
<details><summary>Ablations (src-resolved; ablation-replace.mjs; every
leg restored blob == HEAD, git diff HEAD empty)</summary>
All at 0d2291d (the suite itself 29/29 there). The merge of `main`
changed none of this card's files:
- M1, the inferred-cube door call removed → 2 failed / 29 (the
inferred-cube case on both faces)
- M2, the draft-preview door call removed → 1 failed / 29 (the preview
refusal)
- S1, the derived arm of guard 1 disabled → 1 failed / 29
- S2, the amount-aggregate check on the pointer forced true → 2 failed /
29 (the grouped test on both faces, at `priced_invoices`)
- door: grouping check replaced by `continue` → 11 failed / 29
- per-row pointer assignment deleted → 5 failed / 29
- guard 1, the record arm disabled → 1 failed / 29
- guard 2, `recordPairDiffers` forced false → 6 failed / 29
</details>
## Acceptance notes
- **Guards confined to the record mode (seat ruling, revised after the
ADR contract review).** The fixed-only arms (a measure `currency`
contradicting a `fixed` field's code; a derived measure over two
different `fixed` codes) are outside ruling #27119 and ADR-0144, so R1
has no authority to narrow there. They were dropped at 507ed8f, the
`m_explicit_fixed` pin was restored, and the lines are back to `yes
(widening)` / `added`. The changeset carries no BREAKING paragraph and
no ADR-0087 marker. `check-adr-0087-registration`,
`check-changeset-no-major` and `check-empty-changeset` all exit 0 at
0d2291d. ADR-0144 Open item 12 has since been ruled, letter A
([`6486427200`](#8345 (comment))
on #8345): both fixed-only shapes are refused on a card of their own
after R1, and this PR stays record-mode only.
- **ADR-0144 citations re-checked against the landed text**
(`3eaecb21`). The fixed-only scope now cites open item 12 and its
ruling, and a consolidated total cites D6 (it reads the home field D5
stores) where it had cited D5.
- **Contract review at ae8e3d4: FAIL**
([`6490225184`](#27338 (comment))).
Both must-fixes and two should-fixes are in `0d2291da`: M1 (the
inferred-cube exit is judged), M2 (the draft preview is judged), S1 (a
`currency` on a record-resolved derived measure is refused) and S2 (no
pointer on a `count`). Each has its ablation above.
- **Follow-on, not R1 (S3).** A derived `sum` / `difference` of
record-mode operands over one code field, grouped by it, carries neither
key by contract, although its currency is computable from the same
resolver. A renderer shows a plain number, which is not a mislabel.
- **Wire key spelling: settled by the contract review, keep
`currencyField`.** On the wire it names a result column, consistent with
`fields[].name`.
- The cube face (`query`) carries no per-row pointer, as it carries no
`currency` today. Only the refusal applies there.
- A relationship-path amount (`account.amount`) is refused by the cube
door when ungrouped, because the door locates the column through the hop
resolver. The column enrichment and the compile guards stand down on
dotted fields, the same tier as the existing compile field-type door.
- A `where` that pins one currency code is not counted as grouping. The
rule is the grouping alone.
- An explicit measure `currency` over a `dynamic` field is not judged
(Q6's third guard). It is not in this card.
## Verification
Derived gates: `node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` at 0d2291d, against a fresh `origin/main`
(`968925c8`), gave 127 commands. Each ran with its exit code captured
before any pipe.
Reconciliation (`--ran`, exit codes recorded): `127 derived famil(ies)
accounted for — 126 run, 1 NOT-MEASURED (1 DERIVED from a recorded exit
3)`. All 126 measured gates exited 0.
NOT MEASURED, prerequisite refused locally (exit 3), CI-owned:
- `node scripts/check-plugin-teardown-shape.mjs --self-test`: the
shallow checkout cannot reach its pinned positive-control commit.
Pre-push checks, run by hand before each push: `check-regen-pending
--pre-push` exit 0; `check-commit-card-trailers --pre-push` exit 0 (no
card relation and no model identifier on any pushed commit).
At f79f10d, after the merge of `main` (no code change):
`check-adr-0087-registration`, `check-changeset-no-major` and
`check-empty-changeset` exit 0 against `origin/main`.
Full dogfood: n/a. The title has no `!` and the Clause-② arm is
`(widening)`.
## Human Minimum
- [x] 1. Schemas start as Zod; `packages/spec` holds no runtime logic.
- [x] 2. Every name I added uses the casing for its kind.
- [ ] 3. No formatter ran (true); `pnpm lint` is the CI-owned repo scan,
not run locally.
- [x] 4. One issue, one branch, 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.
- [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.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01Utp6Ca8jzC2P7EAQvqjNBb)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent ded67bf commit 3604371
10 files changed
Lines changed: 824 additions & 8 deletions
File tree
- .changeset
- content/docs/api
- packages
- services/service-analytics/src
- __tests__
- spec/src
- api
- contracts
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
474 | 474 | | |
475 | 475 | | |
476 | 476 | | |
477 | | - | |
| 477 | + | |
478 | 478 | | |
479 | 479 | | |
480 | 480 | | |
| |||
500 | 500 | | |
501 | 501 | | |
502 | 502 | | |
| 503 | + | |
| 504 | + | |
| 505 | + | |
| 506 | + | |
| 507 | + | |
| 508 | + | |
| 509 | + | |
503 | 510 | | |
504 | 511 | | |
505 | 512 | | |
| |||
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
81 | 81 | | |
82 | 82 | | |
83 | 83 | | |
| 84 | + | |
84 | 85 | | |
85 | 86 | | |
86 | 87 | | |
| |||
1134 | 1135 | | |
1135 | 1136 | | |
1136 | 1137 | | |
| 1138 | + | |
| 1139 | + | |
| 1140 | + | |
| 1141 | + | |
| 1142 | + | |
| 1143 | + | |
1137 | 1144 | | |
1138 | 1145 | | |
1139 | 1146 | | |
| |||
1163 | 1170 | | |
1164 | 1171 | | |
1165 | 1172 | | |
1166 | | - | |
| 1173 | + | |
1167 | 1174 | | |
1168 | 1175 | | |
1169 | 1176 | | |
| |||
2549 | 2556 | | |
2550 | 2557 | | |
2551 | 2558 | | |
| 2559 | + | |
| 2560 | + | |
| 2561 | + | |
| 2562 | + | |
| 2563 | + | |
| 2564 | + | |
| 2565 | + | |
2552 | 2566 | | |
2553 | 2567 | | |
2554 | 2568 | | |
| |||
2711 | 2725 | | |
2712 | 2726 | | |
2713 | 2727 | | |
| 2728 | + | |
| 2729 | + | |
| 2730 | + | |
| 2731 | + | |
2714 | 2732 | | |
2715 | 2733 | | |
2716 | 2734 | | |
| |||
3158 | 3176 | | |
3159 | 3177 | | |
3160 | 3178 | | |
3161 | | - | |
| 3179 | + | |
| 3180 | + | |
| 3181 | + | |
| 3182 | + | |
| 3183 | + | |
| 3184 | + | |
| 3185 | + | |
| 3186 | + | |
| 3187 | + | |
| 3188 | + | |
| 3189 | + | |
| 3190 | + | |
3162 | 3191 | | |
3163 | 3192 | | |
3164 | | - | |
| 3193 | + | |
| 3194 | + | |
| 3195 | + | |
| 3196 | + | |
| 3197 | + | |
| 3198 | + | |
3165 | 3199 | | |
3166 | 3200 | | |
3167 | 3201 | | |
| |||
3483 | 3517 | | |
3484 | 3518 | | |
3485 | 3519 | | |
| 3520 | + | |
| 3521 | + | |
| 3522 | + | |
| 3523 | + | |
3486 | 3524 | | |
3487 | 3525 | | |
3488 | 3526 | | |
| |||
3566 | 3604 | | |
3567 | 3605 | | |
3568 | 3606 | | |
| 3607 | + | |
3569 | 3608 | | |
3570 | 3609 | | |
3571 | 3610 | | |
| |||
3582 | 3621 | | |
3583 | 3622 | | |
3584 | 3623 | | |
| 3624 | + | |
3585 | 3625 | | |
3586 | 3626 | | |
3587 | 3627 | | |
| |||
3624 | 3664 | | |
3625 | 3665 | | |
3626 | 3666 | | |
| 3667 | + | |
| 3668 | + | |
| 3669 | + | |
| 3670 | + | |
| 3671 | + | |
| 3672 | + | |
| 3673 | + | |
| 3674 | + | |
| 3675 | + | |
| 3676 | + | |
| 3677 | + | |
| 3678 | + | |
| 3679 | + | |
| 3680 | + | |
| 3681 | + | |
| 3682 | + | |
| 3683 | + | |
| 3684 | + | |
| 3685 | + | |
| 3686 | + | |
| 3687 | + | |
| 3688 | + | |
| 3689 | + | |
| 3690 | + | |
| 3691 | + | |
| 3692 | + | |
| 3693 | + | |
| 3694 | + | |
| 3695 | + | |
| 3696 | + | |
| 3697 | + | |
| 3698 | + | |
| 3699 | + | |
| 3700 | + | |
| 3701 | + | |
| 3702 | + | |
| 3703 | + | |
| 3704 | + | |
| 3705 | + | |
| 3706 | + | |
3627 | 3707 | | |
3628 | 3708 | | |
3629 | 3709 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
0 commit comments