Repository navigation
Commit 4df174b
fix(app-showcase): showcase money reads one way — roll-up scale, USD project amounts, one total-budget format (#25543)
<!-- claude-projects-attribution: {"github_login":"os-sales"} -->
_Requested by **Sales** · [project
thread](https://claude.ai/code/project/chan_01D5bWFY3rFzgJcytD5gD4Uu?thread=cmsg_01D5bWFY3rFzgJcytD5gD4UuACdPWFJTLDcTrByFCHLzqb)_
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.com/claude-code)
https://claude.ai/code/session_016g7nYP9wdNx1GDa5XDr7Uy
Co-authored-by: Claude <noreply@anthropic.com>1 parent e5eab06 commit 4df174b
5 files changed
Lines changed: 168 additions & 15 deletions
File tree
- examples/app-showcase
- src
- data/objects
- ui
- datasets
- pages
- test
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
69 | 69 | | |
70 | 70 | | |
71 | 71 | | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
72 | 77 | | |
73 | 78 | | |
74 | 79 | | |
75 | 80 | | |
| 81 | + | |
76 | 82 | | |
77 | 83 | | |
78 | 84 | | |
79 | 85 | | |
80 | 86 | | |
| 87 | + | |
81 | 88 | | |
82 | 89 | | |
83 | 90 | | |
| |||
88 | 95 | | |
89 | 96 | | |
90 | 97 | | |
| 98 | + | |
91 | 99 | | |
92 | 100 | | |
93 | 101 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
60 | 60 | | |
61 | 61 | | |
62 | 62 | | |
63 | | - | |
64 | | - | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
65 | 84 | | |
66 | 85 | | |
67 | 86 | | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
68 | 90 | | |
69 | 91 | | |
70 | 92 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
55 | 55 | | |
56 | 56 | | |
57 | 57 | | |
58 | | - | |
59 | | - | |
60 | | - | |
61 | | - | |
62 | | - | |
63 | | - | |
64 | | - | |
65 | | - | |
66 | | - | |
67 | | - | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
68 | 70 | | |
69 | 71 | | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
76 | 76 | | |
77 | 77 | | |
78 | 78 | | |
79 | | - | |
| 79 | + | |
80 | 80 | | |
81 | 81 | | |
82 | 82 | | |
83 | | - | |
| 83 | + | |
84 | 84 | | |
85 | 85 | | |
86 | 86 | | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
87 | 96 | | |
88 | 97 | | |
89 | 98 | | |
| |||
159 | 168 | | |
160 | 169 | | |
161 | 170 | | |
162 | | - | |
| 171 | + | |
163 | 172 | | |
164 | 173 | | |
165 | 174 | | |
| |||
| 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 | + | |
0 commit comments