You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
`FormFieldSchema`'s constraint keys converge on the value shapes the object-field surface landed (#12174): `maxLength`/`minLength` tighten from bare `z.number()` to `z.number().int().min(1)` (the #11566/#11949 template — a character length is a positive integer, and `minLength: 0` is a vacuous always-true declaration refused loudly; "no minimum" is expressed by omitting the key), and `precision`/`scale` tighten to `z.number().int().min(0)` (the #8321 template — digit counts). `min`/`max` are unchanged: they are values, not counts, exactly as on the object-field surface.
6
+
7
+
The row keys are live, measured in objectui: the spec bridge (`form-view.ts` mapField, objectui#5898) and plugin-form (`sectionFields.ts`) copy all six onto the runtime field, the console FormPage merges `override.maxLength ?? def.maxLength` onto the rendered input (objectui#5595), the fields package builds validation rules from `minLength`/`maxLength`/`min`/`max`, and `precision`/`scale` drive the number widgets — so `maxLength: 0` on a form row reached the DOM as an input that accepts nothing, and `GET /forms/:slug` serves the rows verbatim to anonymous renderers. What newly gets rejected: `0` (length pair only), negative and non-integer values on the four count-shaped keys. Unlike the object-field twins there is no type-conditional applicability gate: a form row references its object field by name and usually omits `type`, so the referenced field's type is invisible at parse time — value shape is checkable here, key placement stays the object field's own schema's job. Already-legal declarations round-trip byte-identically, and absence stays absence — no default materializes.
|**reference**|`string`| optional | Target object name for lookup/master_detail fields |
142
142
|**publicPicker**|`{ displayFields?: string[]; maxResults?: integer; filter?: object[]; object?: string }`| optional | Opt this field into the anonymous public-form lookup picker (GET /forms/:slug/lookup/:field). Without it the route answers 403 LOOKUP_NOT_PUBLIC and the field is stripped from the rendered public form. |
143
-
|**maxLength**|`number`| optional | Maximum character length (for text/textarea/email/url/phone) |
144
-
|**minLength**|`number`| optional | Minimum character length |
143
+
|**maxLength**|`integer`| optional | Maximum character length (positive integer; for text/textarea/email/url/phone) |
144
+
|**minLength**|`integer`| optional | Minimum character length (positive integer; `minLength: 0` is refused — express "no minimum" by omitting the key)|
145
145
|**min**|`number`| optional | Minimum value (for number/currency/percent/slider) |
146
146
|**max**|`number`| optional | Maximum value |
147
-
|**precision**|`number`| optional | Total digits (for number/currency) |
148
-
|**scale**|`number`| optional | Decimal places |
147
+
|**precision**|`integer`| optional | Total digits (non-negative integer; for number/currency) |
|**reference**|`string`| optional | Target object name for lookup/master_detail fields |
304
304
|**publicPicker**|`{ displayFields?: string[]; maxResults?: integer; filter?: object[]; object?: string }`| optional | Opt this field into the anonymous public-form lookup picker (GET /forms/:slug/lookup/:field). Without it the route answers 403 LOOKUP_NOT_PUBLIC and the field is stripped from the rendered public form. |
305
-
|**maxLength**|`number`| optional | Maximum character length (for text/textarea/email/url/phone) |
306
-
|**minLength**|`number`| optional | Minimum character length |
305
+
|**maxLength**|`integer`| optional | Maximum character length (positive integer; for text/textarea/email/url/phone) |
306
+
|**minLength**|`integer`| optional | Minimum character length (positive integer; `minLength: 0` is refused — express "no minimum" by omitting the key)|
307
307
|**min**|`number`| optional | Minimum value (for number/currency/percent/slider) |
308
308
|**max**|`number`| optional | Maximum value |
309
-
|**precision**|`number`| optional | Total digits (for number/currency) |
310
-
|**scale**|`number`| optional | Decimal places |
309
+
|**precision**|`integer`| optional | Total digits (non-negative integer; for number/currency) |
Copy file name to clipboardExpand all lines: packages/spec/liveness/view.json
+1-1Lines changed: 1 addition & 1 deletion
Original file line number
Diff line number
Diff line change
@@ -317,7 +317,7 @@
317
317
"verifiedAt": "2026-08-11",
318
318
"evidenceScope": "cross-repo",
319
319
"evidence": "objectui: form renderers + spec-bridge form-view.ts:169 (audit L17); packages/rest/src/rest-server.ts registerPublicFormEndpoints (the three /forms/:slug routes walk sections: resolve strips undeclared lookup/master_detail/user fields, submit builds allowedFields from them, and GET /forms/:slug/lookup/:field reads each field entry's publicPicker block)",
320
-
"note": "This row carries the blanket verdict for the whole FormField subtree beneath it (undrilled inheritance). [#7467] adds `publicPicker` {displayFields,maxResults,filter,object} to that subtree — live, measured reader: the GET /forms/:slug/lookup/:field handler in packages/rest/src/rest-server.ts (displayFields projection + contains search, maxResults clamp to hard ceiling 50, filter rows composed ahead of the search predicate, object override for the referenced object). The key had been ENFORCED there but declared nowhere — ADR-0049's mirror direction (enforced, never declarable) — until the maintainer ruled declare (option 1). The route also reads picker.sort, which stays deliberately UNDECLARED (outside the ruling's four-key enumeration; follow-up finding filed from #7467); a form authoring it is a loud parse error, pinned in view-public-picker.test.ts."
320
+
"note": "This row carries the blanket verdict for the whole FormField subtree beneath it (undrilled inheritance). [#7467] adds `publicPicker` {displayFields,maxResults,filter,object} to that subtree — live, measured reader: the GET /forms/:slug/lookup/:field handler in packages/rest/src/rest-server.ts (displayFields projection + contains search, maxResults clamp to hard ceiling 50, filter rows composed ahead of the search predicate, object override for the referenced object). The key had been ENFORCED there but declared nowhere — ADR-0049's mirror direction (enforced, never declarable) — until the maintainer ruled declare (option 1). The route also reads picker.sort, which stays deliberately UNDECLARED (outside the ruling's four-key enumeration; follow-up finding filed from #7467); a form authoring it is a loud parse error, pinned in view-public-picker.test.ts. [#12174] drills the six FormField constraint keys out of the blanket verdict — maxLength/minLength/min/max/precision/scale are all LIVE, measured at objectui@f7c52e2 (2026-08-26): two independent spec-to-runtime routes copy all six onto the runtime field (packages/react/src/spec-bridge/bridges/form-view.ts mapField, objectui#5898; packages/plugin-form/src/sectionFields.ts normalizeSectionField), the console FormPage merges override.maxLength ?? def.maxLength onto the rendered input (apps/console/src/components/FormPage.tsx:548, objectui#5595), packages/fields buildValidationRules turns minLength/maxLength/min/max into react-hook-form rules, ObjectForm.tsx:691 derives the number input step from precision, and NumberField.tsx:21 reads scale; server-side, GET /forms/:slug (packages/rest/src/rest-server.ts) serves the rows verbatim to anonymous renderers (a conduit, no key-level read). On that measurement the four count-shaped keys tightened to the #11566/#11949/#8321 value shapes (lengths int>=1, precision/scale int>=0; min/max stay bare numbers — they are values, not counts)."
0 commit comments