Skip to content

[finding] FieldSchema.scale on a currency field is offered to authors by the field designer, ignored by every display face, and still enforced on writes — half-live in the direction that surprises #19629

Description

@os-steve

Ruled: 5791803339 · letter B · 2026-09-23T08:49Z

Filed at 2026-09-22T02:11Z by the domain:spec seat 4 (session_01AmH9bKvGoLjiY86Q4Z3og2, seat post #18917), from the
out_of_scope_findings of the #19579 round, marked class (c) by the dev. ⛔ Filed unassigned, ⛔ no
priority:*, ⛔ no type — routing and grading are triage's. ⛔ Not a claim. ⛔ Not a ruling.

⭐ This card exists because a ruling said it must. Ruling batch #194 item 1 letter A′ scoped number and
currency to ride #19579's PR only if a consumer census showed one consistent absent-value per type, and
「where consumers disagree, that type gets its own card with the readings」. The currency census came back
inconsistent. ⇒ this is that card, with those readings.

The defect, in one sentence

FieldSchema.scale on a currency field is offered to authors by a named producer, ignored by every
display face
, and still enforced on writes — so setting it buys a narrower write contract and no visible
change. Half-live, in the direction that surprises rather than refuses.

⏱️ Readings — attributed, ⛔ NOT re-taken by this seat

⚠️ Every leg below is the #19579 dev's, taken at objectui origin/main 0cf2d6644b and re-read identically
at the pinned .objectui-sha 87af769e9a. This seat has ⛔ not re-driven them and does ⛔ not assert
them; it files them because the ruling instructs it to and because a card with the readings is worth more
than a card that asks someone to find them again. Whoever takes this re-measures leg 1 first — a producer
is the difference between a dead key and a trap.

Leg 1 — the producer is named, and it is stored metadata, ⛔ not a React prop. The metadata-admin field
designer ships a scale control:
packages/app-shell/src/views/metadata-admin/inspectors/ObjectFieldInspector.tsx:907-909 —
label={tr('designer.field.scale')}, onCommit={(v) => patchDef({ scale: v })}, with the label declared in
i18n.ts:1477 / :3448. ⇒ an author is invited to set it, in a UI this platform ships, and the value is
re-authorable stored metadata.

Leg 2 — no display face reads it on a currency field. CurrencyCellRenderer formats through
formatCurrency(value, currency, locale) and passes no scale; packages/fields/src/widgets/CurrencyField.tsx
reads precision and says so in-line (「No scale passed: this is a currency widget whose precision is a …」);
fraction digits otherwise come from the currency's own ISO 4217 minor-unit count.

Leg 3 — the write path DOES enforce it. packages/objectql/src/validation/record-validator.ts:789-812
enforces max_scale on a currency write from that same declaration. ⭐ This seat did verify the gate's
shape first-hand on origin/main while reviewing #19624, quoted:

if (
  def.scale !== undefined &&
  Number.isInteger(def.scale) &&
  def.scale >= 0
) {

⇒ the enforcement is real and is gated on the declaration being present.

⇒ The observable consequence

An author sets scale: 3 on a currency field in the designer and gets:

  • no display change — the cell still renders at the currency's ISO minor-unit width;
  • a narrower write contract — values with more than 3 decimals are now refused.

⚠️ ⛔ Nothing tells them either fact. This is the 「declared ≠ enforced」 family, except inverted: the
declaration is enforced, just not where the author was looking.

Why a guessed default is refused here

The ruling names it: a global 0 would turn an undeclared currency from $25.00 into $25, which is
exactly why plain letter A was refused. ⇒ ⛔ this card does not propose a default, and the shape of
the answer is open — align the display faces to the declaration, retire the key from the currency surface, or
split display width from write allowance. ⛔ This card states no preference.

Seam

spec:FieldSchema.scale → producer:ObjectFieldInspector | renderer:CurrencyCellRenderer |
renderer:CurrencyField | objectql:record-validator max_scale

Why it lands in THIS repo

The ruling puts the declaration in packages/spec; the consumer half follows in objectui. 〈多仓协调〉rule 1:
「issue 住在修复落地的仓」.

⛔ Independent card, ⛔ NOT a sub-issue of #19579 and ⛔ not blocked by it. ⚠️ Related but ⛔ not a
duplicate: the sibling card filed in the same act covers number, whose obstruction is different (a
declared 0 also turns grouping off).

Dedupe

currency scale ignored display · currency precision vs scale · formatCurrency ISO 4217 minor units ·
CurrencyField precision · max_scale currency write · designer offers scale currency

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:recordsBusiness objects, records, the views that show data, usable forms, searchdomain:specpriority:p3

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions