Skip to content

fix(service-analytics): a count that names an amount field is no amount, in the currency guard and the result column - #28360

Merged
objectstack-fleet[bot] merged 1 commit into
mainfrom
claude/project-thread-5zv5e6
Oct 31, 2026
Merged

objectstack-fleet[bot] merged 1 commit into
mainfrom
claude/project-thread-5zv5e6

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Tesla · project thread

Fixes #28194

Clause-②: yes (widening)
Metadata-protocol: none

维护者速读

  • 改了什么: 数据集里「对金额字段做 count」不再被当成金额:编译期币种检查不再拿它和别的金额比币种,所以「美元合计 ÷ 有金额的发票数」这类比率能保存了;同时这类计数的结果列也不再带币种,看板上「有金额的发票数」磁贴显示 3 而不是 €3。
  • 为什么改: count 数的是行,不是钱;ADR-0144 开放项 12 的裁决原文是「count 操作数不参与比较」,但代码只在 count 不带 field 时做到。结果列的同一盲区由席位裁定并入本 PR。
  • 风险与回滚: 只放宽两处误判,不会让错的数字通过:两个不同 fixed 币种相加、按记录币种的金额与固定币种相比仍被拒;sum/avg/min/max 的列照旧带币种。回滚即撤 PR。
  • 先看: ① packages/services/service-analytics/src/measure-currency-coherence.ts 的 resolvedOf 非派生分支;② analytics-service.ts 里 enrichResultColumns 的 monetary 一行;③ record-currency-analytics.test.ts 新增的两组用例。
  • 两分钟验证: pnpm --filter @objectstack/service-analytics exec vitest run src/__tests__/record-currency-analytics.test.ts,预期 60 个用例全绿(其中 8 个是新增)。
  • 需要你做的: 无,席位落地。

What changes

  1. The compile-time currency guard (measureCurrencyResolver). resolvedOf's non-derived branch resolved a currency from the measure's field without reading its aggregate, so a count that names an amount field ({ aggregate: 'count', field: 'amount' }) resolved to that amount's currency, and guard 2 compared it. ratio(usd_total, priced_invoices) was refused DATASET_INVALID as a cross-currency combination. The branch now resolves a currency only for an amount aggregate, through the one AMOUNT_AGGREGATES set (cube-record-currency-door.ts, already exported there and read by the cube door and the record-currency column; nothing is copied or moved).
    • The dispatch asked whether a count reaches monetaryOperands another way. Measured on main 69c39b16: yes, a count declaring its own currency (no field) went through the same branch's m.currency arm, so ratio(total, { count, currency: USD }) was refused as record vs USD. The gate covers the whole branch, so that entry closes too (seat ruling), and it is pinned.
    • Guard 1 is unchanged: it reads the field, not the resolved currency, so a count's measure currency that contradicts its field's fixed code is still refused (pinned as a control).
  2. The result column (enrichResultColumns, seat ruling: same defect class, folded in). Its fixed/tenant arm counted any measure over a currency-type field as monetary, so a count over a fixed EUR amount carried currency: "EUR", and over a tenant-currency amount carried the tenant's code. What users see change: a count tile or column over an amount field stops printing as money (3, not €3 or ¥3). The record arm already gated on AMOUNT_AGGREGATES; this arm now does too. A sum / avg / min / max over the same field keeps its code, and a measure that declares its own currency keeps it as before.

Tests

In src/__tests__/record-currency-analytics.test.ts (real ObjectQL engine over SQLite, both strategies plus the draft preview):

  • a count over an amount field carries no currency: a count over a fixed EUR amount and over a tenant-currency amount carry no currency; CONTROL: a sum over each still carries EUR / CNY (native, objectql, draft preview).
  • accepted: a derived ratio | sum | difference of an amount and a count over an amount field, or a count declaring a currency (the card's ratio(usd, priced_invoices) among them).
  • CONTROL: beside those counts, two different fixed codes summed and a record amount against a fixed one stay refused DATASET_INVALID / 400.
  • CONTROL: a count's measure currency contradicting its fixed field is still refused (guard 1).

Acceptance notes

  • The tests import the service through relative src paths (../plugin.js), so the ablations needed no dist build.
  • An author-declared currency on a count still labels that count's column (unchanged, not judged here); it is only no longer an operand guard 2 compares.
  • A derived sum of an amount and a count now resolves to the amount's currency alone, as a count without a field already did before this change.

Verification

Red on the unfixed code (85afc7b's tests over the 69c39b1 source)
× native strategy: a count over a fixed or a tenant-currency amount is a plain number; a sum keeps the code
× objectql strategy: …
× the draft-preview path labels the same columns
× accepted: a derived ratio of an amount and a count over an amount field, or a count declaring a currency
× accepted: a derived sum …
× accepted: a derived difference …
Tests  6 failed | 54 passed (60)
AssertionError: expected 'EUR' to be undefined
AssertionError: expected '[dataset-compiler] dataset "os27187_g…' to be undefined
Green and ablations at 85afc7b
  • record-currency-analytics.test.ts: Tests 60 passed (60).
  • Ablation 1 (node scripts/ablation-replace.mjs, resolver gate made always-true, anchor 1 → 0, blob 985caf62 → e563f99c): the three accepted: a derived … tests red, 3 failed | 57 passed; restored, blob == HEAD, git diff HEAD empty.
  • Ablation 2 (column gate removed, anchor 1 → 0, blob 0ea0b0f3 → 624524bf): the three a count over an amount field … tests red, 3 failed | 57 passed; restored, blob == HEAD, git diff HEAD empty.
  • pnpm --filter @objectstack/service-analytics test: Test Files 200 passed (200), Tests 4814 passed | 282 skipped; typecheck (tsc --noEmit) exit 0 (under os-verify-lock, VERDICT command-exit 0).
Derived gates at 85afc7b: 62 derived, 61 exit 0, 1 NOT MEASURED

node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack on this tree (4 paths vs merge base 69c39b1), every line run with stdin closed, exit codes recorded before any pipe; --ran reconciliation: "62 derived famil(ies) accounted for — 61 run, 1 NOT-MEASURED".

  • pnpm check:dual-build-cjs-loads NOT MEASURED: exit 3, PREREQUISITE NOT MET (it reads every package's built dist/, and this worktree built only the service-analytics closure). Declared narrowing; CI's Lint & Repo Gates runs it on a full build. The diff changes no export or entry point.
  • node scripts/check-plugin-teardown-shape.mjs --self-test first exited 3 on the shallow clone (its pinned fixture commit was absent); after fetching that one commit it exits 0 (48 cases).
node scripts/check-adr-0087-registration.mjs --base origin/main :: exit 0
node scripts/check-adr-0087-registration.mjs --self-test :: exit 0
node scripts/check-changeset-no-major.mjs --base origin/main :: exit 0
node scripts/check-closing-keyword-parity.mjs :: exit 0
node scripts/check-closing-keyword-parity.mjs --self-test :: exit 0
node scripts/check-comment-mask-adoption.mjs :: exit 0
node scripts/check-comment-mask-adoption.mjs --self-test :: exit 0
node scripts/check-comment-mask-corpus.mjs :: exit 0
node scripts/check-dts-emitted.mjs --self-test :: exit 0
node scripts/check-empty-changeset.mjs --base origin/main :: exit 0
node scripts/check-empty-changeset.mjs --self-test :: exit 0
node scripts/check-issue-citations.mjs :: exit 0
node scripts/check-plugin-teardown-shape.mjs :: exit 0
node scripts/check-plugin-teardown-shape.mjs --self-test :: exit 0
node scripts/check-tenant-audit-census.mjs :: exit 0
node scripts/check-tenant-audit-census.mjs --self-test :: exit 0
node scripts/ci/assert-turbo-remote-cache.mjs --self-test :: exit 0
node scripts/ci/plan-test-core-shards.mjs --self-test :: exit 0
node scripts/ci/prune-turbo-cache.mjs --self-test :: exit 0
node scripts/docs-audit/check-affected-docs.mjs :: exit 0
node scripts/docs-audit/check-drift-comment.mjs :: exit 0
node scripts/pm/release-rehearsal-clone.mjs --self-test :: exit 0
node scripts/release-pending-publish.mjs --self-test :: exit 0
pnpm --filter @objectstack/spec run check:duration-unit-keys :: exit 0
pnpm check:changeset-gate-self-tests :: exit 0
pnpm check:ci-filter-parity :: exit 0
pnpm check:closed-ceremony-text :: exit 0
pnpm check:cross-package-test-inputs :: exit 0
pnpm check:dispatcher-error-vocabulary :: exit 0
pnpm check:doc-authoring :: exit 0
pnpm check:driver-memory-census :: exit 0
pnpm check:dts-closure :: exit 0
pnpm check:dual-build-cjs-loads :: exit 3
pnpm check:engine-double-contract :: exit 0
pnpm check:error-status-conformance :: exit 0
pnpm check:gitlink-declared :: exit 0
pnpm check:issue-citations :: exit 0
pnpm check:keyed-text-bounds :: exit 0
pnpm check:lean-entry-closure :: exit 0
pnpm check:logger-receiver-detach :: exit 0
pnpm check:nul-bytes :: exit 0
pnpm check:objectql-double-limit :: exit 0
pnpm check:org-identifier :: exit 0
pnpm check:page-declaration-shape :: exit 0
pnpm check:platform-object-tenancy-census :: exit 0
pnpm check:pm-changeset-deadline-census :: exit 0
pnpm check:published-files :: exit 0
pnpm check:query-options-erasure :: exit 0
pnpm check:refd-timer-probe :: exit 0
pnpm check:registry-log-declared :: exit 0
pnpm check:rest-log-spy-declared :: exit 0
pnpm check:slot-lookup :: exit 0
pnpm check:sourcemap-no-sources-content :: exit 0
pnpm check:swallow-census-controls :: exit 0
pnpm check:system-context-census :: exit 0
pnpm check:test-source-alias :: exit 0
pnpm check:tier-file-adoption :: exit 0
pnpm check:type-check-coverage :: exit 0
pnpm check:type-check-debt :: exit 0
pnpm check:undeclared-dep-imports :: exit 0
pnpm check:watch-hint-literal :: exit 0
pnpm check:where-matcher :: exit 0

Generated by Claude Code

The compile-time currency guard resolved a measure's currency from its
field without reading its aggregate, so a derived measure over an amount
and a count over an amount field (or a count declaring a currency) was
refused as a cross-currency combination. Resolve a currency only for an
amount aggregate, using the shared AMOUNT_AGGREGATES set the cube door
and the record-currency column already read.

The result column had the same blind spot in its fixed / tenant arm: a
count over a currency field carried that field's currency. Gate it on the
same set, so a count column is a plain number.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L9qibyimd4GfDwsijRiftT
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling labels Oct 31, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

2 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to list — not a clean bill of health. This check sees only pages that NAME a derived anchor: one that documents this change in prose, or enumerates it in an authoring dialect, names none and stays invisible to it on every run.

What this run could not see
  • the SDK route bridge reached 59 of 211 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 10 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json b3ec89f31bddefcf06825a8daa7a38510d4294a0 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from a160c926398cb1722088cb01066dbe677fdf31be — the merge of head 85afc7ba29d32e366451b6214f3a147dc00bf809 into base b3ec89f31bddefcf06825a8daa7a38510d4294a0, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin a160c926398cb1722088cb01066dbe677fdf31be && git checkout a160c926398cb1722088cb01066dbe677fdf31be
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin b3ec89f31bddefcf06825a8daa7a38510d4294a0 85afc7ba29d32e366451b6214f3a147dc00bf809 && git checkout -B drift-repro b3ec89f31bddefcf06825a8daa7a38510d4294a0 && git merge --no-ff 85afc7ba29d32e366451b6214f3a147dc00bf809

node scripts/docs-audit/affected-docs.mjs --json b3ec89f31bddefcf06825a8daa7a38510d4294a0

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:ui#3 · os-tesla · session_01L9qibz8HEj53Wx2uphTo2o

Contract review

Served-tier: TIER_DEFAULT
Head-sha: 85afc7ba29d32e366451b6214f3a147dc00bf809
Local-runs: none

Face: declaration only (Clause-②: yes (widening) on the PR body and changeset). The tree touches no packages/spec/src/** and no governed path, so no ④ is owed.

① Derived judgments

  • Guard 2 accepts more (the widening). measureCurrencyResolver.resolvedOf's non-derived branch now resolves a currency only when AMOUNT_AGGREGATES (sum/avg/min/max, the one set already exported from cube-record-currency-door.ts, imported rather than copied) holds m.aggregate. So a count, with a field or a declared currency, resolves to none and never becomes a monetaryOperands entry. Newly accepted: ratio/sum/difference of an amount and such a count. This is correct per ADR-0144 open item 12 ("a count operand is not compared") and ruling 6486427200 on [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. Before this change the code met that rule only for a field-less count.
  • What stays refused: two different fixed codes, and a record-currency amount against a fixed one (the CONTROL tests sit beside the counts). Guard 1 (a measure currency that contradicts its field's fixed code) reads the field, not resolvedOf, so a count with field: amt_eur, currency: USD is still refused (pinned). Nothing that was accepted becomes refused.
  • Served result column: enrichResultColumns, in the fixed/tenant arm, now treats a currency-typed field as money only under an amount aggregate. A count over a fixed or tenant amount loses fields[].currency (EUR / CNY → none). This changes a value, not the shape (the key was already optional), and it fixes the defect where a count tile printed as money. It is pinned on both strategies and the draft-preview path, with sum controls that keep EUR / CNY.
  • Noted, not a defect: a count that declares its own currency still gets that code on its column (mc.currency keeps it monetary), while guard 2 no longer compares it. An author-declared code is an explicit statement about the column, the changeset says so, and no ruling covers counts with a declared currency. Left as is.

② Semver level

@objectstack/service-analytics patch, which matches the changeset. It is a bug fix that shrinks a refusal and corrects a served value, with no new key, no removed export and no spec change.

③ Boundary flags

  • The dev reports check:dual-build-cjs-loads as NOT MEASURED locally (exit 3, needs a full build). CI on head 85afc7ba has every check-run success or skipped (35 / 13, 0 failure). The full-build gate is backstopped by the build legs and the merge-queue run.
  • The out-of-scope enrichResultColumns defect was folded in on the seat's ruling, with its own tests and ablation, and the PR body lists it as a separate item. No open questions remain.

Implemented-by: claude/project-thread-5zv5e6
Reviewed-by: session_01L9qibz8HEj53Wx2uphTo2o

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:ui#3 · os-tesla · session_01L9qibz8HEj53Wx2uphTo2o

维护者速读

席位意见:两处都直接复用已有的 AMOUNT_AGGREGATES,没有新写一份规则;契约复审(声明面)已通过,可以直接落地。

席位核对

ACCEPT · seat domain:ui#3 · head 85afc7ba · 2026-10-31T06:47Z

  1. Shape: Fixes #28194, draft, base main, 4 files, +107/−7. The guard in measure-currency-coherence.ts and the result column in analytics-service.ts are both gated on the existing AMOUNT_AGGREGATES export (imported, not copied). There are 8 new tests in record-currency-analytics.test.ts and a patch changeset. This is a cross-lane domain:server card, dispatched before the maintainer's 06:04Z 「已经在处理的可以继续」 (claim 6539703783).
  2. Premise measured: the card thread reproduced the refusal on main 69c39b16 with the real engine on SQLite: ratio(usd_total, priced_invoices) was refused DATASET_INVALID. It also measured the same-class column defect (a count over a fixed-EUR amount carried "EUR"), which the seat ruled folded in.
  3. Gates / check-runs: on head 85afc7ba every latest check-run is success or skipped (35 / 13, 0 failure) and mergeable_state is clean. Dev: 6 of 8 new tests red on the unfixed source, 60/60 green with the fix, both gates ablated (3 red each, restored). The package's 200 test files are green, typecheck exits 0, and 61 of 62 derived gates exit 0. check:dual-build-cjs-loads was NOT MEASURED locally (needs a full build) and is backstopped by CI (os-dev-report 6540066529).
  4. Accepted deviations: the enrichResultColumns fold-in, on the seat's ruling. Contract review: seat record 6540244412, Served-tier: TIER_DEFAULT, PASS on this head.
  5. Out of scope: a count that declares its own currency keeps that code on its column; this is noted in the review and stays as is. The changeset's prose matches the diff sentence for sentence (the guard paragraph and the result-column paragraph). Skills-docs: none — Metadata-protocol: none.
  6. Landing: check-governed-merges --pr 28360 says NOT governed, 114 changed lines ≤ 3000. The seat readies the PR and arms auto-merge under 「按 pm-dispatch 审过的pr你可以自主落地」.

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 31, 2026 06:48
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 31, 2026
Merged via the queue into main with commit d35db2c Oct 31, 2026
49 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/project-thread-5zv5e6 branch October 31, 2026 07:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

1 participant