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
Browse filesBrowse the repository at this point in the historyBrowse files
authored
docs(spec): state the transition-gate vs invariant boundary at each declaration (#14889)
`Field.requiredWhen` and the field bounds (`min`/`max`/`minLength`/`maxLength`)
are transition gates; a `validations[]` `script` rule is a true invariant. Both
semantics are deliberate and neither moves here — what was missing is that no
platform surface said so, while "required when X" reads as an invariant.
Measured against the engine before writing a word (12 cases, all green):
`evaluateValidationRules` refuses a `requiredWhen` write only when the merged
record violates AND the pre-write record complied (the ADR-0113 non-regression
branch), `validateRecord` iterates the UPDATE payload so a bound is checked on
the written value only, and `checkPredicate` re-evaluates a `script` rule
against the merged record with no pre-state exemption.
Adds a schema-reachable pin over the contract text, and regenerates the
reference pages the `.describe()` strings feed.
Claude-Session: https://claude.ai/code/session_017RbbUMnxkUnWhE4j94v8FE
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Sam Altman <sam@objectstack.ai>
Copy file name to clipboardExpand all lines: content/docs/references/data/field.mdx
+7-7Lines changed: 7 additions & 7 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -63,13 +63,13 @@ const result = CurrencyConfigSchema.parse(data);
63
63
|**multiple**|`boolean`| optional (default: `false`) | Allow multiple values (Stores as Array/JSON). Applicable for select, lookup, file, image. An emptied multi-value lookup reads back as `[]`, never `null` — the rule binds every writer (cascade repair, form clears, API writes), not just cascade repair (maintainer ruling 2026-08-18). |
64
64
|**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' |
65
65
|**defaultValue**|`any`| optional | Default applied on INSERT when the field is omitted or null (`''` is a real value, not absence). Three legal shapes, 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. |
66
-
|**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. |
67
-
|**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. |
66
+
|**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. Checked on the WRITTEN value only (the `min`/`max` transition-gate class): a stored value longer than a bound declared later is never re-read and survives unrelated edits — only a write carrying an over-long value is refused. |
67
+
|**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. Checked on the WRITTEN value only (the `min`/`max` transition-gate class): a stored value shorter than a bound declared later is never re-read and survives unrelated edits — only a write carrying a too-short value is refused. |
68
68
|**rows**|`integer`| optional | Height of the INLINE multiline editor, in text rows (positive integer — the HTML textarea `rows` attribute; fullscreen/dialog editor surfaces size themselves and ignore it). Only authorable on multiline editor types: textarea, markdown, html, richtext. Omit it for the widget default height. |
69
69
|**precision**|`integer`| optional | Total digits (non-negative integer) |
|**min**|`number`| optional | Minimum value. Checked on the WRITTEN value only — the same transition-gate class as `requiredWhen`: an UPDATE validates just the fields the payload carries, so a stored value below a bound declared later is never re-read and survives unrelated edits; only a write that carries an out-of-bound value is refused, and a repairing write is accepted. For an invariant re-checked on every write, declare a `validations[]``script` rule instead.|
72
+
|**max**|`number`| optional | Maximum value. Checked on the WRITTEN value only — the same transition-gate class as `min`: a stored value above a bound declared later is never re-read and survives unrelated edits; only a write that carries an out-of-bound value is refused. For an invariant re-checked on every write, declare a `validations[]``script` rule instead.|
73
73
|**useGrouping**|`boolean`| optional | Digit-grouping presentation hint for `number` fields — maps to `Intl.NumberFormat`'s `useGrouping`. Absent = renderer decides (interim heuristic today, locale default eventually); `false` = author opts out of grouping (e.g. a year or other ordinal/identifier integer); `true` = author pins grouping on. |
74
74
|**accept**|`string[]`| optional | Permitted upload types for media fields, as MIME types or extensions (e.g. ["image/*", ".pdf"]). Offered to the file picker AND enforced on write. |
75
75
|**maxSize**|`integer`| optional | Maximum permitted file size in BYTES for media fields. Enforced on write against the stored file size, not just checked in the browser. |
@@ -103,7 +103,7 @@ const result = CurrencyConfigSchema.parse(data);
103
103
|**group**|`string`| optional | Field group name for organizing fields in forms and layouts (e.g., "contact_info", "billing", "system") |
104
104
|**visibleWhen**|`string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }`| optional | Predicate (CEL) — field is shown only when TRUE (else hidden). e.g. P`record.type == 'invoice'`|
105
105
|**readonlyWhen**|`string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }`| optional | Predicate (CEL) — field is read-only when TRUE. e.g. P`record.status == 'paid'`|
106
-
|**requiredWhen**|`string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }`| optional | Predicate (CEL) — field is required when TRUE. The only slot; the `conditionalRequired` alias was removed in protocol 17. |
106
+
|**requiredWhen**|`string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }`| optional | Predicate (CEL) — field is required when TRUE. A TRANSITION GATE, not an invariant: the write is refused only when the merged record violates the requirement AND the pre-write record complied — so the write that flips the predicate TRUE, an INSERT born inside the gate, and a write that clears the cell are all refused, while a row that was already missing the value keeps passing unrelated edits and state moves that stay inside the gate (ADR-0113 non-regression: adding the rule to a deployed object never bricks existing rows). Need an invariant every write must satisfy instead ('X may never exceed Y') — declare a `validations[]``script` rule, which re-checks the merged record with no exemption. Enforced by `evaluateValidationRules`. The only slot; the `conditionalRequired` alias was removed in protocol 17. |
107
107
|**conditionalRequired**|`never`| optional |[REMOVED]`conditionalRequired` was removed in @objectstack/spec 17 — use `requiredWhen`. Rename the key; the value (a CEL predicate) is unchanged. Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand. |
108
108
|**widget**|`string`| optional | Form widget override — names a registered field component (resolved as `field:<widget>`) to render this field instead of the `type` default. Degrades to the `type` renderer when unregistered. e.g. "object-ref", "filter-condition", "recipient-picker". |
109
109
|**hidden**|`boolean`| optional (default: `false`) | Hidden from default UI |
@@ -218,7 +218,7 @@ const result = CurrencyConfigSchema.parse(data);
218
218
|**scale**|`integer`| optional | Decimal places to round a computed numeric/currency result to. |
219
219
|**autofill**|`boolean`| optional | For `lookup` columns: picking a record copies its same-named fields into sibling columns (a product's unit_price/description). On by default; set false to disable. |
220
220
|**readonlyWhen**|`string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }`| optional | Predicate (CEL) — the cell is read-only when TRUE, evaluated per row against the row as `record` plus the header as `parent` (e.g. P`parent.status == 'paid'`). |
221
-
|**requiredWhen**|`string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }`| optional | Predicate (CEL) — the cell is required when TRUE. Same `record` + `parent` scope as `readonlyWhen`. |
221
+
|**requiredWhen**|`string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }`| optional | Predicate (CEL) — the cell is required when TRUE. Same `record` + `parent` scope as `readonlyWhen`. PRESENTATION ONLY: this flags the cell inline-invalid in the grid; nothing on the write path reads it. The server-enforced contract is the child FIELD's own `requiredWhen` — a transition gate, see `Field.requiredWhen` — which hydration copies onto an identity-only column, so declaring the requirement here alone enforces nothing. |
|**scale**|`integer`| optional | Decimal places to round a computed numeric/currency result to. |
369
369
|**autofill**|`boolean`| optional | For `lookup` columns: picking a record copies its same-named fields into sibling columns (a product's unit_price/description). On by default; set false to disable. |
370
370
|**readonlyWhen**|`string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }`| optional | Predicate (CEL) — the cell is read-only when TRUE, evaluated per row against the row as `record` plus the header as `parent` (e.g. P`parent.status == 'paid'`). |
371
-
|**requiredWhen**|`string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }`| optional | Predicate (CEL) — the cell is required when TRUE. Same `record` + `parent` scope as `readonlyWhen`. |
371
+
|**requiredWhen**|`string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }`| optional | Predicate (CEL) — the cell is required when TRUE. Same `record` + `parent` scope as `readonlyWhen`. PRESENTATION ONLY: this flags the cell inline-invalid in the grid; nothing on the write path reads it. The server-enforced contract is the child FIELD's own `requiredWhen` — a transition gate, see `Field.requiredWhen` — which hydration copies onto an identity-only column, so declaring the requirement here alone enforces nothing. |
0 commit comments