Skip to content

fix(fields): CurrencyField takes fraction digits from the currency, never the field-level precision (objectui#10276) - #10319

Merged
objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/issue-10276-currency-fraction-digits-not-precision
Sep 24, 2026
Merged

objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/issue-10276-currency-fraction-digits-not-precision

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

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 (fix(spec,objectql)!: retire scale from 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: 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 Contract question: should publish-time validation reject a declared currency precision that contradicts the currency's ISO 4217 digits? 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

…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
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 329 chunks) 3039.1 KB 3104.5 KB
Main entry chunk (gzip) 147.8 KB 350 KB
Entry file index-qJsGXzS4.js —
Status PASS —

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

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 16.68KB 6.20KB
app-shell (runtime-config.js) 20.68KB 7.36KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.06KB 3.86KB
auth (ActiveOrganizationStorage.js) 25.05KB 9.16KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.18KB 10.59KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.15KB 5.39KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.65KB 2.22KB
auth (SocialSignInButtons.js) 9.61KB 3.89KB
auth (UserMenu.js) 3.41KB 1.23KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 40.21KB 10.80KB
auth (createAuthenticatedFetch.js) 8.46KB 3.43KB
auth (index.js) 3.19KB 1.44KB
auth (invitation-status.js) 1.22KB 0.70KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.30KB 1.02KB
auth (useWorkspaceAdminStatus.js) 11.08KB 4.58KB
collaboration (CommentThread.js) 26.08KB 7.56KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.49KB 2.64KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.05KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 541.66KB 129.51KB
core (index.js) 8.94KB 3.59KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 222.47KB 61.83KB
fields (index.js) 253.39KB 64.00KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 1.22KB 0.64KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 8.87KB 3.64KB
i18n (index.js) 5.22KB 2.26KB
i18n (pickLocalized.js) 9.86KB 3.95KB
i18n (provider.js) 33.36KB 10.88KB
i18n (useDisplayLocale.js) 2.85KB 1.45KB
i18n (useObjectLabel.js) 34.34KB 9.17KB
i18n (useSafeTranslation.js) 5.60KB 2.33KB
layout (index.js) 39.15KB 11.05KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.75KB
mobile (index.js) 1.99KB 0.87KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 6.62KB 2.45KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useSpecGesture.js) 5.52KB 2.10KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 13.52KB 4.88KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.24KB 2.16KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 8.39KB 3.10KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 4.83KB 2.27KB
plugin-ai (index.js) 14.81KB 3.63KB
plugin-calendar (index.js) 51.44KB 14.62KB
plugin-charts (index.js) 71.82KB 20.13KB
plugin-chatbot (index.js) 198.27KB 47.18KB
plugin-dashboard (index.js) 133.00KB 35.19KB
plugin-designer (index.js) 215.98KB 44.34KB
plugin-detail (index.js) 258.85KB 67.53KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 144.77KB 37.17KB
plugin-gantt (index.js) 167.99KB 41.37KB
plugin-grid (index.js) 215.46KB 58.88KB
plugin-kanban (index.js) 48.83KB 15.21KB
plugin-list (index.js) 113.90KB 28.11KB
plugin-map (index.js) 21.74KB 7.07KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 43.55KB 11.99KB
plugin-timeline (index.js) 30.68KB 8.95KB
plugin-tree (index.js) 10.56KB 3.71KB
plugin-view (index.js) 85.18KB 21.05KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.66KB 3.50KB
providers (index.js) 0.45KB 0.23KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 4.47KB 1.63KB
react (SchemaRenderer.js) 110.46KB 36.33KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.63KB 2.18KB
react (schema-input.js) 4.25KB 2.04KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (body-dialect.js) 4.78KB 2.09KB
sdui-parser (codegen.js) 6.58KB 2.74KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 5.78KB 2.56KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (kanban-quick-add.js) 3.89KB 1.87KB
sdui-parser (parse.js) 25.28KB 7.80KB
sdui-parser (provenance.js) 3.66KB 1.82KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 18.27KB 6.20KB
types (ai.js) 4.11KB 2.06KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 1.00KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 2.93KB 1.49KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (data-display.js) 3.75KB 1.85KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.85KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (expression.js) 0.20KB 0.18KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (http-inflight.js) 8.87KB 3.73KB
types (http-retry.js) 4.32KB 2.02KB
types (icon-key-migration.js) 4.26KB 1.63KB
types (index.js) 4.74KB 2.25KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 5.00KB 2.39KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 0.20KB 0.18KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (select-option.js) 0.20KB 0.19KB
types (spec-report.js) 5.05KB 1.93KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (strict-authoring-face.js) 17.15KB 6.32KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.11KB 3.32KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 8fb0561451aef929a4d7ad30793701837040abb5

Rendered by an isolated review subagent spawned by the domain:ui#4 seat; its served tier was checked against its transcript stamps (111 of 111 at the review tier). Adopted by this seat.

① Derived judgments

  • Rulings beat the triage note — confirmed from the texts. Ruling 乙 (5805782503) is headed verbatim "a currency's decimal places are the currency's, not a setting"; its item 1 says "the currency's ISO 4217 minor unit decides its display" and its item 3 names "objectui's CurrencyField reads the field-level precision (a digit count) as decimal places" as the wrong reading. Ruling B (5791803339, letter unchanged by correction 5805294161) item 1 REFUSES scale on type: 'currency' at parse and item 3 orders the sibling footer face "⛔ no scale read on currency". Triage 5817807194 item 1 ("小数位数只来自 scale(作者写了时)…") and the claim 5818421884 ("fraction digits from scale or the currency's ISO 4217 digits") would add a scale reader to a key the ruled spec change retires from this type. The PR's derivation currency ? currencyFractionDigits(currency) : 2 reads neither precision nor scale — this is what 乙 prescribes. CurrencyFieldMetadata (packages/types/src/field-types.ts:433-439) declares currency, precision, min, max and no scale, as the dev says. The departure is declared in the PR body under a ⚠️ heading for the seat; the triage pin "只写 scale: 0 ⇒ 0 位小数" is therefore deliberately NOT met, and scale: 0 on USD is pinned inert ($1,234.50).
  • Behaviour before → after (base derivation currencyField?.precision ?? (currency ? currencyFractionDigits(currency) : 2); head currency ? currencyFractionDigits(currency) : 2; one width feeds display, step={Math.pow(10, -w).toFixed(w)} and onChange(parseFloat(val.toFixed(w))); values confirmed with a tiny read-only node Intl probe run from the scratch directory, en-US):
    • precision: 18 + USD: display $1,234.500000000000000000 → $1,234.50; step 0.000000000000000001 → 0.01; blur of 1234.567 emitted 1234.567 → 1234.57. Ruled: yes (USD has 2).
    • precision: 0 + USD: $1,235 → $1,234.50; step 1 → 0.01; blur rounds to cents instead of whole dollars. Ruled: yes.
    • precision: 2 + JPY: ¥1,234.50 → ¥1,235; step 0.01 → 1; blur 1234.56 → 1235. Ruled: yes (JPY has 0). JPY coupling from objectui#4361 (display, step, blur on one width) is kept — pins "a JPY field offers a whole-unit spinner step" and "blur rounds a typed amount to the currency's own unit" are unchanged.
    • no precision + CNY / JPY / no currency: CN¥1,234.50 / ¥1,235 / 1,234.50 — byte-identical before and after (derivation and old literal agree). ¥1,234,567 keeps grouping (shouldGroupDisplayNumber returns true whenever a currency is set). Ruled: yes; the no-currency 2 is the same fallback formatCurrency uses.
    • scale: 0 + USD / scale: 2 + JPY: $1,234.50 / ¥1,235 before and after — base never read scale either (git log -S'.scale' on the file: no commit ever). Ruled: yes (B item 3, 乙 item 1).
  • No other @object-ui/* currency path turns field-level precision into fraction digits. git grep of non-test packages/*/src + apps/*/src at the head: the remaining precision reads are metadata pass-throughs (plugin-grid/ObjectGrid.tsx, plugin-detail/RecordDetailPanel.tsx, RelatedList.tsx, plugin-form/sectionFields.ts, plugin-list/ObjectGallery.tsx), the designer inspector (app-shell/.../ObjectFieldInspector.tsx), and the percent formatter. CurrencyCellRenderer (packages/fields/src/index.tsx:789) calls formatCurrency(num, currency, locale), whose width is isWhole ? 0 : currency ? currencyFractionDigits(currency) : 2 — never the field. Confirmed as the dev says: useColumnSummary.ts:343 and ObjectMetricWidget.tsx:229 read scale ?? 0 on a currency (objectui#10221's surface, not folded). Observation, not this card's finding: packages/fields/src/widgets/GridField.tsx:322 reads c.scale ?? (c.type === 'currency' ? 2 : undefined) for computed line-item columns — a scale/literal-2 read on a currency column, not a precision read.
  • Three extra-surface edits — each corrects a statement THIS change made false, nothing more. (a) .changeset/9568-percent-widget-reads-scale.md (pending: packages/fields/CHANGELOG.md has no objectui#9568 entry): diff is −6/+5 confined to the last paragraph, which said "CurrencyField's own read of precision is untouched … objectui#4361 ruled an authored precision wins over that"; declaration '@object-ui/fields': minor unchanged. (b) PercentField.tsx −5/+3 inside one docblock; removed text "objectui#4361 ruled an authored precision wins over THAT"; comment only. (c) content/docs/fields/currency.mdx: "the decimal precision" → "the total-digit precision", example drops precision: 2, one new paragraph stating the ruled rule. A grep of content/ and the field/plugin packages finds no other text still teaching currency precision as decimal places; the published objectui#4361 CHANGELOG entry is untouched (correct — historical record). No open PR (39 listed) touches any of the six files.
  • Every sentence of the new changeset checks out against the code. Spec describe "Total digits (non-negative integer)" is field.zod.ts:1211 on objectstack origin/main (the same describe is what the pinned release carries; the widget's own formatDisplayNumber path is unchanged); the step string 0.000000000000000001 and the eighteen-place blur follow from the base code; the cell renderer never reads precision; "two for USD or CNY, none for JPY, three for KWD" matches Intl; "it never did" (read scale) holds; the before/after examples match the base pins; and "the 'an explicitly authored precision still wins' rule objectui#4361 published" is literally in packages/fields/CHANGELOG.md:982 ("On CurrencyField, an explicitly authored precision still wins"). The corrected 9568 paragraph ("since objectui#10276 that widget reads neither precision nor scale") is true at the head. Note for the seat, not a falsity in objectui: the migration sentence "Keep precision only as the total digit count of the stored decimal" collides with the spec's still-live finding(types): ten z.lazy exports in the zod node face rebuild their schema on every getter call, so the recursion point cannot be compared by identity #7918 anchor, which refuses exactly that shape under currencyMode: 'fixed' (see ④). Pre-existing and untouched: the 9568 changeset's earlier quote of scale as "Decimal places (non-negative integer)" no longer matches the current describe "Decimal places (integer 0-100)…" — another card's text, not this change's making.
  • Rewritten comment and currency.mdx are true against 乙 and field.zod.ts. The comment cites 乙 (batch Merge plugin-calendar-view into plugin-calendar #218 item 2) and B, states the ISO 4217 minor-unit rule, quotes the spec describe correctly, says scale is "refused at parse once the spec ships it" (accurate: objectstack PR #19909 is open, no scale-on-currency refusal exists in FieldSchema's superRefine on main), and says currencyConfig.precision is "read by nothing" (乙's governing text: "no reader in packages/*/src"). The mdx paragraph states the same rule and the DECIMAL(18,2) example.
  • Pins are real and can fail. 24 it( blocks at the head (16 at base), imported by relative path from the widget. The nine the report names red-on-base assert values the base derivation cannot produce ($1,234.50 for USD precision: 18/0, ¥1,235 for JPY precision: 2/3, CN¥1,234.50 for CNY precision: 18, 1,234.50 with no currency and precision: 18, step 1 for JPY precision: 2, blur 1234.57 for USD precision: 18); the two scale pins are green on both, as stated. The objectui#4361 pins are rewritten on the same inputs, not deleted, each with a "Was …" note; the base pin "CONTROL: USD is still 0.01, authored or derived" (whose second half asserted JPY precision: 2 ⇒ 0.01) is the one rewritten to "the step follows the currency whatever precision declares". Not re-run here (gate family); derived from code plus the node probe.

② Semver level

minor is right. objectui AGENTS.md (版本号策略, line 261 at the head): 「推论:changeset 里不要声明 major —— fixed 组任一 major 都会把全组推上去、脱离 objectstack 的节奏(…)。objectui 自身的破坏性变更也标 minor(在正文里写清 breaking 语义即可);唯一例外是跟随 objectstack 跨 major 的那一次同步升级。」 (the parenthetical release example is elided) This change reverses a rule the published CHANGELOG states ("an explicitly authored precision still wins") and moves rendered output, step and blur rounding for every currency field whose declared precision differs from its currency's digits — a breaking behaviour change on a published contract, so minor (with the Behaviour change paragraph in the body, as the rule asks), not the dispatch's patch. Precedents on the same reading for the percent faces (.changeset/9295-percent-surfaces-read-scale.md, .changeset/9568-percent-widget-reads-scale.md) both declare '@object-ui/fields': minor. Not major. The Changeset Bump Policy, Changeset Declaration, Changeset Fixed Group Check and Changeset Claim Re-read check-runs are green on the head.

③ Boundary flags

Seat note: last sync before ready

Implemented-by: claude/issue-10276-currency-fraction-digits-not-precision
Reviewed-by: session_01BP8CMtACxTdLjqR6rhd33C

VERDICT: PASS


Generated by Claude Code

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 329 chunks) 3039.1 KB 3104.5 KB
Main entry chunk (gzip) 147.8 KB 350 KB
Entry file index-DIi_sJhC.js —
Status PASS —

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

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 16.68KB 6.20KB
app-shell (runtime-config.js) 20.68KB 7.36KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.06KB 3.86KB
auth (ActiveOrganizationStorage.js) 25.05KB 9.16KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.18KB 10.59KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.15KB 5.39KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.65KB 2.22KB
auth (SocialSignInButtons.js) 9.61KB 3.89KB
auth (UserMenu.js) 3.41KB 1.23KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 40.21KB 10.80KB
auth (createAuthenticatedFetch.js) 8.46KB 3.43KB
auth (index.js) 3.19KB 1.44KB
auth (invitation-status.js) 1.22KB 0.70KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.30KB 1.02KB
auth (useWorkspaceAdminStatus.js) 11.08KB 4.58KB
collaboration (CommentThread.js) 26.08KB 7.56KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.49KB 2.64KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.05KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 541.66KB 129.51KB
core (index.js) 8.94KB 3.59KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 222.47KB 61.83KB
fields (index.js) 253.39KB 64.00KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 1.22KB 0.64KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 8.87KB 3.64KB
i18n (index.js) 5.22KB 2.26KB
i18n (pickLocalized.js) 9.86KB 3.95KB
i18n (provider.js) 33.36KB 10.88KB
i18n (useDisplayLocale.js) 2.85KB 1.45KB
i18n (useObjectLabel.js) 34.34KB 9.17KB
i18n (useSafeTranslation.js) 5.60KB 2.33KB
layout (index.js) 39.15KB 11.05KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.75KB
mobile (index.js) 1.99KB 0.87KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 6.62KB 2.45KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useSpecGesture.js) 5.52KB 2.10KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 13.52KB 4.88KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.24KB 2.16KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 8.39KB 3.10KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 4.83KB 2.27KB
plugin-ai (index.js) 14.81KB 3.63KB
plugin-calendar (index.js) 51.44KB 14.62KB
plugin-charts (index.js) 71.82KB 20.13KB
plugin-chatbot (index.js) 198.27KB 47.18KB
plugin-dashboard (index.js) 133.00KB 35.19KB
plugin-designer (index.js) 216.12KB 44.37KB
plugin-detail (index.js) 258.85KB 67.53KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 144.77KB 37.17KB
plugin-gantt (index.js) 167.99KB 41.37KB
plugin-grid (index.js) 215.46KB 58.88KB
plugin-kanban (index.js) 48.83KB 15.21KB
plugin-list (index.js) 113.90KB 28.11KB
plugin-map (index.js) 21.74KB 7.07KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 43.55KB 11.99KB
plugin-timeline (index.js) 30.68KB 8.95KB
plugin-tree (index.js) 10.56KB 3.71KB
plugin-view (index.js) 85.18KB 21.05KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.66KB 3.50KB
providers (index.js) 0.45KB 0.23KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 4.47KB 1.63KB
react (SchemaRenderer.js) 110.46KB 36.33KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.63KB 2.18KB
react (schema-input.js) 4.25KB 2.04KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (body-dialect.js) 4.78KB 2.09KB
sdui-parser (codegen.js) 6.58KB 2.74KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 5.78KB 2.56KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (kanban-quick-add.js) 3.89KB 1.87KB
sdui-parser (parse.js) 25.28KB 7.80KB
sdui-parser (provenance.js) 3.66KB 1.82KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 18.27KB 6.20KB
types (ai.js) 4.11KB 2.06KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 1.00KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 2.93KB 1.49KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (data-display.js) 3.75KB 1.85KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.85KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (expression.js) 0.20KB 0.18KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (http-inflight.js) 8.87KB 3.73KB
types (http-retry.js) 4.32KB 2.02KB
types (icon-key-migration.js) 4.26KB 1.63KB
types (index.js) 4.74KB 2.25KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 5.00KB 2.39KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 0.20KB 0.18KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (select-option.js) 0.20KB 0.19KB
types (spec-report.js) 5.05KB 1.93KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (strict-authoring-face.js) 17.15KB 6.32KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.11KB 3.32KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review September 24, 2026 18:29
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Sep 24, 2026
Merged via the queue into main with commit 31938f0 Sep 24, 2026
45 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-10276-currency-fraction-digits-not-precision branch September 24, 2026 18:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

1 participant