You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit 31938f0
Browse filesBrowse the repository at this point in the historyBrowse files
fix(fields): CurrencyField takes fraction digits from the currency, never the field-level precision (objectui#10276) (#10319)
Fixes#10276
Clause-②: no — one widget stops reading a spec key with the wrong
meaning; no declared key, schema, export or accept set moves (carried
from the claim, comment 5818421884).
Dispatched implementation, `domain:ui#4` seat, session
`session_01BP8CMtACxTdLjqR6rhd33C`. Draft until the seat lands it.
## What changed
`CurrencyField` in `@object-ui/fields` took its one fraction-digit width
from `currencyField?.precision ?? (currency ?
currencyFractionDigits(currency) : 2)`. In `@objectstack/spec` the
field-level `precision` is "Total digits (non-negative integer)" — a
DECIMAL(18,2) amount is `precision: 18, scale: 2` — so a currency field
declaring `precision: 18` rendered eighteen decimal places, offered
`step="0.000000000000000001"` and rounded typed input to eighteen places
on blur.
The width is now `currency ? currencyFractionDigits(currency) : 2`: the
resolved currency's ISO 4217 minor unit, and the historical 2 when no
currency resolves (the same no-currency fallback `formatCurrency` uses
for the grid cell). Neither `precision` nor `scale` is read. The one
derived value still drives display, `step` and blur rounding, so the JPY
coupling objectui#4361 pinned holds: whole yen, `step="1"`, blur rounds
`1234.56` to `1235`.
Governing text: the maintainer ruling recorded on
objectstack-ai/objectstack#19910 (record `5805782503`, batch 218 item 2,
letter 乙), whose item 3 names this exact reading as wrong and whose
heading is "a currency's decimal places are the currency's, not a
setting"; and the ruling on objectstack-ai/objectstack#19629 (letter B,
record `5791803339`) that takes `scale` off the `currency` type.
## ⚠️ Where this departs from the triage notes — for the seat to confirm
Triage comment `5817807194` says fraction digits come "from `scale`
(when authored) or the currency's ISO 4217 digits", with the pin "only
`scale: 0` ⇒ 0 decimals". This PR does **not** read `scale`, and pins
`scale: 0` on a USD field as inert (`$1,234.50`), because:
- the 乙 ruling's heading puts the decimal places on the currency, not on
any setting; ruling B takes `scale` off the currency type and orders "no
`scale` read on currency" for the sibling footer face;
- `CurrencyFieldMetadata` in `@object-ui/types` does not declare `scale`
— measured: `{ type: 'currency', name: 'x', label: 'X', scale: 2 }`
typed as `CurrencyFieldMetadata` fails to compile against the built
`@object-ui/types` dist with TS2353;
- the widget did not read `scale` before this change either, so not
reading it moves nothing; reading it would add a reader of a key the
ruled spec change refuses at parse (objectstack-ai/objectstack#19909,
open).
The other two triage pins hold exactly as written: `precision: 18,
scale: 2` ⇒ 2 decimals; `precision: 18` alone ⇒ the currency's digits (2
for CNY and for USD). Zero decimals keep their grouping separators —
pinned on JPY (`¥1,234,567`). If the seat rules the `scale` reading
instead, the change is the one derivation line plus the two `scale`
pins.
## Measured, per the dispatch's mechanism assumptions
- **A1 held.** One derived value drives display, `step` and blur
rounding. The JPY step and blur pins stay green; the pin "USD is still
0.01, authored or derived", whose second half asserted `precision: 2` on
JPY gives a 0.01 step, is rewritten (JPY with `precision: 2` now steps
by 1; USD with `precision: 18` steps by 0.01).
- **A2 done.** The derivation comment no longer says "An AUTHORED
`precision` wins"; it states the ruled rule, cites both rulings, and
records why neither `precision`, `scale` nor `currencyConfig.precision`
is read.
- **A3 falsified — the cell is clean.** `CurrencyCellRenderer` calls
`formatCurrency(num, currency, locale)`, whose width is `isWhole ? 0 :
currency ? currencyFractionDigits(currency) : 2`; it never reads the
field. A grep of non-test `packages/*/src` and `apps/*/src` for
`precision` reads finds no other currency path that turns the
field-level `precision` into fraction digits. The grid summary footer
and `ObjectMetricWidget` read `scale ?? 0` on a currency — that is
objectui#10221's surface, not folded.
- **A4.** The pinned `@objectstack/spec` (17.4.0) declares `scale` on
`FieldSchema` for every type and still accepts it on a currency
(`FieldSchema.safeParse({ name: 'amount', type: 'currency', scale: 0 })`
succeeds); `@object-ui/types` declares `precision` but not `scale` on
`CurrencyFieldMetadata`. Nothing reads the undeclared key here, and the
gap agrees with ruling B's direction, so it is not reported as a
finding.
## File surface — three additions beyond the claim, declared
The claim named the widget, its tests and one changeset. This PR also
touches:
1. `.changeset/9568-percent-widget-reads-scale.md` — pending (not yet in
`packages/fields/CHANGELOG.md`); its closing paragraph described
CurrencyField's `precision` read as live under objectui#4361's "authored
`precision` wins", and it would ship in the same release as this change.
Corrected per the dispatch's own instruction; its declaration is
unchanged (`@object-ui/fields: minor`). The objectui#4361 text already
published in the CHANGELOG is historical record and is not touched.
2. `packages/fields/src/widgets/PercentField.tsx` — one docblock
sentence ("objectui#4361 ruled an authored `precision` wins over THAT")
that this change makes false; comment-only, same package, same gates,
and none of the 25 open PRs' file lists touches the file.
3. `content/docs/fields/currency.mdx` — the published page called
`precision` "the decimal precision" and taught `precision: 2`; this
change makes that false. It now says decimal places follow the currency
and `precision` is the total digit count, and the example drops
`precision: 2`.
## Changeset
`.changeset/10276-currency-fraction-digits-not-precision.md`,
`'@object-ui/fields': minor`. ⚠️ The dispatch named `patch`. `minor`
because the AGENTS.md version rule marks objectui's own breaking changes
`minor`, this reverses a rule the published CHANGELOG states ("an
explicitly authored `precision` still wins"), and both precedents for
the same reading on the percent faces (objectui#9295, objectui#9568)
declared `minor`. In the fixed group the two bump the same while other
`minor` changesets are pending; the seat may flip it.
## Tests and gates — all read at `8fb056145`
- `pnpm exec vitest run --maxWorkers=2 packages/fields/` (repo root,
under the shared verify lock) → `Test Files 178 passed | 1 skipped
(179)`, `Tests 3013 passed | 7 skipped (3020)`.
- `pnpm --filter @object-ui/fields type-check` (`tsc --noEmit && tsc -p
tsconfig.test.json`) → exit 0, after building the dependency closure
`pnpm --filter "@object-ui/fields^..." build`. The test file is in the
test program: `tsc -p tsconfig.test.json --listFilesOnly` lists
`CurrencyField.minorUnits.test.tsx` once among 179 test files.
- `eslint .` in `packages/fields` (the unit CI's `turbo run lint` runs)
→ 264 files, 0 errors; the touched files' warnings are all
`no-explicit-any` on lines this diff did not add (zero added lines
contain `any`).
- `check-changeset-presence` ✅ "3 source file(s) of 1 released
package(s) changed, and this change declares 1 changeset(s)";
`check-changeset-no-major` ✅; `check-changeset-fixed` ✅;
`check-changeset-claims --json` ✅ "No pending changeset names a file
this change touches"; `check-pending-changeset-literals` ✅;
`check-changeset-overwrite` reports the 9568 edit (report-only; case 2,
a deliberate prose correction, declaration unchanged).
- `check:new-line-citations` → `VERDICT new-cross-file-line-citations: 0
new citation(s)`; `check-control-bytes` ✅; `check-doc-links`,
`check-doc-fence-languages`, `check-doc-example-ids`,
`check-doc-component-types`, `check-doc-expression-carriage` → exit 0.
- `check-governed-queue-guard --test` over all six paths → NOT GOVERNED.
- NOT MEASURED: `check-doc-snippet-types` as a whole (it builds a
35-package closure; CI owns it). Narrowed instead: the edited
`currency.mdx` snippet compiles `--strict` against the built
`@object-ui/types` dist, with a control snippet in the same program that
fails (TS2353 on an undeclared key), so the types did resolve. Other
packages' tests are not owed: no export, type or spec contract moves.
## Reverse verification
Fix committed first (`3d6b06223`). A trap-guarded script restored
`CurrencyField.tsx` to the base `8b1f06619`, confirmed the mutation on
disk (old read `currencyField?.precision ??` count 1, new derivation
count 0), and ran the test file (it imports the widget by relative path,
so no build is involved): `Tests 9 failed | 15 passed (24)` — the nine
are the six `precision` pins, the no-currency `precision` pin, the step
pin and the `precision: 18` blur pin. The two `scale` pins stay green on
base, as expected: base did not read `scale` either. Restore: `git
checkout HEAD -- PATH`, then the working blob equals the `HEAD` blob
(`879bb362…`) and `git diff HEAD` is empty.
## Acceptance notes
- Out of scope, reported to the seat in the dev report, not filed here:
in objectstack's spec, the objectstack-ai/objectstack#7918 field-level
`precision` anchor in `FieldSchema`'s `superRefine` still reads a
currency field's `precision` as its display width. Measured on the
pinned 17.4.0: `{ type: 'currency', precision: 18, currencyConfig: {
currencyMode: 'fixed', defaultCurrency: 'USD' } }` is refused ("currency
USD has 2 fraction digits; `precision: 18` contradicts it", remedy
"Declare `precision: 2`"), against the key's own describe "Total digits
(non-negative integer)"; its comment's premise that objectui's
CurrencyField reads the key stops being true with this PR.
- Observation, no carrier: `ObjectForm`'s unregistered-widget fallback
derives a currency field's `step` from `scale`; after the spec refuses
`scale` on currency it resolves to `'any'`. Not reached by the
registered `CurrencyField`.
- Pre-existing and unchanged: the widget shows `$1,234.00` where the
grid cell shows `$1,234` (the cell's wholeness switch).
---
_Generated by [Claude
Code](https://claude.ai/code/session_01BP8CMtACxTdLjqR6rhd33C)_
---------
Co-authored-by: Claude <noreply@anthropic.com>
0 commit comments