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
feat(spec): declare rows (multiline editors) and options[].description as authorable field-metadata keys (objectui#6140 / objectui#6153, spec half) (#13669)
* feat(spec): declare rows (multiline editors) and options[].description as authorable field-metadata keys
objectui#6140 (maintainer ruling 2026-08-25, Option A) + objectui#6153
(inherited per key): both keys were consumed-but-undeclared — objectui's
RichTextField/TextAreaField read rows through an as-any, LookupField
searches option description — while the strict publish door refused them.
Declared for the measured consumption sets, the #11566 template: superRefine
refuses rows outside textarea/markdown/html/richtext; house count discipline
refuses 0/negative/fractional rows. dependsOn deliberately NOT declared (the
canonical depends_on exists; widget fix rides the objectui half). The four
inert editor keys stay undeclared, pinned.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PBjwYLS6BciTQW3c9xQiD2
* chore(i18n): regenerate the nine translation bundles for the new metadataForms rows entries
node scripts/check-i18n-bundles.mjs --write (merge mode) — the two new form
rows of objectui#6140 land as metadataForms keys; translated locales receive
the source text per merge-mode contract and still need translating.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PBjwYLS6BciTQW3c9xQiD2
* docs(permissions): re-anchor the system-context preserveAudit citation after the field.zod.ts insertion (census --fix)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PBjwYLS6BciTQW3c9xQiD2
* chore(liveness): classify field/rows (live, cross-repo evidence) and note options[].description; regenerate state counts
The liveness walk found the newly declared rows key UNCLASSIFIED. Entry
follows the placeholder precedent (consumers-shipped-first, cross-repo
objectui evidence with line cites). options note now names description
(objectui#6153). state-counts.md regenerated: field 88->89 live.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PBjwYLS6BciTQW3c9xQiD2
* fix(driver-sql,lint): classify the new rows key (presentation) and invert the shipped-form census pins in place
Patch round on PR #13669's red head 83eeba1, two signatures:
- driver-sql builtin-column-collision (#12015): the classification pin
demands every FieldSchema key be classified deliberately; rows joins as
'presentation' (read by objectui's TextAreaField/RichTextField, never by
the DDL). CI red 'unclassified FieldSchema key(s): [rows]' -> green.
- packages/lint validate-predicate-path-refs census pins: the two new
visibleWhen-gated rows entries move the corpus 49->51 (shipped-form
predicates) and 17->18 (object form's data.type-rooted predicates);
inverted in place with the reason recorded, per the #13216 precedent.
The ==-literal census stays 42 (the new rows use 'data.type in [...]').
Maintainer ruled the open question A: the four-type multiline set stands.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PBjwYLS6BciTQW3c9xQiD2
* docs(permissions): re-derive the system-context census anchor on the merged tree
Discharges the regeneration owed by the preceding merge commit:
`node scripts/check-system-context-census.mjs --fix` re-anchored the one
line-rotted citation; the gate is green on the merged tree.
* docs(permissions): re-derive the system-context census anchor on the merged tree
---------
Co-authored-by: Claude <noreply@anthropic.com>
**`rows` and `options[].description` become declared, authorable field-metadata keys** (objectui#6140 / objectui#6153 — maintainer ruling 2026-08-25, Option A, verbatim: 「就全部接受,然后继续下一批」; the accepted-set-expansion follows the #11566/#11949`maxLength`/`minLength` convergence template).
6
+
7
+
Both keys were consumed-but-undeclared — real behaviour in the running app, reached through an `as any` on the metadata carrier, while the strict publish door refused exactly the author who wrote them legally:
8
+
9
+
-`rows?: number` (positive integer) on the multiline editor types `textarea` / `markdown` / `html` / `richtext`. objectui's `RichTextField` — the one widget behind the `markdown`/`html`/`richtext` registry keys — reads `richField?.rows || 8`, and `TextAreaField` reads `textareaField?.rows || 4`, sizing the inline (non-fullscreen) editor surface. The ruled pair is `markdown`/`html` (aligning the `TextareaFieldMetadata` precedent); `textarea`/`richtext` complete the measured consumption set. A superRefine refuses the key on every other type (the #11566 template), and the house count discipline refuses `0` / negative / fractional values. Both authoring forms show the key for exactly this set. The ruling's capability expansion STOPS here: the four inert rich-text editor keys (`toolbar`/`preview`/`minHeight`/`maxHeight`) stay undeclared, and a pin holds that door shut.
10
+
-`description?: string` on `SelectOptionSchema`. objectui's `LookupField` searches it on a lookup's authored static options (`opt.description && opt.description.toLowerCase().includes(q)`) and its `recordToOption` produces the same key for fetched options — and the object-definition authoring form has offered a `description` input all along; the declaration makes both honest. It flows into `FormSelectOptionSchema` by the #12868 Omit construction. Per the same inherited ruling, `dependsOn` is deliberately NOT declared — the canonical field-level `depends_on` already exists, and the widget-side spelling fix rides the objectui half.
11
+
12
+
Additive in both cases: no stored shape changes, and every previously-written body carrying either key was refused at parse, so nothing legal changes meaning.
Copy file name to clipboardExpand all lines: content/docs/permissions/system-context.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
@@ -196,7 +196,7 @@ assuming `isSystem` covers it is a documented source of bugs.
196
196
| "It suppresses triggers / record-change automation" |**No.** Only `skipTriggers` does. A bare `{ isSystem: true }` on a seed write re-fired automation on freshly seeded rows and wedged first boot |`metadata-protocol/src/seed-loader.ts:1909` (rationale at `:1819`–`1821`, #3760), `flow.zod.ts:685`|
197
197
| "It skips the state machine" |**No.** That is `skipStateMachine`, carried by seed replay and by `treatAsHistorical` imports |`objectql/src/engine.ts` FSM gate; see [State Machine](/docs/protocol/objectql/state-machine)|
198
198
| "It skips validation rules" |**No.** Field shape, `format`, `script` and the rest still run. The `readonly` strip runs *before* validation precisely so a discarded value is not judged |`objectql/src/engine.ts:9588`–`9605`|
199
-
| "It preserves a supplied `updated_at` / `updated_by`" |**No.** That is `preserveAudit`, a separate opt-in — and an UPDATE-path exemption only |`field.zod.ts:1451` (#3493 / #6640) |
199
+
| "It preserves a supplied `updated_at` / `updated_by`" |**No.** That is `preserveAudit`, a separate opt-in — and an UPDATE-path exemption only |`field.zod.ts:1514` (#3493 / #6640) |
200
200
| "It stamps `created_by`" |**No.** Audit stamping reads `userId` from the context. A user-less system write stamps nothing — that is today's behaviour, not an error |`runtime-identity.ts:280`–`281`|
201
201
| "It bypasses every guard" |**No.** The last-admin guard applies to **every** context, `isSystem` included — the deprovision path that actually locks an org out is the system one |`last-admin-guard.ts:286`|
202
202
| "A client can request it" |**No.** Never settable from inbound HTTP or from an action body |`rest-server.ts:1240`, `:1269`; `domains/actions.ts:404`|
Copy file name to clipboardExpand all lines: content/docs/references/data/field.mdx
+4-1Lines changed: 4 additions & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -65,14 +65,15 @@ const result = CurrencyConfigSchema.parse(data);
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
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
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. |
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. |
68
69
|**precision**|`integer`| optional | Total digits (non-negative integer) |
|**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. |
73
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. |
74
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. |
|**reference**|`string`| optional | Target object name (snake_case) for lookup/master_detail fields. Required for relationship types. Used by $expand to resolve foreign key IDs into full objects. |
77
78
|**referenceVia**|`string`| optional | Declares this text field as the id half of a polymorphic pointer pair (ADR-0052 §5 ActivityPointer): the value is a record id of the object named by the SIBLING FIELD this key names — e.g. `record_id` with `referenceVia: 'object_name'`. The sibling must be a declared field on the same object holding an object machine name. Text fields only; mutually exclusive with `reference` (a static and a per-record target contradict). Enforced today at seed load: the value resolves as a natural key against the object the sibling column names, and an unresolvable pointer is refused loudly instead of stored verbatim. Adds no referential integrity or $expand behavior. |
78
79
|**deleteBehavior**|`Enum<'set_null' \| 'cascade' \| 'restrict'>`| optional (default: `"set_null"`) | What happens if referenced record is deleted |
@@ -189,6 +190,7 @@ const result = CurrencyConfigSchema.parse(data);
189
190
| :--- | :--- | :--- | :--- |
190
191
|**label**|`string`| ✅ | Display label (human-readable, any case allowed) |
191
192
|**value**|`string`| ✅ | Stored value (lowercase machine identifier) |
193
+
|**description**|`string`| optional | Optional secondary/help text for this option. Lookup option search matches it in addition to the label; renderers may show it as supporting text. |
192
194
|**color**|`string`| optional | Color code for badges/charts |
193
195
|**default**|`boolean`| optional | Is default option |
194
196
|**visibleWhen**|`string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }`| optional | Per-option visibility predicate (CEL) — option is offered only when TRUE (else omitted). Env: the live `record` plus the host predicate scope, which binds `current_user`. The one VISIBILITY predicate the SERVER also enforces — the rule validator refuses a write of a value whose predicate is false — so a user-gated CHOICE belongs here. e.g. P`record.country == 'cn'` or P`'admin' in current_user.positions`|
|**label**|`string`| ✅ | Display label (human-readable, any case allowed) |
402
404
|**value**|`string`| ✅ | Stored value (lowercase machine identifier) |
405
+
|**description**|`string`| optional | Optional secondary/help text for this option. Lookup option search matches it in addition to the label; renderers may show it as supporting text. |
403
406
|**color**|`string`| optional | Color code for badges/charts |
404
407
|**default**|`boolean`| optional | Is default option |
405
408
|**visibleWhen**|`string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }`| optional | Per-option visibility predicate (CEL) — option is offered only when TRUE (else omitted). Env: the live `record` plus the host predicate scope, which binds `current_user`. The one VISIBILITY predicate the SERVER also enforces — the rule validator refuses a write of a value whose predicate is false — so a user-gated CHOICE belongs here. e.g. P`record.country == 'cn'` or P`'admin' in current_user.positions`|
Copy file name to clipboardExpand all lines: content/docs/references/data/object.mdx
+4-2Lines changed: 4 additions & 2 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -227,14 +227,15 @@ const result = ApiMethod.parse(data);
227
227
|**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. |
228
228
|**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. |
229
229
|**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. |
230
+
|**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. |
230
231
|**precision**|`integer`| optional | Total digits (non-negative integer) |
|**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. |
235
236
|**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. |
236
237
|**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. |
|**reference**|`string`| optional | Target object name (snake_case) for lookup/master_detail fields. Required for relationship types. Used by $expand to resolve foreign key IDs into full objects. |
239
240
|**referenceVia**|`string`| optional | Declares this text field as the id half of a polymorphic pointer pair (ADR-0052 §5 ActivityPointer): the value is a record id of the object named by the SIBLING FIELD this key names — e.g. `record_id` with `referenceVia: 'object_name'`. The sibling must be a declared field on the same object holding an object machine name. Text fields only; mutually exclusive with `reference` (a static and a per-record target contradict). Enforced today at seed load: the value resolves as a natural key against the object the sibling column names, and an unresolvable pointer is refused loudly instead of stored verbatim. Adds no referential integrity or $expand behavior. |
240
241
|**deleteBehavior**|`Enum<'set_null' \| 'cascade' \| 'restrict'>`| optional (default: `"set_null"`) | What happens if referenced record is deleted |
@@ -555,14 +556,15 @@ const result = ApiMethod.parse(data);
555
556
|**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. |
556
557
|**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. |
557
558
|**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. |
559
+
|**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. |
558
560
|**precision**|`integer`| optional | Total digits (non-negative integer) |
|**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. |
563
565
|**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. |
564
566
|**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. |
|**reference**|`string`| optional | Target object name (snake_case) for lookup/master_detail fields. Required for relationship types. Used by $expand to resolve foreign key IDs into full objects. |
567
569
|**referenceVia**|`string`| optional | Declares this text field as the id half of a polymorphic pointer pair (ADR-0052 §5 ActivityPointer): the value is a record id of the object named by the SIBLING FIELD this key names — e.g. `record_id` with `referenceVia: 'object_name'`. The sibling must be a declared field on the same object holding an object machine name. Text fields only; mutually exclusive with `reference` (a static and a per-record target contradict). Enforced today at seed load: the value resolves as a natural key against the object the sibling column names, and an unresolvable pointer is refused loudly instead of stored verbatim. Adds no referential integrity or $expand behavior. |
568
570
|**deleteBehavior**|`Enum<'set_null' \| 'cascade' \| 'restrict'>`| optional (default: `"set_null"`) | What happens if referenced record is deleted |
0 commit comments