|
| 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`). --> |
0 commit comments