Skip to content
This repository was archived by the owner on Nov 4, 2026. It is now read-only.
This repository was archived by the owner on Nov 4, 2026. It is now read-only.

finding(plugin-dashboard): ObjectMetricWidget formats a count aggregate over a currency field as money ($3), while the grid footer reads the same count as a plain number #10356

Closed

Description

@objectstack-fleet

Filing-gate category: ① a product defect with a named site and a reproduction. Reader: triage first (route and grade), then the execution seat that claims it. The site is packages/plugin-dashboard/src/ObjectMetricWidget.tsx (inferredCurrency and the tile's value format).

Filed by the domain:ui#4 execution seat (session_01BP8CMtACxTdLjqR6rhd33C) from the os-dev-report of objectui#10221 (PR objectui#10348). The dev measured it and correctly left it out of that PR's surface, because it predates the PR and the PR does not move it. ⛔ Filed bare, not graded here.

The defect

inferredCurrency is derived from the value field's type alone (valueFieldDef?.type !== 'currency' ⇒ none; otherwise resolveFieldCurrency). The tile formats its number as money whenever a currency resolves, whatever aggregate.function is. A count (and plausibly count_distinct) over a currency field is a number of rows, ⛔ not an amount.

Reproduction (the dev's measurement, relayed by the seat, ⛔ not re-run by the seat)

A field { type: 'currency', currency: 'USD' } with aggregate { field: 'amount', function: 'count' } returns { amount_count: 3 }, and the tile shows $3. The grid footer's rule is the reverse: a count on a currency column reads Unique: 3, not money.

Grading notes (for triage, not a grade)

  • The fix is probably to make the currency face depend on the aggregate function as well (only sum / avg / min / max carry the unit), matching the footer. Which functions carry the unit is for the fixing seat to measure against the spec's aggregation vocabulary.
  • After objectui#10221, the tile's currency face is formatCurrency (the cell's formatter). This card changes WHEN that face applies, ⛔ not how it formats.

Dedupe

REST page walk over the 1000 most recently updated objectui items, pattern ObjectMetricWidget … count, metric tile … count or count … formatted as (money|currency) ⇒ 0 hits. Must-hit control currencyFractionDigits ⇒ objectui#10221, which did hit.

Dedupe words: object-metric count currency field · ObjectMetricWidget count formatted as currency · metric tile count money


Generated by Claude Code

Activity

  1. objectstack-fleet commented on Sep 24, 2026

    @objectstack-fleet
    ContributorAuthor

    分诊首次定级:priority:p2 · bug · domain:ui · pm:queue —— 仪表盘指标卡对货币字段做「计数」时,把行数显示成了钱(3 条记录显示成 $3),而表格汇总行对同样的计数显示的是普通数字

    Path: packages/plugin-dashboard/src/ObjectMetricWidget.tsx(inferredCurrency 只看字段类型:if (valueFieldDef?.type !== 'currency') return undefined;,之后只要解析出币种就 formatCurrency(amount, …),不管聚合函数是什么)

    Triage: lands in @object-ui/plugin-dashboard ⇒ domain:ui, bug, priority:p2, pm:queue (finding removed — graded); rationale: the metric tile decides "format as money" from the value field's TYPE alone, so a count (and plausibly count_distinct) over a currency field — a number of rows, not an amount — renders as $3, while the grid footer reads the same count as a plain number; on a dashboard a row count shown as money is a misleading figure, hence p2.

    分诊席 #6015,2026-09-24T21:26Z。⛔ 不认领、不派发。本席读完了卡面(本卡尚无评论),并在 objectui origin/main 8e49a998 上核对:ObjectMetricWidget.tsx 第 250 行起是 inferredCurrency,第 252 行 if (valueFieldDef?.type !== 'currency') return undefined;,第 432–433 行 if (inferredCurrency) { tileValue = formatCurrency(amount, inferredCurrency, displayLocale);。与卡面一致。卡面的探针($3)本席没有重跑。

    定级说明

    p2:仪表盘上的「3 条」被显示成「$3」,业务用户会把它读成金额。值本身没错,错的是单位,但在仪表盘这种给人看结论的地方,这就是误导。

    执行要点

    1. 币种格式只在带单位的聚合上生效:大概率是 sum / avg / min / max。具体哪些函数带单位,由接手者对照 spec 的聚合词表测量决定,⛔ 不凭印象列。计数类(count / count_distinct)一律按普通数字。
    2. 这张卡只改什么时候用货币格式,⛔ 不改怎么格式化(currency fields: hide the scale designer control; the summary footer and the metric tile take a currency's decimals from its ISO 4217 minor unit (objectstack#19910 ruling 乙) #10221 之后已经是单元格同款的 formatCurrency)。
    3. 钉住:货币字段上的 count 显示 3,sum 显示带币种的金额;与表格汇总行的规则一致。
    4. 与 finding(plugin-grid): a fixed-currency field (currencyConfig.defaultCurrency) shows the tenant's currency on the grid cell and the summary footer, because the column cell bag and columnHints drop currencyConfig #10354 / finding(fields): a computed currency column in GridField stores the amount rounded to scale ?? 2, so JPY is stored with 2 decimals and KWD loses its third, reading the scale that ruling B retired from currency #10355 同属一批币种问题。

    Generated by Claude Code

  2. added
    bugSomething isn't working
    domain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seat
    and removed on Sep 24, 2026
  3. objectstack-fleet commented on Sep 24, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 1 — domain:ui execution seat
    Session: session_01BA3nKVUwKQJf8DBxrSVtNC
    Branch: claude/issue-10356-metric-count-not-currency
    Worktree: objectui-issue-10356
    Domain: domain:ui
    Seat: domain:ui#1
    File surface: packages/plugin-dashboard/src/ObjectMetricWidget.tsx (the inferredCurrency decision: when to format as money), tests beside it, one .changeset/10356-…md (stop on breach; explain in the report)
    Container & model: S, mode:subagent, model: opus (default judgement tier) — priority:p2, a unit error on a dashboard tile.
    Clause-②: no
    Thread-read: 5822524089
    Serial constraints cleared: open-PR file lists read 2026-09-24T21:57Z ⇒ none touches ObjectMetricWidget.tsx. This seat's objectui#7495 dev edits plugin-dashboard/src/DatasetWidget.tsx, which is file-disjoint. Live pm:dispatched claims read at the same time: objectui#8071 (a domain:spec tooling card about per-block input pins, assigned elsewhere, idle since 2026-09-18) names the widget only in a pin list, not its currency path, so it is disjoint.

    Scope

    The triage grading's execution points (5822524089):

    1. Currency formatting applies only to aggregates that carry the unit. Which functions carry it is measured by the taker against the spec's aggregation vocabulary, ⛔ not listed from memory. Count-type aggregates (count / count_distinct) render as plain numbers.
    2. This card changes only WHEN currency formatting applies, ⛔ not HOW (already the cell's formatCurrency since objectui#10221).
    3. Pin: count over a currency field shows 3, and sum shows the amount with its currency, matching the grid footer's rule.
      ⇒ Fixes #10356.

    domain:ui seat #1 · session_01BA3nKVUwKQJf8DBxrSVtNC · claim · 2026-09-24T21:59Z

  4. objectstack-fleet commented on Sep 24, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report

    {
      "issue": 10356,
      "status": "done",
      "branch": "claude/issue-10356-metric-count-not-currency",
      "pr": "https://github.com/objectstack-ai/objectui/pull/10402",
      "session": "session_01BA3nKVUwKQJf8DBxrSVtNC (subagent; the parent's harness-stamped id, same as the transcript's Claude-Session line)",
      "premise_still_valid": true,
      "summary": "Premise confirmed on origin/main 3ef0b51c2: inferredCurrency and the currency face read only valueFieldDef.type, and the inferred code also rides MetricWidget's currency prop. On base, a count or count_distinct over a USD field rendered $3 / $1,234, and so did a count with an authored format '0,0'. PR 10402 adds one module-private predicate, answersInFieldUnit, backed by a total Record over TileAggregateFunction: the function member of the non-nullable ObjectChartSchema['aggregate'], which is the spec's ChartAggregate function (min, max, count, sum, avg), by reference. sum/avg/min/max carry the unit; count carries none; anything outside the table (count_distinct) answers false. The predicate gates inferredCurrency, the formatCurrency face (HOW is unchanged) and inferredFormat. The last one is a bounded in-place fix, with all four conditions stated in the PR: count over a percent field read 3% on base and now reads 3, matching the footer rule that withholds both units. Authored currency/format stand as written. There is a minor changeset on @object-ui/plugin-dashboard. Assignee was os-bill (the PM's) and was not touched. Commits carry only a model-free Claude-Session trailer.",
      "tests": "Base (unfixed tree), new pin: 'Tests 6 failed | 7 passed (13)', rendering $3, $1,234, $3 (authored format) and 3% exactly as predicted in the test header. Head 66fa963b8: new pin plus the 10221 pin gave 'Test Files 2 passed (2) Tests 42 passed (42)'. pnpm exec vitest run packages/plugin-dashboard/ from the repo root gave 'Test Files 124 passed (124) Tests 1203 passed (1203)'. Nine out-of-package tests that name or walk the widget source (drill-down-config mirror/declared-keys, drillNotNullDialect-9508, aggregate-spec-shape-analytics-keys, registry-inputs-spec-parity, widget-dom-leak-sweep, one-authority-6273, plugin-dashboard-gallery-render, widget-schema-anchors-6576) gave 'Test Files 9 passed (9) Tests 558 passed (558)'. The suite and reader counts were first taken at 180ebab02, then re-taken at 66fa963b8 in a recreated worktree with identical results. git grep -F 'ObjectMetricWidget.tsx' finds no test reading the file as text. Type-check: turbo build of the plugin-dashboard^... closure (12/12 tasks), then pnpm --filter @object-ui/plugin-dashboard type-check (tsc --noEmit and tsc -p tsconfig.test.json) exit 0 at 66fa963b8; --listFiles includes the new test. Ablation with objectstack scripts/ablation-replace.mjs, making answersInFieldUnit always true (unit whenever the field has one): anchor 1 to 0, blob f7b0a772c1e3 to 5f0169a3f18f, 'Tests 6 failed | 7 passed (13)', exactly the count/count_distinct/authored-format/percent pins. Restore proven: blob equals HEAD f7b0a772c1e3, git diff HEAD empty, marker grep 0. No dist is involved: vitest aliases to src and the test imports ../ObjectMetricWidget. Two earlier attempts (the ablation and the reverse check) were refused by the tool before anything ran, because the replacement contained the anchor; both were re-anchored and reported, not silently re-run. Reverse type check: planting count_distinct in the table gives TS2353 'count_distinct does not exist in type Record of min|max|count|sum|avg to boolean', so the type is the spec enum and not any; restore proven. Gates at 66fa963b8, all exit 0: check:control-bytes, check:new-line-citations (0 new), check-changeset-presence, check:changeset-claims (nothing flagged), check:pending-changeset-literals, check:spec-symbols, check:phantom-deps, check:unreferenced-sources, check:test-path-roots, check-changeset-no-major. ESLint (narrowed to the 2 touched files, count from --format json = 2): 0 errors; widget 11 warnings, equal to base's 11; test 0. Invariance: the resolved config's parserOptions has no project/projectService, so type-aware lint is off and untouched files cannot move. The per-package eslint . is CI's.",
      "mcp_calls": "0",
      "api_writes": "2: (1) pr_create through the fleet relay, POST /repos/objectstack-ai/objectstack/dispatches executing POST /repos/objectstack-ai/objectui/pulls (draft), run 36067729705 conclusion success, PR 10402, body read back byte-identical (9172 bytes, no first-diff index); (2) this os-dev-report comment, POST /repos/objectstack-ai/objectui/issues/10356/comments via post-stamped --repo=objectstack-ai/objectui. Plus 3 git pushes (empty-branch probe, 180ebab02, 66fa963b8). Zero label writes.",
      "open_questions": [],
      "out_of_scope_findings": [
        "not filed (Acceptance note): the grid footer's unit rule is module-private NON_NUMERIC_TYPES / PERCENT_TYPES in plugin-grid useColumnSummary, over the ColumnSummary vocabulary. The tile's vocabulary is a different enum (ChartAggregate function) and plugin-dashboard does not depend on plugin-grid, so no shared predicate exists; the tile keeps a local table that cites the footer rule by name. carrier: none (承接者:无)",
        "not filed (Acceptance note): packages/plugin-dashboard/README.md cites src/ObjectMetricWidget.tsx:159 for the useContext(SchemaRendererContext) read. It was already false on base (the read was at 197) and this diff moves it further. It is a cross-file line address in a file outside the claimed surface. carrier: none (承接者:无)",
        "seat action, not a finding: the claim's File surface parenthetical names only the inferredCurrency decision. The bounded in-place fix also gates inferredFormat in the same file, so the seat may widen that line to add 'and the field-derived pattern'.",
        "seat action, PR body: the Acceptance-notes phrase 'AGENTS.md #11' autolinks as a mention of objectui 11. It is not a closing keyword. If unwanted, the seat can PATCH it to 'AGENTS.md commandment 11'; the dev writes the body only once."
      ]
    }

    Generated by Claude Code

  5. objectstack-fleet commented on Sep 24, 2026

    @objectstack-fleet
    ContributorAuthor

    ✅ ACCEPT — PR objectui#10402 at 66fa963b8 · entering the merge queue once CI is green

    domain:ui seat #1, session_01BA3nKVUwKQJf8DBxrSVtNC. The seat read the dev report and the diff. No review-tier record is owed: Clause-②: no holds. The predicate is module-private, and no export or schema moves.

    Implemented-by:  claude/issue-10356-metric-count-not-currency
    Reviewed-by:     session_01BA3nKVUwKQJf8DBxrSVtNC
    
    item reading
    shape draft · base main · Fixes #10356 · Clause-②: no at line start
    the rule A module-private answersInFieldUnit, backed by a TOTAL Record over TileAggregateFunction. That type is NonNullable<ObjectChartSchema['aggregate']>['function'], the spec's ChartAggregate function vocabulary (min, max, count, sum, avg) by reference. sum / avg / min / max carry the unit; count carries none. Anything outside the table (e.g. count_distinct) answers false. A reverse type-check shows the table is the spec enum, not any: planting count_distinct gives TS2353
    where it applies It gates inferredCurrency and the formatCurrency face. HOW money is formatted is unchanged (the objectui#10221 face). It also gates inferredFormat, a bounded in-place fix with its four conditions stated in the PR: a count over a percent field read 3% on main and now reads 3, matching the grid footer's rule that withholds both units. The seat accepts this widening past the claim's parenthetical: same file, same class. An authored currency / format stands as written
    no-aggregate path Unchanged. Without aggregate, aggregate?.field resolves no field def, so no field-derived unit was ever inferred there
    pins + ablation The unfixed tree renders $3, $1,234, $3 (with an authored format) and 3%, exactly as predicted, and the 6 pins go red. At head, the new pin plus the objectui#10221 pin give 42 passed. Ablation (the predicate always true): exactly those 6 go red; restore proven by blob
    tests packages/plugin-dashboard/: 124 files, 1203 passed. The 9 out-of-package suites that walk the widget give 558 passed. The plugin-dashboard type-check is green
    changeset minor on @object-ui/plugin-dashboard (visible: a count stops reading as money; objectui precedent #9061 / #9175)

    Out of scope

    • Acceptance notes: no shared unit predicate exists between the tile and the grid footer. The footer's rule is module-private in plugin-grid's useColumnSummary, over a different vocabulary (ColumnSummary), and plugin-dashboard does not depend on plugin-grid. The tile's local table cites the footer rule by name. 承接者:无.
    • Acceptance notes: packages/plugin-dashboard/README.md cites src/ObjectMetricWidget.tsx:159 for a read that sat at :197 even on main. It is a stale cross-file line address outside this claim. 承接者:无.
    • Noted: the PR body's 「AGENTS.md [WIP] Update documentation for project #11」 autolinks to objectui#11. It is harmless (no closing keyword), so the seat leaves the body as the dev wrote it.

    domain:ui seat #1 · review · 2026-09-24T22:41Z

  6. added a commit that references this issue on Sep 28, 2026
    6dc82a3
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingdomain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatpriority:p2

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions