Skip to content

Commit e56112c

Browse files
os-steveclaude
andauthored
fix(spec): bound the form-row scale at the renderer ceiling of 100 (#19088) (#19313)
Fixes #19088 Clause-②: no (narrowing) `FormFieldBaseSchema.scale` in `packages/spec/src/ui/view.zod.ts` — the per-field override a form row carries — was declared as any non-negative integer with no upper bound. It is the third declaration of `scale` to reach the platform ceiling #18972 bounded on the two in `data/field.zod.ts`: every consumer turns `scale` into decimal places, and both `Number.prototype.toFixed` and `Intl.NumberFormat`'s `maximumFractionDigits` throw a `RangeError` at 101, so a conforming declaration could not render. The row now carries that bound, and the refusal names both primitives, the `RangeError` and the legal maximum rather than saying "too large". Evidence below is measured on this branch at `df3185cf9f`, base `origin/main` = `e6a03e649`, node v22.22.2. ## The card's premise, re-verified — one half of it is now false The dispatch's hardest fence rests on PR #19083 being **open** and holding both `field.zod.ts` sites plus the module-private `MAX_RENDERABLE_SCALE`. Measured: **#19083 merged at 2026-09-18T17:48:04Z** as `021755abc4`, two days before the dispatch was written, and both bounds plus the constant are on `main` today. The defect half of the premise is intact and was re-measured, not inherited: at the branch base `scale: 101` **parsed clean** on this row (bright control below). So the card stands; only the reason the fence gave for itself has expired. **The fence was obeyed anyway.** `packages/spec/src/data/field.zod.ts` is untouched in this diff — the file surface the claim declares is `view.zod.ts` and its tests, and widening it is not a call this seat makes even when the in-flight PR that motivated the fence has landed. The consequence is stated plainly in the arm measurement below: route 3 could not be completed on both ends this round, and what remains is recorded rather than smuggled in. ## The shared-constant half — three arms, measured | arm | what it costs | verdict | | :-- | :-- | :-- | | ① duplicate the literal in `view.zod.ts` | one platform fact recorded in a second place with nothing holding the two equal; a later consolidation still has to mint the shared home | viable, not taken | | ② promote `MAX_RENDERABLE_SCALE` to a published export | `packages/spec/src/data/index.ts:132` re-exports `field.zod` with `export *`, so the constant lands on the `@objectstack/spec/data` surface: `Clause-②: yes`, at least `minor`, an at-tier contract review — **and it edits `field.zod.ts`**, which the fence forbids | refused, on both counts | | ③ spec-internal, non-exported shared module | one new leaf file; **no cycle possible** (it imports nothing); **no published export** | **taken** | Arm ③'s two open questions, both answered by measurement rather than by reading: - **Can `view.zod.ts` import it without a cycle?** `packages/spec/src/shared/scale-ceiling.ts` has zero imports, so it is a graph leaf and cannot participate in one. `pnpm --filter @objectstack/spec build` exits 0 and `view.zod.ts` already imports eight other `../shared/*` modules. - **Does it move the published surface?** No. `check:api-surface`, `check:api-surface-declarations`, `check:export-origins` and `check:declaration-map` are all green with the file added — `shared/index.ts` is an explicit list of `export *` lines, not a wildcard, and this module is deliberately absent from it. The precedent is established: `strict-object.ts`, `editability-boundary.ts`, `union-branch-policy.ts` and `section-group-reference.ts` all live there unexported, and `SELECT_OPTION_EDITABILITY_GUIDANCE` has zero occurrences in `packages/spec/api-surface/`. **What arm ③ does NOT buy this round, stated rather than glossed.** Sharing has two ends, and the fence holds one of them: `field.zod.ts` keeps its own module-private `MAX_RENDERABLE_SCALE` and `SCALE_UPPER_BOUND_MESSAGE`, so the tree still records the number twice. The difference from arm ① is what the second copy is *for*: the new module is a home `field.zod.ts` can move into as a pure delete-and-import, buying no export and no review — and until it does, the two are held equal from this end by a pin that asserts the refusal text at both sites is **byte-identical**. A drift between the copies is a red test, not two divergent explanations of one platform limit. ## Acceptance — three legs **Bright control (before the change).** At the branch base `e6a03e649`, against the built `packages/spec/dist/ui/index.mjs`: ``` scale:100 : PASS scale:101 : PASS scale:-1 : REFUSED code=too_small message="Too small: expected number to be >=0" scale:2.5 : REFUSED code=invalid_type message="Invalid input: expected int, received number" precision:101: PASS ``` `scale: 101` passed, so this round demonstrably narrowed something and the probe was never dead. **Subject (after).** Same probe, same built artifact, rebuilt at `df3185cf9f`: ``` scale:100 : PASS scale:101 : REFUSED code=too_big message="Decimal places cannot exceed 100 — the limit is the renderers', not a policy: every consumer turns `scale` into fraction digits, and both `Number.prototype.toFixed` and `Intl.NumberFormat`'s `maximumFractionDigits` throw a RangeError above 100, so a larger declaration is unrenderable rather than merely large. Declare at most 100 (an IEEE-754 double carries ~17 significant digits, so a meaningful display precision is far below it)." ``` `100` parses and round-trips; `101` is refused with a `too_big` issue at `[scale]` whose message names `toFixed`, `maximumFractionDigits`, `RangeError` and the legal maximum — the same text the two `field.zod.ts` sites produce, asserted as such rather than as prose. **Dark control.** `scale: -1` and `scale: 2.5` are refused before and after with the **same code and the same message bytes** (`too_small` / "Too small: expected number to be >=0"; `invalid_type` / "Invalid input: expected int, received number"). The #8321 / #12174 refusals are untouched; only the top of the range moved. The test asserts those two messages by equality, not by regex, so a restatement of the bottom of the range turns it red. **The platform fact re-measures every run, per the card.** `100` is never trusted as a literal: the pin probes `(1.5).toFixed(100 | 101)` and `new Intl.NumberFormat(undefined, { maximumFractionDigits: 100 | 101 })` for `RangeError` on every run and then asserts the schema's own accept/refuse boundary sits on the same number. If a future runtime moves either limit, the pin goes red and the declaration is re-argued. This is #18972's shape, copied. ## The `check-widening-tells` warning the dispatch raised — measured, and it did NOT block The dispatch flagged #19099: `check-widening-tells` T1 can refuse the criterion-honest `Clause-②: no (narrowing)`, making over-declaring the only unblocked path. Run against this branch's real diff: ``` node scripts/pm/check-widening-tells.mjs --declaration no --diff /tmp/pr.diff -> exit 0 ✓ check-widening-tells: 4 changed file(s) — 2 judged against a declared surface (no widening tell), 2 NOT MEASURED. ``` So this diff is a **negative sample** for #19099, not another positive one. The two files judged against a declared surface are `view.zod.ts` and the new `shared/scale-ceiling.ts`; neither fired a tell. Worth recording for that card: T1 did **not** misfire on a `scale:` line that the diff re-emits in full to attach `.max(…)`, and T3 did not fire on a new file carrying two `export const`s that reach no entry point. The declaration stayed `no (narrowing)` on its merits; nothing was flipped to get unblocked. ## What this PR deliberately does not touch - **`packages/spec/src/data/field.zod.ts`** — the dispatch's fence, kept (see above). It is read-only input to one test assertion. - **`precision`** — untouched on this row, as on the object-field row. It is a TOTAL digit count and reaches neither primitive, so the renderer-ceiling argument does not carry to it; a pin records that the omission was measured rather than missed. Whether the three `precision` arms should agree is its own judgement. - **No renderer, no clamp, no fallback.** Refusing an unrenderable declaration at the door is this card; what a renderer should do when handed a large value stays objectui#9808's. - **objectui is not changed** — the consumer chain is cited from the schema's own existing comment, not re-measured across repositories this round. ## Verification | command | result | | :-- | :-- | | `pnpm --filter @objectstack/spec typecheck` | exit 0 | | `pnpm --filter @objectstack/spec test` | exit 0 — 500 files, 14650 tests passed | | `pnpm --filter @objectstack/spec exec vitest run src/ui/view.test.ts` | exit 0 — 466 tests passed; the 7 new cases named individually under `--reporter=verbose` | | `pnpm --filter @objectstack/spec build` | exit 0 (base and head) | | `pnpm --filter @objectstack/spec check:generated` | exit 0 after `--fix` regenerated `content/docs/references/ui/view.mdx` (`gen:docs`) — the two `scale` descriptions, nothing else | | `pnpm lint` (whole repo, `eslint . --no-inline-config`) | exit 0 at `df3185cf9f` | | `pnpm check:nul-bytes` | exit 0 — 9028 text files, no raw control bytes | | `node scripts/check-adr-0087-registration.mjs --base origin/main` | exit 0 — 1 declared-breaking changeset, `not-required (no-migration-prescription)` | | `node scripts/check-changeset-no-major.mjs --base origin/main` | exit 0 | | `node scripts/pm/check-widening-tells.mjs --declaration no --diff …` | exit 0 | | gate families from `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` | 106 derived, all 106 run, each exit code captured before any pipe; `--ran` reconciles to 0 UNRUN | **NOT MEASURED — seven of the derived commands, none of them a finding.** Each refuses its own prerequisite because it reads built `dist/` of packages this diff does not touch: `check:doc-formula-expressions`, `check:doc-security-posture`, `check:docs-transcript-drift`, `check:dual-build-cjs-loads`, `check:lean-entry-closure` and `check:type-check-debt` exit 3 with a PREREQUISITE NOT MET verdict; `check:skill-examples` refuses the same way but exits **1** (`packages/client-react/dist` holds no `.d.ts`), so `dispatch-gates --ran` classifies only six. Building that closure locally exceeded this container's foreground limit (`pnpm --filter '@objectstack/client-react...' build`, killed at 570s). They are recorded as NOT MEASURED, not as green; CI builds the closure and runs them for real. **Base.** This branch is not merged with `main` — it branched at `e6a03e649` and `main` has since moved. The intervening commits touch no `packages/spec/src` file (only the generated `packages/spec/liveness/*` ledger), so nothing above was measured against a file this branch also moves; the merge queue's rebuilt generation is the race-free joint check. ## Changeset `skip-changeset` does not hold and no such label is written: `@objectstack/spec` publishes `dist` in its `files[]` and `src/**/*.zod.ts` besides, and this diff changes what the published artifact accepts at runtime. The changeset is a `minor` carrying the **BREAKING** banner and the ADR-0087 disposition `not-required (no-migration-prescription)`, per the launch-window convention in which `major` is refused and breaking-ness rides the banner rather than the level. Its zero-hit reading: 126 `scale: N` declarations across `*.ts` / `*.tsx` / `*.json` / `*.mdx` / `*.yml` / `*.yaml` on this tree as the lit control, of which four read above 100 and all four are refusal fixtures or prose about them — zero authored declarations above the ceiling. Radius: this repository's tracked files; the known target outside it is a `sys_metadata` row already stored in a running deployment, which no in-repo instrument reaches (#18972 recorded the same blind spot). ## Acceptance notes - **noted, not filed: the platform fact is still recorded three times in this tree.** `shared/scale-ceiling.ts` (new), `data/field.zod.ts`'s module-private copy, and `packages/objectql`'s `MAX_FORMULA_SCALE`. The first two are now pinned byte-equal from this side and the third cannot be folded in at all — `objectql` depends on `spec`, not the reverse, so the duplication there is structural. Successor for the first pair: whichever seat next holds `data/field.zod.ts`, where the adoption is a two-line delete-and-import against the module this PR lands and needs no export and no review. Successor for `MAX_FORMULA_SCALE`: none today. - **noted, not filed: `precision` is bounded on one arm and unbounded on two others.** `CurrencyConfigSchema.precision` is `.max(10)`; `FieldSchema.precision` and `FormFieldBaseSchema.precision` have no maximum (`precision: 101` is measured to parse clean on this row, before and after). No renderer-ceiling argument applies to any of them, so this is not the same defect wearing another name, and whether the three should agree is a judgement about three different field arms rather than a bug. #18972 carried the identical note. Successor: none named — it is one line away from both sites, so whoever takes a `precision` decision takes all three. - **noted, not filed: the dispatch's fence outlived its stated reason.** PR #19083 had already merged when the fence was written against it still being open. Recorded because the next card cut from the same seam will inherit the same sentence; the fence was still obeyed here. Successor: the PM loop that issues the next `field.zod.ts` card. --- _Generated by [Claude Code](https://claude.ai/code/session_01AmH9bKvGoLjiY86Q4Z3og2)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent c86d351 commit e56112c

5 files changed

Lines changed: 302 additions & 3 deletions

File tree

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
---
2+
'@objectstack/spec': minor
3+
---
4+
5+
fix(spec)!: the form-row `scale` is bounded at the renderer ceiling of 100 (#19088)
6+
7+
Clause-②: no (narrowing)
8+
9+
`FormFieldBaseSchema.scale` — the per-field override a form row carries — was declared as
10+
any non-negative integer with no upper bound. It is the third declaration of `scale` to
11+
reach the same platform ceiling #18972 bounded on the two in `data/field.zod.ts`: every
12+
renderer that turns a declared `scale` into fraction digits reaches one of two platform
13+
primitives, and both refuse above 100. `Number.prototype.toFixed` throws `RangeError:
14+
toFixed() digits argument must be between 0 and 100`, and `Intl.NumberFormat` throws
15+
`RangeError: maximumFractionDigits value is out of range.` So a spec-valid declaration was
16+
unrenderable by any conforming consumer, and its author got no signal at publish time —
17+
the failure arrived as a render-time crash in someone else's repository. The route from
18+
this row to that reader, measured in the sibling checkout at the `.objectui-sha` pin
19+
`53ded82bf7`: plugin-form copies the row's constraint keys onto the runtime field
20+
(`packages/plugin-form/src/sectionFields.ts:220`, `if (fd.scale != null) base.scale =
21+
fd.scale;`), and the number cell renderer hands that value straight to `Intl.NumberFormat`
22+
(`packages/fields/src/index.tsx:661-667`, `maximumFractionDigits: scale ?? 20`). That one
23+
route carries the premise on its own.
24+
25+
The row now carries that upper bound, and the refusal says **why** — it names both
26+
primitives, the `RangeError` and the legal maximum — so an author reads a platform limit
27+
they can verify rather than a cap somebody chose. The bound is the platform's own: at 100
28+
both primitives are measured to succeed, at 101 both are measured to throw, and a unit
29+
test re-measures that boundary on every run rather than trusting the literal.
30+
31+
**BREAKING** — a form-row `scale` above 100 that parsed clean before is refused at
32+
authoring now. This is a deliberate narrowing of a published accepted set, priced as such
33+
rather than as a tidy-up. The declarations it refuses could only ever have crashed a
34+
renderer: there is no value above 100 that any conforming consumer can render, which is
35+
why the bound is the platform's limit and not a policy number.
36+
37+
Unchanged in both directions: `scale: 100` still parses, `scale: 0` still parses, absence
38+
is still absence, and the malformed-declaration refusals from #8321/#12174 (`scale: -1`,
39+
`scale: 2.5`) keep their existing codes and their existing wording. `precision` is
40+
untouched on this row as on the object-field row — it is a total digit count that reaches
41+
neither primitive, so the renderer-ceiling argument does not carry to it.
42+
43+
The number itself moves into `src/shared/scale-ceiling.ts`, a spec-internal leaf module
44+
that no package entry re-exports, so no published export moves and `check:api-surface`,
45+
`check:export-origins` and `check:declaration-map` all stay green. `data/field.zod.ts`
46+
keeps the module-private copy #18972 minted; a pin asserts the two sites refuse with
47+
byte-identical text, so the duplication is held equal rather than left to drift, and that
48+
file can adopt the shared module later as a pure delete-and-import.
49+
50+
Shipped as `minor` under the repo's launch-window convention, in which
51+
`check-changeset-no-major` refuses `major` and breaking-ness is carried by this banner
52+
plus the ADR-0087 disposition rather than by the level.
53+
54+
<!-- adr-0087: not-required (no-migration-prescription) no authorable key is renamed, retired or reshaped: `scale` keeps its name, its place on the form row and its type, and what moves is the top of one existing key's accepted numeric range. So `objectstack migrate meta` has nothing to visit — there is no stored spelling to rewrite and no FROM side to map, because the values this now refuses have no correct mechanical replacement: 101 fraction digits is not a precision a renderer can honour at all, and picking the display precision an author actually meant is their judgement, not a transform the ledger can carry. A ledger row would therefore have to invent the very semantics ADR-0078 and PD #12 forbid inventing, and the channel that does reach every affected author is the parse refusal itself, which names both primitives, the `RangeError` and the legal maximum at the moment the declaration is written. Measured on this tree at `7e52288463`: 126 `scale: N` declarations across `*.ts` / `*.tsx` / `*.json` / `*.mdx` / `*.yml` / `*.yaml` as the lit control, of which four read above 100 and all four are refusal fixtures or prose about them (two from #18972's tests, two from this branch's) — zero authored declarations above the ceiling. The instrument's radius is this repository's tracked file contents; the known target outside it is a `sys_metadata` row already stored in a running deployment's database, which no in-repo instrument reaches, and #18972 recorded the same blind spot. The other four categories are closed on facts: `@objectstack/spec` publishes to npm and declares no `private` (not `unpublished`); no ADR-0087 id is minted in this diff (not `registered`) and none pre-dating the base covers it (not `already-registered`); and the surface that moves is a Zod metadata schema, not a runtime-only TS interface and not a type annotation (neither `runtime-interface-only` nor `type-surface-only`). -->

‎content/docs/references/ui/view.mdx‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -189,7 +189,7 @@ Column footer summary configuration
189189
| **min** | `number` | optional | Minimum value (for number/currency/percent/slider) |
190190
| **max** | `number` | optional | Maximum value |
191191
| **precision** | `integer` | optional | Total digits (non-negative integer; for number/currency) |
192-
| **scale** | `integer` | optional | Decimal places (non-negative integer) |
192+
| **scale** | `integer` | optional | Decimal places (integer 0-100). The upper bound is the platform's, not a policy: renderers turn `scale` into fraction digits through `toFixed` and `Intl.NumberFormat`'s `maximumFractionDigits`, both of which throw a RangeError above 100 — so a larger declaration is unrenderable by any conforming consumer. |
193193
| **multiple** | `boolean` | optional | Allow multiple values (for select/lookup/file/image) |
194194
| **label** | `string \| Record<string, string>` | optional | Display label override |
195195
| **placeholder** | `string \| Record<string, string>` | optional | Placeholder text |
@@ -354,7 +354,7 @@ View filter rule
354354
| **min** | `number` | optional | Minimum value (for number/currency/percent/slider) |
355355
| **max** | `number` | optional | Maximum value |
356356
| **precision** | `integer` | optional | Total digits (non-negative integer; for number/currency) |
357-
| **scale** | `integer` | optional | Decimal places (non-negative integer) |
357+
| **scale** | `integer` | optional | Decimal places (integer 0-100). The upper bound is the platform's, not a policy: renderers turn `scale` into fraction digits through `toFixed` and `Intl.NumberFormat`'s `maximumFractionDigits`, both of which throw a RangeError above 100 — so a larger declaration is unrenderable by any conforming consumer. |
358358
| **multiple** | `boolean` | optional | Allow multiple values (for select/lookup/file/image) |
359359
| **label** | `string \| Record<string, string>` | optional | Display label override |
360360
| **placeholder** | `string \| Record<string, string>` | optional | Placeholder text |
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
/**
4+
* # The platform ceiling on decimal places (#18972, #19088)
5+
*
6+
* Every renderer that turns a declared `scale` into fraction digits reaches one
7+
* of two platform primitives, and BOTH refuse above 100. Measured first-hand on
8+
* node v22.22.2:
9+
*
10+
* (1.5).toFixed(100) -> '1.5000...' (ok)
11+
* (1.5).toFixed(101) -> RangeError: toFixed() digits argument must be between 0 and 100
12+
* new Intl.NumberFormat(u, { maximumFractionDigits: 100 }) -> ok
13+
* new Intl.NumberFormat(u, { maximumFractionDigits: 101 }) -> RangeError: maximumFractionDigits value is out of range.
14+
*
15+
* So the bound is the PLATFORM's, not a policy: it is the largest value every
16+
* conforming consumer can render, and a declaration past it is unrenderable
17+
* rather than merely large. The literal is never trusted on its own — the pins
18+
* beside each consuming schema re-measure both primitives on every run and
19+
* assert the schema's own accept/refuse boundary sits on the same number, so a
20+
* runtime that moves either limit turns the pin red instead of silently
21+
* shifting what authors may write.
22+
*
23+
* ## Why this module exists, and why it exports nothing to consumers
24+
*
25+
* #18972 minted this fact inside `data/field.zod.ts` as a module-private
26+
* `MAX_RENDERABLE_SCALE`, for the two `scale` declarations that file carries.
27+
* #19088 is the third site — `FormFieldBaseSchema.scale` in `ui/view.zod.ts`,
28+
* the form-row override — which reaches the same `Intl.NumberFormat` reader
29+
* through plugin-form. Measured in the sibling checkout at the `.objectui-sha`
30+
* pin `53ded82bf7`:
31+
*
32+
* packages/plugin-form/src/sectionFields.ts:220
33+
* if (fd.scale != null) base.scale = fd.scale; // form row -> runtime field
34+
* packages/fields/src/index.tsx:661-667
35+
* maximumFractionDigits: scale ?? 20 // runtime field -> Intl
36+
*
37+
* ⛔ Do NOT cite an objectui "spec bridge" (`form-view.ts` `mapField`) here.
38+
* That route is RETIRED at this pin — the tree carries
39+
* `.changeset/retire-spec-bridge-6366.md`, there is no `spec-bridge/`
40+
* directory, and the only `mapField` hits are `mapFieldTypeToFormType` in
41+
* `app-shell`, a different symbol. The citation survives in this repo's older
42+
* neighbouring comments (the #12174 block in `ui/view.zod.ts` and its test),
43+
* inherited from `liveness/view.json`'s 2026-08-26 reading at objectui@f7c52e2;
44+
* it was carried forward once into this card and corrected here. The
45+
* plugin-form route above carries the premise on its own.
46+
*
47+
* A second file needing the same number left three ways to share it, each with
48+
* its own price:
49+
*
50+
* 1. **Duplicate the literal** in `view.zod.ts` — cheapest edit, and it records
51+
* one platform fact in a second place with nothing holding the two equal.
52+
* 2. **Promote `MAX_RENDERABLE_SCALE` out of `field.zod.ts` as a published
53+
* export** — `data/index.ts` re-exports that file with `export *`, so the
54+
* constant would land on the `@objectstack/spec/data` surface: a new export
55+
* (`Clause-②: yes`, at least `minor`, and a contract-tier review) bought for
56+
* one bound, on a file another card holds.
57+
* 3. **This module** — a `src/shared/` leaf that is deliberately absent from
58+
* `shared/index.ts`, so it reaches no package entry and no `api-surface`
59+
* snapshot. It is the established shape for spec-internal constants
60+
* (`strict-object.ts`, `editability-boundary.ts`, `union-branch-policy.ts`
61+
* are the same kind), it imports nothing so it cannot participate in a
62+
* cycle, and adopting it costs a consumer one import line.
63+
*
64+
* Route 3 is what landed. `data/field.zod.ts` still carries its own private
65+
* copy of both values, because the surface #19088 was dispatched against is
66+
* `ui/view.zod.ts` alone — this module is the home that copy can move into
67+
* later as a pure delete-and-import, with no export and no review to buy. Until
68+
* it does, the two are held equal from the other end: the pin beside
69+
* `FormFieldBaseSchema.scale` asserts its refusal text is byte-identical to the
70+
* one `FieldSchema.scale` produces, so a drift between the copies is a red
71+
* test rather than two divergent messages for one platform limit.
72+
*
73+
* The consumer-side half of the same fact lives in `packages/objectql` as its
74+
* module-private `MAX_FORMULA_SCALE` (it skips formula rounding past the
75+
* ceiling so a display declaration can never fail a read). That one is
76+
* deliberately not folded in here either: `packages/spec` may not depend on a
77+
* consumer package.
78+
*/
79+
80+
/**
81+
* The largest `scale` (decimal places) every conforming renderer can honour.
82+
* Both platform primitives throw `RangeError` above it.
83+
*/
84+
export const MAX_RENDERABLE_SCALE = 100;
85+
86+
/**
87+
* The refusal text for a `scale` past {@link MAX_RENDERABLE_SCALE}. It names
88+
* WHY, so an author reads a platform limit they can verify rather than an
89+
* arbitrary cap somebody chose. Every occurrence of the number is derived from
90+
* the constant, so the prose cannot drift from the bound it explains.
91+
*/
92+
export const SCALE_UPPER_BOUND_MESSAGE =
93+
'Decimal places cannot exceed ' + MAX_RENDERABLE_SCALE + ' — the limit is the renderers\', not a policy: every consumer '
94+
+ 'turns `scale` into fraction digits, and both `Number.prototype.toFixed` and '
95+
+ '`Intl.NumberFormat`\'s `maximumFractionDigits` throw a RangeError above ' + MAX_RENDERABLE_SCALE + ', so a larger '
96+
+ 'declaration is unrenderable rather than merely large. Declare at most ' + MAX_RENDERABLE_SCALE + ' (an IEEE-754 '
97+
+ 'double carries ~17 significant digits, so a meaningful display precision is far below it).';

0 commit comments

Comments
 (0)