|
25 | 25 | * - `valueDomain` a declared standard domain's membership, judged by the |
26 | 26 | * spec's shared `isValueDomainMember` — the WRITTEN value |
27 | 27 | * only (#14168, maintainer ruling 2026-09-02 option A) |
| 28 | + * - number types the value must be a finite JS number, the spec's stored |
| 29 | + * value; a string, array or boolean is `invalid_number`, |
| 30 | + * never coerced (#20309) |
28 | 31 | * - `min` / `max` (number/currency/percent/rating/slider) |
29 | 32 | * - `scale` more decimal places than the field's STORED allowance → |
30 | 33 | * `max_scale` (#7501; rejection, NEVER rounding — |
@@ -857,11 +860,25 @@ function validateOne( |
857 | 860 | // failed with `ERR_SUMMARY_RECOMPUTE` on memory and SQLite. A blank on a |
858 | 861 | // `summary` is still `null` at the door (`normalizeBlankTypedValues` reads the |
859 | 862 | // whole numeric class). |
| 863 | + // |
| 864 | + // [#20309] ONLY a finite JS number passes: the spec's stored value for this |
| 865 | + // class, `valueSchemaFor`'s `z.number().finite()`. The arm judges the value |
| 866 | + // the driver receives, and the driver receives exactly what was sent, since |
| 867 | + // nothing between here and the driver rewrites a numeric value. The arm used |
| 868 | + // to judge `Number(value)` instead, so every value JS coerces to a finite |
| 869 | + // number passed and was then written as sent: `[500]` (SQLite stored the |
| 870 | + // TEXT `'[500]'`, memory the array), `[]`, `true` / `false`, `'0x10'`, |
| 871 | + // `' 12 '`, `'1e3'`, and a plain `'12'` (memory stored the string). |
| 872 | + // ⛔ No coercion here: refuse, never silently alter (the #7501 posture). A |
| 873 | + // write door that parses a string into a number is a second dialect of the |
| 874 | + // value contract. A producer that holds a string converts it itself, as the |
| 875 | + // import route does (`parseNumberCell` in `@objectstack/rest`) before the |
| 876 | + // engine sees the row. |
860 | 877 | if (NUMERIC_VALUE_TYPES.has(t) && !COMPUTED_VALUE_TYPES.has(t)) { |
861 | | - const n = typeof value === 'number' ? value : Number(value); |
862 | | - if (!Number.isFinite(n)) { |
| 878 | + if (typeof value !== 'number' || !Number.isFinite(value)) { |
863 | 879 | return fail('invalid_number'); |
864 | 880 | } |
| 881 | + const n = value; |
865 | 882 | // [#20308] `progress` joined the TYPE check above, and only that. The |
866 | 883 | // bounds and `scale` below keep the five types they always read: `scale`'s |
867 | 884 | // own contract names the types it is enforced on (`number`, `percent`, |
|
0 commit comments