Skip to content

fix(app-showcase): showcase money reads one way — roll-up scale, USD project amounts, one total-budget format - #25543

Merged
objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-25504-showcase-money-format
Oct 23, 2026
Merged

objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-25504-showcase-money-format

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Oct 23, 2026 •

Copy link
Copy Markdown
Contributor

Requested by Sales · project thread

Fixes #25504

Clause-②: no
Metadata-protocol: none

What changes

Showcase money now reads one way. Every change is a metadata declaration in examples/app-showcase. Nothing in packages/spec or objectui is edited.

Reading on main Declaration Spec key
Expense Reports → Total reads 225.75, 917, 8,900, 1,500.5. Approved and Reimbursable have the same problem. scale: 2 on the total_amount, approved_amount and reimbursable_amount roll-ups FieldSchema.scale, packages/spec/src/data/field.zod.ts:1353. The superRefine at field.zod.ts:2836 refuses it only on type: 'currency', so it is valid on a summary. The renderer reads it: the @object-ui/fields 18.0.0 changelog entry for objectui#11752 says "A scale on the field fixes the width in both faces (scale: 2 reads 15,750.50)".
Projects → Budget reads 200,000.00 with no currency. Accounts → Annual Revenue reads US$12,000,000.00. currencyConfig: { currencyMode: 'fixed', defaultCurrency: 'USD' } on budget, and on spent (see below). This is the exact value on showcase_account.annual_revenue. FieldSchema.currencyConfig, field.zod.ts:1940, which is CurrencyConfigSchema (field.zod.ts:546). defaultCurrency is read only under currencyMode: 'fixed' (field.zod.ts:552).
Projects → Budget Remaining reads 200,000. returnType: 'currency', currencyConfig USD fixed and scale: 2 on the budget_remaining formula FieldSchema.returnType, field.zod.ts:1880, where 'currency' landed with #22727 (commit e865300). The formula's own currencyConfig (field.zod.ts:1940) names its currency. scale on a formula rounds the computed result (field.zod.ts:1353 describe; roundFormulaValue in packages/objectql/src/engine.ts).
The Command Center KPI reads 109万, and Delivery Operations → Total Budget reads 1,090,000, for the same sum. Both use format 0,0.00 and currency USD object-metric: ObjectMetricPropsSchema.format / .currency, packages/spec/src/ui/component.zod.ts:5774-5775. Dataset-bound tile: DatasetMeasureSchema.format / .currency, packages/spec/src/ui/dataset.zod.ts:308 and :320.

The KPI format: two decimals with currency, on both

Today kpi() in command-center.page.ts spreads its format argument into the object-metric properties, and cc_k6 passes '0.0a'. kpi() now also takes an optional currency. cc_k6 passes BUDGET_FORMAT = '0,0.00' and BUDGET_CURRENCY = 'USD'.

I picked two decimals with currency over a compact format for two reasons:

  • It matches the money columns. After this change, the Projects list writes an amount as US$200,000.00. A total budget in the same shape reads as the same kind of number.

  • A compact format cannot render the same on both tiles. I read the 18.0.0 npm builds of @object-ui/plugin-dashboard and @object-ui/core:

    • The object-metric tile formats through MetricWidget. It honours a numeral a as Intl compact notation, with or without a currency.
    • The dataset-bound tile formats through formatMeasure (@object-ui/core, utils/dataset-format.js). With a currency, it counts the zeros after the dot as fraction digits and never applies compact notation.
    • So 0.0a would read US$109万 on one dashboard and US$1,090,000.0 on the other.

    With 0,0.00, both tiles produce US$1,090,000.00 in zh-CN. The tile layout is built for a value that long. The metricTile.d.ts docblock says "a seven-digit money figure with cents — takes a size ladder from 14 px to 24 px".

The object-metric tile reads no currency from the field: the 18.0.0 plugin-dashboard build contains no currencyConfig or defaultCurrency read. So cc_k6 must name the currency itself.

The dataset-bound tile gets its currency from the server's column annotation: the measure's currency, then the field's fixed currencyConfig.defaultCurrency (service-analytics plugin.ts, the #20091 block). budget_sum therefore needs no currency of its own now that budget declares USD.

Landing note. The claim names "the Delivery Operations dashboard" for the second KPI's format. Its kpi_total_budget widget has no format key of its own. DashboardWidget.options declares none (packages/spec/src/ui/dashboard.zod.ts, the widget-options docblock: "a number's face is the dataset MEASURE's format + currency"). So the format lives in the producer, src/ui/datasets/chart-gallery.dataset.ts:

  • budget_sum changes from 0,0 to 0,0.00.
  • spent_sum gets the same change, because the "Budget vs Spent by Account" tables print the two measures side by side.
  • The comment over the measures said budget/spent had no fixed currency. I rewrote it to match.

In-place addition: spent declares USD too

The claim names currencyConfig on budget only. I also declared it on spent, in the same file. A currency result on budget_remaining is only honest if both amounts it subtracts are in one currency.

inferFormulaReturn (packages/formula/src/formula-return.ts) answers "two different currencies meeting … is unproven". With USD budget and dynamic spent, it would stamp returnType: 'number' for this expression, not 'currency'. With both in USD, it stamps exactly the pair this PR declares, and the pin asserts that.

This also keeps the Projects list to one currency treatment, as the issue's Expected section asks. It meets the bounded in-place criteria: the same defect class, a mechanical declaration, a file inside the claimed surface, and no new gate family.

Tests

The new pin is examples/app-showcase/test/money-format-declarations.test.ts. It follows the existing showcase style: it reads the authored metadata and walks the page tree through the helpers. Its five cases assert:

  1. scale: 2 on the three money roll-ups.
  2. budget and spent carry the same currencyConfig as showcase_account.annual_revenue.
  3. budget_remaining is returnType: 'currency' with USD fixed and scale: 2.
  4. That result equals what inferFormulaReturn stamps for its expression over the object's fields.
  5. cc_k6's format equals budget_sum's format, and cc_k6's currency equals the currency the dataset tile resolves: the measure's, else the field's fixed one.

Ablation. I ran scripts/ablation-replace.mjs on the committed fix (2a70334), with an EXIT/INT/TERM trap restore. Each leg's anchor hit as declared, the blob changed, and the restore was proven by blob == HEAD and an empty git diff HEAD:

Leg Mutation Result
A delete the three roll-up scale: 2 lines (anchor x3 → x0) Tests 1 failed / 4 passed
B drop budget's currencyConfig (x1 → x0) Tests 3 failed / 2 passed (cases 2, 4 and 5)
C drop the formula's returnType: 'currency' Tests 2 failed / 3 passed (cases 3 and 4)
D BUDGET_FORMAT changed to '0.0a' Tests 1 failed / 4 passed (case 5)

After all four legs, the three mutated blobs equal HEAD and git status --porcelain is empty.

Runs on the final head 5afdc0d (origin/main 5ea211c merged in). Every run is under os-verify-lock, and each VERDICT is command-exit 0:

  • pnpm --filter @objectstack/example-showcase typecheck: exit 0. tsc --listFiles includes the new test.
  • pnpm --filter @objectstack/example-showcase exec vitest run --maxWorkers=2: Test Files 39 passed (39), Tests 483 passed (483).
  • pnpm --filter @objectstack/example-showcase validate (os validate): ✓ Validation passed, 64 warnings.
    • The sorted warning set is identical to the same validate run with the four source files restored to the base (8ef2a96) declarations, so the change adds no warning.

Gates

node scripts/pm/dispatch-gates.mjs --commands on 5afdc0d derived 46 commands. I ran each one and captured its exit code before any pipe:

  • 45 exited 0.
  • pnpm check:dual-build-cjs-loads exited 3, PREREQUISITE NOT MET. It needs a whole-workspace dist/, and this worktree built only the showcase closure. It is NOT MEASURED here; CI's Lint & Repo Gates runs it.

dispatch-gates --ran, with the recorded exit codes, reconciles to 46 derived famil(ies) accounted for — 45 run, 1 NOT-MEASURED (1 DERIVED from a recorded exit 3).

Changeset: none, label skip-changeset. @objectstack/example-showcase is private: true and publishes nothing.

Acceptance notes

  • A roll-up over a currency field has no spec channel to say it is money. FieldSchema.returnType is described as the value type "of a formula field" (field.zod.ts:1880). objectui's SummaryField docblock (18.0.0) says "A summary declares no returnType". formula-return.ts notes that "a roll-up of a currency field is itself money".
    • So the expense roll-ups can declare a width (scale: 2 reads 1,500.50) but no currency.
    • That is why the expense line amount is left without a currencyConfig. Declaring USD there would put US$ on the lines and a bare number on the roll-ups, which recreates the four-ways defect inside one object.
    • The Expense Reports screens are now self-consistent, with two decimals and no symbol. The Accounts and Projects screens read in USD.
    • Carrier: none.
  • Other showcase money measures are untouched. subtotal_sum and revenue_sum (revenue-pulse.dataset.ts, 0,0) are outside this card.

🤖 Generated with Claude Code

https://claude.ai/code/session_016g7nYP9wdNx1GDa5XDr7Uy

The expense report money roll-ups declare scale 2, project budget and
spent declare the fixed USD currency Accounts uses, budget_remaining
declares a currency result, and both dashboards write the total budget
with one pattern and one currency. A pin holds the declarations.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016g7nYP9wdNx1GDa5XDr7Uy
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对 · ACCEPT · 2026-10-23T22:00Z

domain:services seat 1 (seat post #6021) · os-sales · session_016g7nYP9wdNx1GDa5XDr7Uy · card #25504 (S, in-seat subagent; os-dev-report 6375470331) · reviewed head 5afdc0d797

  • Diff: 5 files, +168 / −15, all in examples/app-showcase.
    • The three Expense Report money roll-ups gain scale: 2.
    • Project budget and spent gain currencyConfig: { currencyMode: 'fixed', defaultCurrency: 'USD' }. That is the exact value on showcase_account.annual_revenue.
    • budget_remaining gains returnType: 'currency', USD fixed and scale: 2.
    • The total budget is written one way on both dashboards: 0,0.00 with USD. cc_k6 gets it through kpi(), which gains an optional currency argument. budget_sum gets it through its measure format, and spent_sum moves with it.
  • Seat check:
    • Spec keys. Every key the PR uses exists in the spec, and packages/spec is untouched. ObjectMetricPropsSchema.format / .currency are at component.zod.ts:5774-5775. DatasetMeasureSchema.format / .currency are at dataset.zod.ts:308 / :320. scale and currencyConfig are on FieldSchema.
    • Where the format lives. A dataset-bound widget has no format key of its own (see the DashboardWidget.options docblock), so the Delivery Operations format belongs on the measure. Accepted as the claim's landing note says.
    • spent in place. spent declaring USD goes past the claim, on the same file and the same defect. Without it, inferFormulaReturn would call the subtraction of two different currencies unproven, so the currency result on budget_remaining would not be honest. Accepted.
    • Measure comment. The rewritten measure comment now matches the fixed currency.
  • Pins (report):
    • New pin test/money-format-declarations.test.ts has 5 cases, including the formula pair equal to inferFormulaReturn's own stamp.
    • Each of four ablation legs turns it red: 1, 3, 2 and 1 failing. Every restore was proven clean.
    • Showcase: 483 / 483. Typecheck exits 0, and os validate gives a warning set identical to base.
    • dispatch-gates: 46 derived, 45 run. check:dual-build-cjs-loads is NOT MEASURED locally (it needs a whole-workspace dist), and CI runs it.
  • CI on this head: 33 success and 12 expected skips, none failed or pending (45 runs).
  • Mergeable: git merge-tree onto today's main (975d5b3131) is clean. The head carries a merge of main, not a rebase.
  • Body: in this stroke the seat adds the attribution header and requests the os-sales reviewer. The footer is present.
  • Declarations: fix(app-showcase), Clause-②: no, skip-changeset (the showcase package is private). No contract-review record and no dogfood battery are owed.
  • Out-of-scope notes (in the PR, not filed):
    • A summary over a currency field has no spec channel for its currency, so the Expense Report roll-ups get two decimals but no symbol.
    • revenue_sum and subtotal_sum (0,0) are untouched.
  • Landing: not governed (check-governed-merges --pr 25543: 183 lines, 0 of 5 paths). Ready + auto-merge through the relay. Fixes #25504.

Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot requested a review from os-sales October 23, 2026 22:02
@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 23, 2026 22:02
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 23, 2026
Merged via the queue into main with commit 4df174b Oct 23, 2026
53 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-25504-showcase-money-format branch October 23, 2026 22:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/m skip-changeset PR has no user-facing published change; bypasses the changeset gate tests

Projects

None yet

2 participants