Skip to content

Commit 86e765a

Browse files
os-warrenclaude
andauthored
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>
1 parent 00d8f65 commit 86e765a

23 files changed

Lines changed: 404 additions & 19 deletions
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
"@objectstack/spec": minor
3+
---
4+
5+
**`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.

content/docs/permissions/system-context.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -196,7 +196,7 @@ assuming `isSystem` covers it is a documented source of bugs.
196196
| "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` |
197197
| "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) |
198198
| "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) |
200200
| "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` |
201201
| "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` |
202202
| "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` |

content/docs/references/data/field.mdx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,14 +65,15 @@ const result = CurrencyConfigSchema.parse(data);
6565
| **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. |
6666
| **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. |
6767
| **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. |
6869
| **precision** | `integer` | optional | Total digits (non-negative integer) |
6970
| **scale** | `integer` | optional | Decimal places (non-negative integer) |
7071
| **min** | `number` | optional | Minimum value |
7172
| **max** | `number` | optional | Maximum value |
7273
| **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. |
7374
| **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. |
7475
| **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. |
75-
| **options** | `{ label: string; value: string; color?: string; default?: boolean; … }[]` | optional | Static options for select/multiselect |
76+
| **options** | `{ label: string; value: string; description?: string; color?: string; … }[]` | optional | Static options for select/multiselect |
7677
| **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. |
7778
| **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. |
7879
| **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);
189190
| :--- | :--- | :--- | :--- |
190191
| **label** | `string` || Display label (human-readable, any case allowed) |
191192
| **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. |
192194
| **color** | `string` | optional | Color code for badges/charts |
193195
| **default** | `boolean` | optional | Is default option |
194196
| **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` |
@@ -400,6 +402,7 @@ Allowed Values: `phone`, `id_card`, `bank_account`, `email`, `name`
400402
| :--- | :--- | :--- | :--- |
401403
| **label** | `string` || Display label (human-readable, any case allowed) |
402404
| **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. |
403406
| **color** | `string` | optional | Color code for badges/charts |
404407
| **default** | `boolean` | optional | Is default option |
405408
| **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` |

content/docs/references/data/object.mdx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -227,14 +227,15 @@ const result = ApiMethod.parse(data);
227227
| **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. |
228228
| **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. |
229229
| **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. |
230231
| **precision** | `integer` | optional | Total digits (non-negative integer) |
231232
| **scale** | `integer` | optional | Decimal places (non-negative integer) |
232233
| **min** | `number` | optional | Minimum value |
233234
| **max** | `number` | optional | Maximum value |
234235
| **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. |
235236
| **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. |
236237
| **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. |
237-
| **options** | `{ label: string; value: string; color?: string; default?: boolean; … }[]` | optional | Static options for select/multiselect |
238+
| **options** | `{ label: string; value: string; description?: string; color?: string; … }[]` | optional | Static options for select/multiselect |
238239
| **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. |
239240
| **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. |
240241
| **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);
555556
| **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. |
556557
| **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. |
557558
| **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. |
558560
| **precision** | `integer` | optional | Total digits (non-negative integer) |
559561
| **scale** | `integer` | optional | Decimal places (non-negative integer) |
560562
| **min** | `number` | optional | Minimum value |
561563
| **max** | `number` | optional | Maximum value |
562564
| **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. |
563565
| **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. |
564566
| **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. |
565-
| **options** | `{ label: string; value: string; color?: string; default?: boolean; … }[]` | optional | Static options for select/multiselect |
567+
| **options** | `{ label: string; value: string; description?: string; color?: string; … }[]` | optional | Static options for select/multiselect |
566568
| **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. |
567569
| **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. |
568570
| **deleteBehavior** | `Enum<'set_null' \| 'cascade' \| 'restrict'>` | optional (default: `"set_null"`) | What happens if referenced record is deleted |

0 commit comments

Comments
 (0)