Repository navigation
fix(fields): CurrencyField takes fraction digits from the currency, never the field-level precision (objectui#10276) - #10319
Conversation
…ever the field-level precision (objectui#10276)
The field-level `precision` is a total-digit count in `@objectstack/spec`
("Total digits (non-negative integer)"), so a DECIMAL(18,2) currency field
declared `precision: 18` rendered eighteen decimal places. The widget's one
width now comes from the resolved currency's ISO 4217 minor-unit count
(2 when no currency is known) and drives display, step and blur rounding.
Neither `precision` nor `scale` is read, per the rulings on
objectstack-ai/objectstack#19910 and objectstack-ai/objectstack#19629.
The objectui#4361 "authored precision wins" pins are rewritten on the same
inputs to assert the ruled rule.
Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BP8CMtACxTdLjqR6rhd33C
…red currency precision wins (objectui#10276) - `.changeset/10276-…`: the behaviour change, declared once. - `.changeset/9568-…` (pending): its closing paragraph described CurrencyField's `precision` read as live; it ships in the same release as this change, so it now states the ruled rule. - `PercentField`'s docblock pointed at the same read; now it points at the currency's own minor unit. - `content/docs/fields/currency.mdx` called `precision` "the decimal precision" and taught `precision: 2`; it now says decimal places follow the currency and `precision` is the total digit count. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BP8CMtACxTdLjqR6rhd33C
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
Contract reviewServed-tier: Rendered by an isolated review subagent spawned by the ① Derived judgments
② Semver level
③ Boundary flags
Seat note: last sync before ready
Implemented-by: VERDICT: PASS Generated by Claude Code |
…rrency-fraction-digits-not-precision
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
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#4seat, sessionsession_01BP8CMtACxTdLjqR6rhd33C. Draft until the seat lands it.What changed
CurrencyFieldin@object-ui/fieldstook its one fraction-digit width fromcurrencyField?.precision ?? (currency ? currencyFractionDigits(currency) : 2). In@objectstack/specthe field-levelprecisionis "Total digits (non-negative integer)" — a DECIMAL(18,2) amount isprecision: 18, scale: 2— so a currency field declaringprecision: 18rendered eighteen decimal places, offeredstep="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 fallbackformatCurrencyuses for the grid cell). Neitherprecisionnorscaleis read. The one derived value still drives display,stepand blur rounding, so the JPY coupling objectui#4361 pinned holds: whole yen,step="1", blur rounds1234.56to1235.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, record5791803339) that takesscaleoff thecurrencytype.Triage comment
5817807194says fraction digits come "fromscale(when authored) or the currency's ISO 4217 digits", with the pin "onlyscale: 0⇒ 0 decimals". This PR does not readscale, and pinsscale: 0on a USD field as inert ($1,234.50), because:scaleoff the currency type and orders "noscaleread on currency" for the sibling footer face;CurrencyFieldMetadatain@object-ui/typesdoes not declarescale— measured:{ type: 'currency', name: 'x', label: 'X', scale: 2 }typed asCurrencyFieldMetadatafails to compile against the built@object-ui/typesdist with TS2353;scalebefore 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 (fix(spec,objectql)!: retirescalefrom the currency field type — refused at parse, no longer enforced on writes objectstack#19909, open).The other two triage pins hold exactly as written:
precision: 18, scale: 2⇒ 2 decimals;precision: 18alone ⇒ 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 thescalereading instead, the change is the one derivation line plus the twoscalepins.Measured, per the dispatch's mechanism assumptions
stepand blur rounding. The JPY step and blur pins stay green; the pin "USD is still 0.01, authored or derived", whose second half assertedprecision: 2on JPY gives a 0.01 step, is rewritten (JPY withprecision: 2now steps by 1; USD withprecision: 18steps by 0.01).precisionwins"; it states the ruled rule, cites both rulings, and records why neitherprecision,scalenorcurrencyConfig.precisionis read.CurrencyCellRenderercallsformatCurrency(num, currency, locale), whose width isisWhole ? 0 : currency ? currencyFractionDigits(currency) : 2; it never reads the field. A grep of non-testpackages/*/srcandapps/*/srcforprecisionreads finds no other currency path that turns the field-levelprecisioninto fraction digits. The grid summary footer andObjectMetricWidgetreadscale ?? 0on a currency — that is objectui#10221's surface, not folded.@objectstack/spec(17.4.0) declaresscaleonFieldSchemafor every type and still accepts it on a currency (FieldSchema.safeParse({ name: 'amount', type: 'currency', scale: 0 })succeeds);@object-ui/typesdeclaresprecisionbut notscaleonCurrencyFieldMetadata. 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:
.changeset/9568-percent-widget-reads-scale.md— pending (not yet inpackages/fields/CHANGELOG.md); its closing paragraph described CurrencyField'sprecisionread as live under objectui#4361's "authoredprecisionwins", 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.packages/fields/src/widgets/PercentField.tsx— one docblock sentence ("objectui#4361 ruled an authoredprecisionwins 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.content/docs/fields/currency.mdx— the published page calledprecision"the decimal precision" and taughtprecision: 2; this change makes that false. It now says decimal places follow the currency andprecisionis the total digit count, and the example dropsprecision: 2.Changeset
.changeset/10276-currency-fraction-digits-not-precision.md,'@object-ui/fields': minor.patch.minorbecause the AGENTS.md version rule marks objectui's own breaking changesminor, this reverses a rule the published CHANGELOG states ("an explicitly authoredprecisionstill wins"), and both precedents for the same reading on the percent faces (objectui#9295, objectui#9568) declaredminor. In the fixed group the two bump the same while otherminorchangesets are pending; the seat may flip it.Tests and gates — all read at
8fb056145pnpm 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 closurepnpm --filter "@object-ui/fields^..." build. The test file is in the test program:tsc -p tsconfig.test.json --listFilesOnlylistsCurrencyField.minorUnits.test.tsxonce among 179 test files.eslint .inpackages/fields(the unit CI'sturbo run lintruns) → 264 files, 0 errors; the touched files' warnings are allno-explicit-anyon lines this diff did not add (zero added lines containany).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-overwritereports 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 --testover all six paths → NOT GOVERNED.check-doc-snippet-typesas a whole (it builds a 35-package closure; CI owns it). Narrowed instead: the editedcurrency.mdxsnippet compiles--strictagainst the built@object-ui/typesdist, 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 restoredCurrencyField.tsxto the base8b1f06619, confirmed the mutation on disk (old readcurrencyField?.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 sixprecisionpins, the no-currencyprecisionpin, the step pin and theprecision: 18blur pin. The twoscalepins stay green on base, as expected: base did not readscaleeither. Restore:git checkout HEAD -- PATH, then the working blob equals theHEADblob (879bb362…) andgit diff HEADis empty.Acceptance notes
precisionthat contradicts the currency's ISO 4217 digits? objectstack#7918 field-levelprecisionanchor inFieldSchema'ssuperRefinestill reads a currency field'sprecisionas 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: 18contradicts it", remedy "Declareprecision: 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.ObjectForm's unregistered-widget fallback derives a currency field'sstepfromscale; after the spec refusesscaleon currency it resolves to'any'. Not reached by the registeredCurrencyField.$1,234.00where the grid cell shows$1,234(the cell's wholeness switch).Generated by Claude Code