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
fix(spec): minLength is a positive integer, authorable only on bounded-string field types; register the deferred #11566 maxLength ADR-0087 entry (#12191)
* fix(spec): minLength is a positive integer, authorable only on bounded-string field types
Issue #11949 context (maintainer ruling 2026-08-25, option B): the #11566
template applies in full. Shape tightens to z.number().int().min(1) — the
lower bound is 1, so minLength: 0 is refused loudly ("no minimum" is
expressed by omitting the key, not by declaring a vacuous truth).
Applicability converges on BOUNDED_STRING_FIELD_TYPES via a superRefine twin
of maxLength's; both authoring-form rows align to the same set
(field.form.ts previously showed the key for 3 types, object.form.ts for 9).
Card relationship is declared in the PR body, not here.
Ruled pre-step: corpus + generator grep for minLength: 0 / negative /
fractional output — zero hits repo-wide, so no cleanup rides the PR and no
customer-facing generator fork exists.
- field.zod.ts: shape + applicability superRefine + set doc comment
- field.form.ts / object.form.ts: minLength rows aligned to the ten
- field.test.ts: refusal envelopes (path + code + message) for 0 / -5 / 12.5
and for nine wrong-type representatives; byte-identical round-trip on all
ten bounded-string types; minLength: 1 lower-bound pin; absence stays
absent
- lint RHS-rule census re-derived 44 -> 41 (3 fewer == literal comparisons
after the in-list respell of the field-form row — the #11989 precedent)
- D3 semantic entry field-min-length-malformed-or-misplaced-refused
(major-18 one-file shard + gen:migration-registry)
- changeset: @objectstack/spec minor (launch-window convention), adr-0087:
registered
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NDGG54XF5gbTLdQzCtnaVV
* feat(spec): register the deferred #11566 maxLength narrowing in the ADR-0087 ledger
Issue #11950 context: the #11566 enforcement (PR #11989) shipped without its
ADR-0087 ledger entry — the migrations registry was serialized behind an
in-flight change in that wave — and this commit lands the missing half as a
major-18 D3 semantic entry, following the #8321 scale/precision template.
Semantics are authoritative to PR #11989's actual diff: refused shapes
(0 / negative / non-integer on any type), refused placement (any value
outside the ten-member BOUNDED_STRING_FIELD_TYPES set as of that landing),
forms convergence, byte-identical round-trip for well-formed declarations.
The entry separates the mechanical half (misplaced keys were inert by
construction — the validator's bounded-string branch never read them) from
the judgment half (malformed values on bounded-string types WERE consumed by
the raw comparison — maxLength: 0 accepted only empty strings, a negative
value refused every write — so the author must re-declare the bound they
meant). No accept/reject behaviour moves in this commit. Card relationship
is declared in the PR body, not here.
- D3 semantic entry field-max-length-malformed-or-misplaced-refused
(major-18 one-file shard + gen:migration-registry)
- changeset: @objectstack/spec minor, adr-0087: registered
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NDGG54XF5gbTLdQzCtnaVV
* regen: migration registry + docs references from the merged tree
Discharges the os-regen deferral the merge commit recorded: registry
regenerated from the (merge-updated) semantic shards; field.mdx rendered
from the merged schema (minLength row at the twelve-member set).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NDGG54XF5gbTLdQzCtnaVV
---------
Co-authored-by: Claude <noreply@anthropic.com>
The #11566`maxLength` narrowing (shipped in 17.x: `z.number().int().min(1)`, refused outside `BOUNDED_STRING_FIELD_TYPES`) is now registered in the ADR-0087 migration ledger (#11950) — the enforcement PR deliberately deferred the entry because the registry file was serialized behind an in-flight change. Following the #8321`scale`/`precision` template, the major-18 semantic entry carries both halves: the mechanical one (delete the key where it was misplaced — inert by construction outside the write-time validator's bounded-string branch) and the judgment one (a malformed value on a bounded-string type WAS consumed by the validator's raw comparison — `maxLength: 0` accepted only empty strings, a negative value refused every write — so only the author knows the bound they meant; the entry tells them to re-declare it). `objectstack migrate meta`, `spec-changes.json` and the upgrade guide surface the entry at the major boundary; no accept/reject behaviour changes in this release.
`FieldSchema.minLength` tightens on both axes (#11949, maintainer ruling 2026-08-25) — `maxLength`'s twin defect pair (#11566), closed with the same template. Shape: the key is now `z.number().int().min(1)`, so `minLength: 0`, negative and non-integer declarations are refused at parse. The lower bound is 1 by ruling: "no minimum" is expressed by omitting the key, not by declaring a vacuous truth — `minLength: 0` can never fail, and a permanently-true declaration is exactly the noise an AI metadata author mass-produces, so it is refused loudly at authoring time. Applicability: the key sat on the base schema and was authorable on every field type; it is now refused on any type that does not store a bounded string, and accepted on exactly the `BOUNDED_STRING_FIELD_TYPES` set — `text`, `textarea`, `email`, `url`, `phone`, `password`, `markdown`, `html`, `richtext`, `code`, `signature`, `qrcode` (twelve members since #11875) — the same set `maxLength` converged on.
6
+
7
+
What newly gets rejected: `minLength: 0` / negative / non-integer on any type, and `minLength` with any value on every non-bounded-string type (`boolean`, `number`, `date`, `select`, `lookup`, `autonumber`, `formula`, `json`, `secret`, …). Both rejections are prescriptive — the message names the legal shape, the legal type set, and the fix. The two authoring forms converge on the same set (`field.form.ts` previously showed the key for three types; `object.form.ts` for nine). Already-legal declarations (a positive-integer `minLength` on a bounded-string type) round-trip byte-identically, and absence stays absence — no default materializes.
Copy file name to clipboardExpand all lines: content/docs/references/data/field.mdx
+1-1Lines changed: 1 addition & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -66,7 +66,7 @@ const result = CurrencyConfigSchema.parse(data);
66
66
|**unique**|`boolean \| 'global' \| 'organization'`| optional (default: `false`) | Unique constraint and its scope (ADR-0120). 'organization' = one holder per organization (NULL-safe composite with the organization key part on organization-scoped objects) — prefer this explicit spelling in new code; true = same per-organization scope (positional synonym, stays valid); 'global' = one holder across the whole installation. 'tenant'/'org' are rejected — the word is 'organization' |
67
67
|**defaultValue**|`any`| optional | Default applied on INSERT when the field is omitted or null (`''` is a real value, not absence). Three legal shapes (#7127), discriminated in the engine's own order: a CEL Expression envelope `{ dialect: 'cel', source: 'today()' }` (accepted structurally; result type is a runtime concern); a runtime TOKEN — `NOW()` on `datetime`/`date`/`time` only, `current_user` on `user` or `lookup` with `reference: 'sys_user'` only, neither on a multi-value field; or a LITERAL, which must satisfy this field's own stored value contract (ADR-0104 D1 `valueSchemaFor`). Anything else is refused at parse time with a prescriptive message. |
68
68
|**maxLength**|`integer`| optional | Max character length (positive integer). Only authorable on types that store a bounded string: text, textarea, email, url, phone, password, markdown, html, richtext, code, signature, qrcode. |
69
-
|**minLength**|`number`| optional | Min character length |
69
+
|**minLength**|`integer`| optional | Min character length (positive integer; `minLength: 0` is refused — express "no minimum" by omitting the key). Only authorable on types that store a bounded string: text, textarea, email, url, phone, password, markdown, html, richtext, code, signature, qrcode.|
70
70
|**precision**|`integer`| optional | Total digits (non-negative integer) |
{field: 'defaultValue',helpText: 'Default value for new records'},
36
36
{field: 'placeholder',helpText: 'Hint text shown inside the empty input (disappears once a value is entered); use inlineHelpText for always-visible help'},
// 2026-08-25: the #11566 template applies in full). This row used to
41
+
// show the key for 3 types while the schema accepted it on every
42
+
// type; it moves with the set, exactly like the row below.
43
+
{field: 'minLength',visibleWhen: "data.type in ['text','textarea','email','url','phone','password','markdown','html','richtext','code','signature','qrcode']",helpText: 'Minimum character length'},
39
44
// #11566 — `maxLength` is shown for exactly the bounded-string types
40
45
// the schema accepts it on and the write-time validator enforces it
41
46
// for (BOUNDED_STRING_FIELD_TYPES; maintainer ruling 2026-08-24).
maxLength: z.number().int().min(1).optional().describe('Max character length (positive integer). Only authorable on types that store a bounded string: text, textarea, email, url, phone, password, markdown, html, richtext, code, signature, qrcode.'),
900
-
minLength: z.number().optional().describe('Min character length'),
901
+
// #11949 (maintainer ruling 2026-08-25) — `minLength` converges on the
902
+
// #11566 template above, `maxLength`'s twin defect pair: same shape, same
903
+
// applicability set, same forms convergence. The lower bound is deliberately
904
+
// 1, not 0: "no minimum" is expressed by OMITTING the key, so `minLength: 0`
905
+
// is a permanently-true declaration — exactly the vacuous noise an AI
906
+
// metadata author mass-produces — and is refused loudly at authoring instead
907
+
// of parsing cleanly and asserting nothing.
908
+
minLength: z.number().int().min(1).optional().describe('Min character length (positive integer; `minLength: 0` is refused — express "no minimum" by omitting the key). Only authorable on types that store a bounded string: text, textarea, email, url, phone, password, markdown, html, richtext, code, signature, qrcode.'),
901
909
902
910
/** Number Constraints */
903
911
// #8321 — `precision`/`scale` are digit COUNTS, so a non-integer or negative
0 commit comments