Repository navigation
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
Conversation
…unt points at a sibling ISO 4217 code field Claude-Session: https://claude.ai/code/session_01Utp6CbfVSc1hXtxLUPHZ2f Co-authored-by: Claude <noreply@anthropic.com>
…ent in the dropped-refinements ledger Claude-Session: https://claude.ai/code/session_01Utp6CbfVSc1hXtxLUPHZ2f Co-authored-by: Claude <noreply@anthropic.com>
…mode sites Claude-Session: https://claude.ai/code/session_01Utp6CbfVSc1hXtxLUPHZ2f Co-authored-by: Claude <noreply@anthropic.com>
…field naming convention, paid by tightening the currency block Claude-Session: https://claude.ai/code/session_01Utp6CbfVSc1hXtxLUPHZ2f Co-authored-by: Claude <noreply@anthropic.com>
…cord-mode currencyField Claude-Session: https://claude.ai/code/session_01Utp6CbfVSc1hXtxLUPHZ2f Co-authored-by: Claude <noreply@anthropic.com>
…ield on grouped results, cube door refuses an ungrouped amount aggregate, compile guards on measure currencies (WIP) Claude-Session: https://claude.ai/code/session_01Utp6Ca8jzC2P7EAQvqjNBb Co-authored-by: Claude <noreply@anthropic.com>
…, the preview path and the compile guards Claude-Session: https://claude.ai/code/session_01Utp6Ca8jzC2P7EAQvqjNBb Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Utp6CbfVSc1hXtxLUPHZ2f Co-authored-by: Claude <noreply@anthropic.com>
…ly restate its code — the two chain pins move to the dynamic field Claude-Session: https://claude.ai/code/session_01Utp6Ca8jzC2P7EAQvqjNBb Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Utp6CbfVSc1hXtxLUPHZ2f Co-authored-by: Claude <noreply@anthropic.com>
…side currency Claude-Session: https://claude.ai/code/session_01Utp6Ca8jzC2P7EAQvqjNBb Co-authored-by: Claude <noreply@anthropic.com>
…ics minor, the two compile guards declared breaking with their ADR-0087 disposition Claude-Session: https://claude.ai/code/session_01Utp6Ca8jzC2P7EAQvqjNBb Co-authored-by: Claude <noreply@anthropic.com>
…protocol narrowed — the compile guards refuse dataset metadata that compiled before Claude-Session: https://claude.ai/code/session_01Utp6Ca8jzC2P7EAQvqjNBb Co-authored-by: Claude <noreply@anthropic.com>
…e field-level answer never reads a code from a record-mode config Claude-Session: https://claude.ai/code/session_01Utp6CbfVSc1hXtxLUPHZ2f Co-authored-by: Claude <noreply@anthropic.com>
…urrencyField row The field form's new `currencyConfig.currencyField` row is one more unwalked composite child (10 -> 11) and one more translated row label in each locale's positive control (695 -> 696). Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Utp6CbfVSc1hXtxLUPHZ2f
… default Under `currencyMode: 'record'` each record names its own code in the sibling field, which only a per-row renderer can read. The field-level answer is now `undefined` outright, so a record with a blank code shows no symbol instead of a currency it never named. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Utp6CbfVSc1hXtxLUPHZ2f
…only — the fixed-only arms are not R1's to narrow; the two chain pins restored, the changeset back to widening / added Claude-Session: https://claude.ai/code/session_01Utp6Ca8jzC2P7EAQvqjNBb Co-authored-by: Claude <noreply@anthropic.com>
…485013063 The review on 775d8c0 (PASS, no must-fix) found three places where the record said less, or other, than its own Decision: - Builds-on scoped ADR-0112 to two runtime refusals and called every build-time refusal an authoring diagnostic, while D6 places the ungrouped-aggregate refusal in two request-time doors. The paragraph now says ADR-0112 governs every refusal that answers on the wire, that "at build" covers the declared aggregates (summary roll-ups at registration, dataset measures at compile), and that an ad-hoc aggregate meets the same rule at the request door with the existing catalog code INVALID_FIELD; the two new codes stay the only ledger additions. - Consequences predicted the consumers' Metadata-protocol declarations. It now states the record's own claim: record and homeAmount are additive, the one narrowing of what an author can write is #27157's, and each consumer's semver is its own changeset's to declare. - Open item 12 records what the ruling on #27119 did not decide: whether cross-currency arithmetic between fixed fields of different codes is refused. The epic PM seat ruled that #27187 (PR #27338) confines its compile guards to record-mode amounts; the general guard waits on the maintainer and epic #8345 carries the question. Items 1-11 keep their numbers. Claude-Session: https://claude.ai/code/session_01Utp6CbTZrFqRYNgjSaSbN5 Co-authored-by: Claude <noreply@anthropic.com>
…356516 The delta review on e9bb190 (PASS, no must-fix) found three residues in the text the previous commit added: - Builds-on called the declared-aggregate refusals "authoring diagnostics, not wire errors", which is wrong for the dataset-compile half (the compile runs in the request path and DATASET_INVALID / 400 reaches the caller) and loose for the registration half (INVALID_METADATA / 422 when a metadata write triggers it). The clause now names those existing codes; "They are the only ledger additions" stays the operative sentence. - Consequences attributed #27157's narrowing to the epic; the record's own References line places #27157 outside the epic, as a member of R1. It now reads "R1's one narrowing of what an author can write". - Open item 12 said the epic PM seat "ruled", a word this record reserves for the maintainer's text, and cited nothing a reader can open. It now says "decided", cites the decision comment on #27187 and PR #27338's commit 507ed8f as the tree evidence, and links the question as posted on epic #8345. Claude-Session: https://claude.ai/code/session_01Utp6CbTZrFqRYNgjSaSbN5 Co-authored-by: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Utp6CbfVSc1hXtxLUPHZ2f
The authorable-surface baseline is taken from main (#27233 retired `CurrencyValue`) and rebuilt, keeping `currencyConfig.currencyField`; the field reference page is regenerated from the merged tree. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Utp6CbfVSc1hXtxLUPHZ2f
Brings in the record currency mode at its final head (main merged through 73725c2). No conflicts. Claude-Session: https://claude.ai/code/session_01Utp6Ca8jzC2P7EAQvqjNBb Co-authored-by: Claude <noreply@anthropic.com>
…ts ruling, D6 for consolidation Claude-Session: https://claude.ai/code/session_01Utp6Ca8jzC2P7EAQvqjNBb Co-authored-by: Claude <noreply@anthropic.com>
|
epic:#8345#1 · os-bill · session_01Utp6CbTZrFqRYNgjSaSbN5 Contract reviewServed-tier: ① Derived judgments
② Semver level
③ Boundary flags
④ Design judgment
Should-fixes, to fold into the same delta:
Everything else in this file holds: the
Implemented-by: VERDICT: FAIL |
…d cube and the draft preview; refuse a currency on a record-resolved derived measure; no pointer on a count Contract review on ae8e3d4: the inferred-cube exit of ensureCube and the draft-preview evaluator now refuse an ungrouped sum/avg/min/max over a record-mode amount, as the declared paths do. Guard 1 also refuses a measure currency on a derived measure whose resolved currency is the record mode, and a count column no longer carries currencyField. Claude-Session: https://claude.ai/code/session_01Utp6Ca8jzC2P7EAQvqjNBb Co-authored-by: Claude <noreply@anthropic.com>
…ency (#27188) A grouped analytics answer over a `record`-mode amount marks the measure column with `currencyField` (#27338), naming the result column that holds each row's ISO 4217 code. `measureCellCurrency(field, row)` reads it structurally (the spec type does not carry it yet) and returns that row's code, nothing for a row without one, and the column's declared `currency` for every other column. The dashboard table's body cells and the Studio dataset preview read it off the RAW row, so a relabelled dimension cannot move a code. Totals and cross-tab cells pass no row and draw no symbol. Claude-Session: https://claude.ai/code/session_01Utp6CXtadYa5QXb2PbXPev Co-authored-by: Claude <noreply@anthropic.com>
|
epic:#8345#1 · os-bill · session_01Utp6CbTZrFqRYNgjSaSbN5 Contract reviewServed-tier: ① Derived judgments
② Semver levelUnchanged and right: ③ Boundary flags
④ Design judgment
Implemented-by: VERDICT: PASS |
|
epic:#8345#1 · os-bill · session_01Utp6CbTZrFqRYNgjSaSbN5 Contract reviewServed-tier: ① Derived judgments
② Semver levelUnchanged and right. Changeset at ③ Boundary flags
④ Design judgment
Implemented-by: VERDICT: PASS Head reviewed: |
|
epic:#8345#1 · os-bill · session_01Utp6CbTZrFqRYNgjSaSbN5 席位记录 · epic PM
|
and #27339 Claude-Session: https://claude.ai/code/session_01Utp6CaqVRa7ow6CjE1JwaG Co-authored-by: Claude <noreply@anthropic.com>
epic:#8345#1 · os-bill · session_01Utp6CbTZrFqRYNgjSaSbN5
Requested by Bill · project thread
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
维护者速读
currencyField,指向按币种分组的那一列,每行的币种就是该行这列的值;cube 门拒收不按币种字段分组的 sum/avg/min/max(所有路径:数据集与已注册 cube、按对象名的临时查询、草稿预览);数据集编译期新增两条只针对 record 币种的护栏:record 金额上写死度量currency拒收(派生度量结果是 record 币种的也拒收),派生度量把 record 金额与别的币种混算拒收。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, andmainis brought in by the merge commit48cdeb43(no rebase) throughscripts/pm/os-regen-merge.sh. The script took main's side ofpackages/spec/authorable-surface/data.json(f79f10d0), andcheck:generatedthen read all 14 generated artifacts up to date, so no regeneration commit was needed. The diff againstmainis this card's own 10 files, +824 / -8. The contract-review fixes are0d2291da.Contract (spec).
AnalyticsResult.fields[].currencyField?: string(contracts/analytics-service.ts), and the same key onAnalyticsResultResponseSchema(api/analytics.zod.ts), which the existing type-identity pin inapi/analytics.test.tsholds equal to the contract. On a measure column over acurrencyMode: '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 nocurrency.Consumer (service-analytics).
plugin.tsrelayscurrencyFieldthroughsourceFieldMetaonly undercurrencyMode: 'record', by the same rule that relaysdefaultCurrencyonly underfixed.enrichResultColumnssets 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 acount/count_distinctover the amount gets neither key, grouped or not.cube-record-currency-door.tsruns on all three exits ofensureCube(the inferred cube that an ad-hocqueryover the object mints, the augmented cube and the declared one), after the analytics: a config cube min/max over a text column is served at the cube door under fields[] type number; the dataset door refuses the pair by the compatibility table, the cube door consults nothing #21044 field-type door and ahead of both strategies. The ADR-0037 draft-preview evaluator never reachesensureCube, so its per-query gate (previewService.query) calls the same judgment. Asum/avg/min/maxover a record-mode amount whosedimensionsdo not include the code field (on the same object) is refusedINVALID_FIELD/ 400 withmember,param,cube,fieldandobject. Datasettotalsgroupings reach it one by one, so a grand total over the amount is refused too.measure-currency-coherence.ts, called fromcompileDataset(DATASET_INVALID/ 400):currencyover a record-mode amount, whose currency is per row, and a measurecurrencyon a derived measure whose resolved currency is the record mode;fixedcode, an explicit code or the tenant's.fixedfields only (a measurecurrencycontradicting afixedfield, a derived measure over two differentfixedcodes) are not judged; see Acceptance notes.Docs.
content/docs/api/data-api.mdxlistscurrencyFieldamong the optionalfields[]members, which that page says mirrorAnalyticsResultResponseSchemamember 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_sum200 with no currency).Tests
New suite: src/__tests__/record-currency-analytics.test.ts — 29/29
Real ObjectQL engine over
SqlDriver(better-sqlite3), the plugin's own composition, run on two faces:native(plugin capabilities, NativeSQLStrategy) andobjectql(narrowed to the engine aggregate). Per face: grouped by code gives{ USD: 150, EUR: 100 }andcurrencyField: 'currency'with nocurrency; a totals grouping that keeps the code is served; three refusals (grouped by another dimension, no grouping, a grand total) answerINVALID_FIELD/ 400 with fieldamountand the object, and the first two read nothing; acountis served ungrouped with neither key, and acountwhosefieldis 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 bycurrency_code. The draft preview path carries the pointer and refuses the ungrouped amount. Compile guards: a measurecurrencyover 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 measurecurrencyover 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.Existing pins unchanged
currency-mode-relay.test.ts(m_explicit_fixed: JPY over a fixed-EUR field still answers JPY) andquery-dataset.test.tsare byte-identical tomainat f79f10d. They were moved under the earlier narrowing ruling and have since been restored.Package runs
At f79f10d, with
mainmerged in, under the verify lock:pnpm install --frozen-lockfile,pnpm --filter '@objectstack/service-analytics...' build, then service-analyticstypecheck && test, gaveVERDICT command-exit 0, Test Files 196 passed (196), Tests 4734 passed / 282 skipped. The same counts as at 0d2291d. Spec:tsc --noEmit -p tsconfig.json0 errors;vitest run src/api/analytics.test.ts34/34;check:generated"All 14 generated artifacts are up to date", taken against the dist that build produced. Nothing needed regenerating after the merge.Ablations (src-resolved; ablation-replace.mjs; every leg restored blob == HEAD, git diff HEAD empty)
All at 0d2291d (the suite itself 29/29 there). The merge of
mainchanged none of this card's files:priced_invoices)continue→ 11 failed / 29recordPairDiffersforced false → 6 failed / 29Acceptance notes
currencycontradicting afixedfield's code; a derived measure over two differentfixedcodes) are outside ruling [Decision] Currency, all of it: resume multi-currency (#8345) and in what shape, and one rule for an amount whose currency is not known (Studio's USD, the template's USD,fixed's CNY, the example's USD) #27119 and ADR-0144, so R1 has no authority to narrow there. They were dropped at 507ed8f, them_explicit_fixedpin was restored, and the lines are back toyes (widening)/added. The changeset carries no BREAKING paragraph and no ADR-0087 marker.check-adr-0087-registration,check-changeset-no-majorandcheck-empty-changesetall exit 0 at 0d2291d. ADR-0144 Open item 12 has since been ruled, letter A (6486427200on [Epic] multi-currency (shape C′): record currency, dated exchange rates, write-time home-currency projection — 17 children, R1/R2/R3 release pairing (ruling 6465431966) #8345): both fixed-only shapes are refused on a card of their own after R1, and this PR stays record-mode only.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.6490225184). Both must-fixes and two should-fixes are in0d2291da: M1 (the inferred-cube exit is judged), M2 (the draft preview is judged), S1 (acurrencyon a record-resolved derived measure is refused) and S2 (no pointer on acount). Each has its ablation above.sum/differenceof 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.currencyField. On the wire it names a result column, consistent withfields[].name.query) carries no per-row pointer, as it carries nocurrencytoday. Only the refusal applies there.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.wherethat pins one currency code is not counted as grouping. The rule is the grouping alone.currencyover adynamicfield 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/objectstackat 0d2291d, against a freshorigin/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-pushexit 0;check-commit-card-trailers --pre-pushexit 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-majorandcheck-empty-changesetexit 0 againstorigin/main.Full dogfood: n/a. The title has no
!and the Clause-② arm is(widening).Human Minimum
packages/specholds no runtime logic.pnpm lintis the CI-owned repo scan, not run locally.packages/spectouched: its generated artifacts are regenerated and committed.content/docs/references/,packages/*/CHANGELOG.mdorcontent/docs/releases/.Generated by Claude Code