From a566c0cbd87a742a15e41ec0d2c76e041705c460 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 3 Nov 2026 01:50:42 +0000 Subject: [PATCH 1/3] docs(spec): a cleared optional multi-value field is stored as null Amend the FieldSchema `multiple` and `required` doc blocks and the `multiple` describe to the ruled empty representation: a cleared optional multi-value field stores null at the write door, `[]` and `''` store null, `required` still means a non-empty list, and the `$empty` table is unchanged. Claude-Session: https://claude.ai/code/session_01WGufzeW5cprrZRccVNgPFr Co-authored-by: Claude --- .../29652-multi-value-empty-null-docs.md | 15 ++++++++ packages/spec/src/data/field.zod.ts | 37 ++++++++++++------- .../spec/src/data/filter-empty-operator.ts | 3 +- 3 files changed, 40 insertions(+), 15 deletions(-) create mode 100644 .changeset/29652-multi-value-empty-null-docs.md diff --git a/.changeset/29652-multi-value-empty-null-docs.md b/.changeset/29652-multi-value-empty-null-docs.md new file mode 100644 index 00000000000..0d1b59d4f28 --- /dev/null +++ b/.changeset/29652-multi-value-empty-null-docs.md @@ -0,0 +1,15 @@ +--- +'@objectstack/spec': minor +--- + +docs(spec): a cleared optional multi-value field is stored as `null`, the same as one never set; `FieldSchema`'s `multiple` and `required` docs now say so + +Clause-②: yes +Metadata-protocol: semantic + +`FieldSchema.multiple` documented that an emptied multi-value lookup reads back as `[]`, never `null`, for every writer, and `required` leaned on that. The rule is now the other way round for optional columns. + +- **Now.** A cleared optional multi-value field (`multiselect`, `checkboxes`, `tags`, and `select`, `lookup`, `user`, `file` or `image` with `multiple: true`) is stored as `null` at the engine's write door, the same as a field never set, whether or not a unique constraint covers it. `[]` and `''` written to such a column store `null`. The rule binds every writer: cascade repair, form clears and API writes. +- **Reading.** Treat `null` and `[]` alike as empty. The `is_empty` filter (`$empty`) already does, and its table is unchanged. A row stored before this rule can still hold `[]`. +- **Unchanged.** `required` on a multi-value field still means a non-empty list: `[]` is refused, as is `null`. +- **This change is documentation.** The `multiple` and `required` descriptions (and the generated field reference) state the rule. The write-door change that applies it to `[]` and `''` ships separately, and stored `[]` values are cleaned by a separate `os migrate` data step. diff --git a/packages/spec/src/data/field.zod.ts b/packages/spec/src/data/field.zod.ts index ccb34347a02..893563d5d2b 100644 --- a/packages/spec/src/data/field.zod.ts +++ b/packages/spec/src/data/field.zod.ts @@ -1313,11 +1313,12 @@ export const FieldSchema = lazySchema(() => { * * On a multi-value lookup (`multiple: true`), `required` means NON-EMPTY * array: an emptied required set fails validation loudly — `[]` does not - * satisfy `required` (#9447, maintainer ruling 2026-08-18). The empty set is - * always representable (it reads back as `[]`, never `null` — see - * `multiple`), so the required check judges emptiness, not absence. The - * describe is form help in Studio and carries the rule, not the ruling date - * (#22093). + * satisfy `required` (#9447, maintainer ruling 2026-08-18; this half stands + * under ruling A on #29343, record 6591577985). An emptied OPTIONAL + * multi-value field is stored as `null`, the same as one never set (see + * `multiple`), so the required check judges emptiness: `null` and `[]` both + * fail it. The describe is form help in Studio and carries the rule, not the + * ruling date (#22093). */ required: z.boolean().default(false).describe('Write-time contract (ADR-0113): an insert must provide a non-null value, and an update may not null it out. On a multi-value lookup (`multiple: true`) required means NON-EMPTY array — an emptied required set fails validation loudly; `[]` does not satisfy it. NOT a column constraint — the physical NOT NULL is a separate explicit opt-in (`storage.notNull`), so tightening this on a deployed object is safe: existing null rows stay readable, and editable as long as the write does not touch this field.'), @@ -1354,21 +1355,29 @@ export const FieldSchema = lazySchema(() => { + 'is the whole search set and this flag is not read (ADR-0061).', ), /** - * Multi-value empty representation (#9447, maintainer ruling 2026-08-18): - * an emptied multi-value lookup reads back as `[]`, never `null`. This - * binds the field's empty representation for EVERY writer — cascade repair - * (`set_null` member removal), form clears, API writes — not as a - * cascade-only convention: an array field always reads as an array, so - * readers (generated code, formula/filter predicates) never need a null - * branch. Same ruling: `required` on a multi-value lookup means non-empty - * array (see `required` above). + * Multi-value empty representation (ruling A on #29343, record 6591577985, + * amending #9447, maintainer ruling 2026-08-18): a cleared OPTIONAL + * multi-value field — every type `isMultiValueField` answers true for: + * multiselect, checkboxes, tags, and select / lookup / user / file / image + * with `multiple: true` — is stored as `null` by the write door, the same as + * one never set, unique-covered or not: `[]` and `''` written to it store + * `null`. This binds the field's empty representation for EVERY writer — + * cascade repair (`set_null` member removal), form clears, API writes — not + * as a cascade-only convention. It reverses #9447's "an emptied multi-value + * lookup reads back as `[]`, never `null`" for optional columns: readers + * (generated code, formula/filter predicates) meet `null` for an empty set, + * and read `null` and `[]` alike as empty, since a row stored before the + * rule can still hold `[]`. The `$empty` table's `multi_value` row + * (`filter-empty-operator.ts`) is unchanged: `null` and `[]` both read as + * empty. #9447's other half stands: `required` on a multi-value field means + * non-empty list (see `required` above). * * Declarability: `multiple: true` outside the multi-capable types is refused * at parse (maintainer ruling 2026-09-13), and on `radio` by the narrower * 2026-08-22 ruling. The rulings and their dates live in these comments; the * describe is form help in Studio and carries only the rule (#22093). */ - multiple: z.boolean().default(false).describe('Allow multiple values (Stores as Array/JSON). Declarable ONLY on the multi-capable types — select, lookup, user, file, image — and redundantly on the inherently-multi option types (multiselect, checkboxes, tags); `multiple: true` on any other type, `radio` included, is REFUSED at parse. 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.'), + multiple: z.boolean().default(false).describe('Allow multiple values (Stores as Array/JSON). Declarable ONLY on the multi-capable types — select, lookup, user, file, image — and redundantly on the inherently-multi option types (multiselect, checkboxes, tags); `multiple: true` on any other type, `radio` included, is REFUSED at parse. A cleared optional multi-value field is stored as `null`, the same as one never set: an empty list `[]` or an empty string written to it stores `null`, and the rule binds every writer (cascade repair, form clears, API writes). Read `null` and `[]` alike as empty, as the `is_empty` filter (`$empty`) does, since a row stored before this rule can still hold `[]`. On a `required` multi-value field the empty list is refused: `required` means a non-empty list.'), // `true` = unique WITHIN the tenant on a tenant-scoped object (composite // `(tenantField, field)` index); `'global'` = platform-wide single-column // unique. See {@link UniqueScopeSchema} for the scope vocabulary (ADR-0120). diff --git a/packages/spec/src/data/filter-empty-operator.ts b/packages/spec/src/data/filter-empty-operator.ts index 786dcfd4173..c3275536b4a 100644 --- a/packages/spec/src/data/filter-empty-operator.ts +++ b/packages/spec/src/data/filter-empty-operator.ts @@ -88,7 +88,8 @@ export const EMPTY_OPERATOR_ARMS: Readonly Date: Tue, 3 Nov 2026 01:54:04 +0000 Subject: [PATCH 2/3] docs(spec): regenerate the field references for the multiple describe Claude-Session: https://claude.ai/code/session_01WGufzeW5cprrZRccVNgPFr Co-authored-by: Claude --- content/docs/references/data/field.mdx | 2 +- content/docs/references/data/object.mdx | 4 ++-- content/docs/references/system/migration.mdx | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/content/docs/references/data/field.mdx b/content/docs/references/data/field.mdx index 5662c123c50..6508cb02e68 100644 --- a/content/docs/references/data/field.mdx +++ b/content/docs/references/data/field.mdx @@ -58,7 +58,7 @@ const result = CurrencyConfigSchema.parse(data); | **required** | `boolean` | optional (default: `false`) | Write-time contract (ADR-0113): an insert must provide a non-null value, and an update may not null it out. On a multi-value lookup (`multiple: true`) required means NON-EMPTY array — an emptied required set fails validation loudly; `[]` does not satisfy it. NOT a column constraint — the physical NOT NULL is a separate explicit opt-in (`storage.notNull`), so tightening this on a deployed object is safe: existing null rows stay readable, and editable as long as the write does not touch this field. | | **storage** | `{ notNull?: boolean }` | optional | Physical storage constraints (ADR-0113). Owns the DDL the write contract deliberately does not imply. Absent = no storage-level constraint requested. | | **searchable** | `boolean` | optional (default: `false`) | Search this field when its object declares no `searchableFields`. The set `search` scans, and the set a `$searchFields` override may name, is then the type-based default (the text-like and select columns) plus every field marked `searchable: true`, of any type except `secret`, `password` and `vector`, which stay excluded (a virtual `formula` field has no stored column, so it is never scanned either). `false`, the default, does not remove a field the type-based default already scans; to choose the set, declare `searchableFields`. When the object declares `searchableFields`, that list is the whole search set and this flag is not read (ADR-0061). | -| **multiple** | `boolean` | optional (default: `false`) | Allow multiple values (Stores as Array/JSON). Declarable ONLY on the multi-capable types — select, lookup, user, file, image — and redundantly on the inherently-multi option types (multiselect, checkboxes, tags); `multiple: true` on any other type, `radio` included, is REFUSED at parse. 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. | +| **multiple** | `boolean` | optional (default: `false`) | Allow multiple values (Stores as Array/JSON). Declarable ONLY on the multi-capable types — select, lookup, user, file, image — and redundantly on the inherently-multi option types (multiselect, checkboxes, tags); `multiple: true` on any other type, `radio` included, is REFUSED at parse. A cleared optional multi-value field is stored as `null`, the same as one never set: an empty list `[]` or an empty string written to it stores `null`, and the rule binds every writer (cascade repair, form clears, API writes). Read `null` and `[]` alike as empty, as the `is_empty` filter (`$empty`) does, since a row stored before this rule can still hold `[]`. On a `required` multi-value field the empty list is refused: `required` means a non-empty list. | | **unique** | `boolean \| 'global' \| 'organization'` | optional | 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'. Omitted ⇒ false, EXCEPT on an `autonumber` field, where omitted ⇒ 'organization' (an auto-number is a business identifier, so the platform makes it unique per organization by default — the same tenant-composite shape an explicit `unique: true` produces). To opt an autonumber field out, write `unique: false` explicitly — legitimate only for a display-only sequence that is not used to identify the record; the platform's duplicate scan (`os migrate duplicates`) still treats every autonumber field as an identifier. | | **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. | | **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. | diff --git a/content/docs/references/data/object.mdx b/content/docs/references/data/object.mdx index ba9b37ba982..951db85eb3c 100644 --- a/content/docs/references/data/object.mdx +++ b/content/docs/references/data/object.mdx @@ -227,7 +227,7 @@ const result = ApiMethod.parse(data); | **required** | `boolean` | optional (default: `false`) | Write-time contract (ADR-0113): an insert must provide a non-null value, and an update may not null it out. On a multi-value lookup (`multiple: true`) required means NON-EMPTY array — an emptied required set fails validation loudly; `[]` does not satisfy it. NOT a column constraint — the physical NOT NULL is a separate explicit opt-in (`storage.notNull`), so tightening this on a deployed object is safe: existing null rows stay readable, and editable as long as the write does not touch this field. | | **storage** | `{ notNull?: boolean }` | optional | Physical storage constraints (ADR-0113). Owns the DDL the write contract deliberately does not imply. Absent = no storage-level constraint requested. | | **searchable** | `boolean` | optional (default: `false`) | Search this field when its object declares no `searchableFields`. The set `search` scans, and the set a `$searchFields` override may name, is then the type-based default (the text-like and select columns) plus every field marked `searchable: true`, of any type except `secret`, `password` and `vector`, which stay excluded (a virtual `formula` field has no stored column, so it is never scanned either). `false`, the default, does not remove a field the type-based default already scans; to choose the set, declare `searchableFields`. When the object declares `searchableFields`, that list is the whole search set and this flag is not read (ADR-0061). | -| **multiple** | `boolean` | optional (default: `false`) | Allow multiple values (Stores as Array/JSON). Declarable ONLY on the multi-capable types — select, lookup, user, file, image — and redundantly on the inherently-multi option types (multiselect, checkboxes, tags); `multiple: true` on any other type, `radio` included, is REFUSED at parse. 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. | +| **multiple** | `boolean` | optional (default: `false`) | Allow multiple values (Stores as Array/JSON). Declarable ONLY on the multi-capable types — select, lookup, user, file, image — and redundantly on the inherently-multi option types (multiselect, checkboxes, tags); `multiple: true` on any other type, `radio` included, is REFUSED at parse. A cleared optional multi-value field is stored as `null`, the same as one never set: an empty list `[]` or an empty string written to it stores `null`, and the rule binds every writer (cascade repair, form clears, API writes). Read `null` and `[]` alike as empty, as the `is_empty` filter (`$empty`) does, since a row stored before this rule can still hold `[]`. On a `required` multi-value field the empty list is refused: `required` means a non-empty list. | | **unique** | `boolean \| 'global' \| 'organization'` | optional | 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'. Omitted ⇒ false, EXCEPT on an `autonumber` field, where omitted ⇒ 'organization' (an auto-number is a business identifier, so the platform makes it unique per organization by default — the same tenant-composite shape an explicit `unique: true` produces). To opt an autonumber field out, write `unique: false` explicitly — legitimate only for a display-only sequence that is not used to identify the record; the platform's duplicate scan (`os migrate duplicates`) still treats every autonumber field as an identifier. | | **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. | | **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. | @@ -598,7 +598,7 @@ const result = ApiMethod.parse(data); | **required** | `boolean` | optional (default: `false`) | Write-time contract (ADR-0113): an insert must provide a non-null value, and an update may not null it out. On a multi-value lookup (`multiple: true`) required means NON-EMPTY array — an emptied required set fails validation loudly; `[]` does not satisfy it. NOT a column constraint — the physical NOT NULL is a separate explicit opt-in (`storage.notNull`), so tightening this on a deployed object is safe: existing null rows stay readable, and editable as long as the write does not touch this field. | | **storage** | `{ notNull?: boolean }` | optional | Physical storage constraints (ADR-0113). Owns the DDL the write contract deliberately does not imply. Absent = no storage-level constraint requested. | | **searchable** | `boolean` | optional (default: `false`) | Search this field when its object declares no `searchableFields`. The set `search` scans, and the set a `$searchFields` override may name, is then the type-based default (the text-like and select columns) plus every field marked `searchable: true`, of any type except `secret`, `password` and `vector`, which stay excluded (a virtual `formula` field has no stored column, so it is never scanned either). `false`, the default, does not remove a field the type-based default already scans; to choose the set, declare `searchableFields`. When the object declares `searchableFields`, that list is the whole search set and this flag is not read (ADR-0061). | -| **multiple** | `boolean` | optional (default: `false`) | Allow multiple values (Stores as Array/JSON). Declarable ONLY on the multi-capable types — select, lookup, user, file, image — and redundantly on the inherently-multi option types (multiselect, checkboxes, tags); `multiple: true` on any other type, `radio` included, is REFUSED at parse. 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. | +| **multiple** | `boolean` | optional (default: `false`) | Allow multiple values (Stores as Array/JSON). Declarable ONLY on the multi-capable types — select, lookup, user, file, image — and redundantly on the inherently-multi option types (multiselect, checkboxes, tags); `multiple: true` on any other type, `radio` included, is REFUSED at parse. A cleared optional multi-value field is stored as `null`, the same as one never set: an empty list `[]` or an empty string written to it stores `null`, and the rule binds every writer (cascade repair, form clears, API writes). Read `null` and `[]` alike as empty, as the `is_empty` filter (`$empty`) does, since a row stored before this rule can still hold `[]`. On a `required` multi-value field the empty list is refused: `required` means a non-empty list. | | **unique** | `boolean \| 'global' \| 'organization'` | optional | 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'. Omitted ⇒ false, EXCEPT on an `autonumber` field, where omitted ⇒ 'organization' (an auto-number is a business identifier, so the platform makes it unique per organization by default — the same tenant-composite shape an explicit `unique: true` produces). To opt an autonumber field out, write `unique: false` explicitly — legitimate only for a display-only sequence that is not used to identify the record; the platform's duplicate scan (`os migrate duplicates`) still treats every autonumber field as an identifier. | | **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. | | **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. | diff --git a/content/docs/references/system/migration.mdx b/content/docs/references/system/migration.mdx index a31b01998f1..55f2d11dd6e 100644 --- a/content/docs/references/system/migration.mdx +++ b/content/docs/references/system/migration.mdx @@ -61,7 +61,7 @@ Add a new field to an existing object | **required** | `boolean` | optional (default: `false`) | Write-time contract (ADR-0113): an insert must provide a non-null value, and an update may not null it out. On a multi-value lookup (`multiple: true`) required means NON-EMPTY array — an emptied required set fails validation loudly; `[]` does not satisfy it. NOT a column constraint — the physical NOT NULL is a separate explicit opt-in (`storage.notNull`), so tightening this on a deployed object is safe: existing null rows stay readable, and editable as long as the write does not touch this field. | | **storage** | `{ notNull?: boolean }` | optional | Physical storage constraints (ADR-0113). Owns the DDL the write contract deliberately does not imply. Absent = no storage-level constraint requested. | | **searchable** | `boolean` | optional (default: `false`) | Search this field when its object declares no `searchableFields`. The set `search` scans, and the set a `$searchFields` override may name, is then the type-based default (the text-like and select columns) plus every field marked `searchable: true`, of any type except `secret`, `password` and `vector`, which stay excluded (a virtual `formula` field has no stored column, so it is never scanned either). `false`, the default, does not remove a field the type-based default already scans; to choose the set, declare `searchableFields`. When the object declares `searchableFields`, that list is the whole search set and this flag is not read (ADR-0061). | -| **multiple** | `boolean` | optional (default: `false`) | Allow multiple values (Stores as Array/JSON). Declarable ONLY on the multi-capable types — select, lookup, user, file, image — and redundantly on the inherently-multi option types (multiselect, checkboxes, tags); `multiple: true` on any other type, `radio` included, is REFUSED at parse. 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. | +| **multiple** | `boolean` | optional (default: `false`) | Allow multiple values (Stores as Array/JSON). Declarable ONLY on the multi-capable types — select, lookup, user, file, image — and redundantly on the inherently-multi option types (multiselect, checkboxes, tags); `multiple: true` on any other type, `radio` included, is REFUSED at parse. A cleared optional multi-value field is stored as `null`, the same as one never set: an empty list `[]` or an empty string written to it stores `null`, and the rule binds every writer (cascade repair, form clears, API writes). Read `null` and `[]` alike as empty, as the `is_empty` filter (`$empty`) does, since a row stored before this rule can still hold `[]`. On a `required` multi-value field the empty list is refused: `required` means a non-empty list. | | **unique** | `boolean \| 'global' \| 'organization'` | optional | 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'. Omitted ⇒ false, EXCEPT on an `autonumber` field, where omitted ⇒ 'organization' (an auto-number is a business identifier, so the platform makes it unique per organization by default — the same tenant-composite shape an explicit `unique: true` produces). To opt an autonumber field out, write `unique: false` explicitly — legitimate only for a display-only sequence that is not used to identify the record; the platform's duplicate scan (`os migrate duplicates`) still treats every autonumber field as an identifier. | | **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. | | **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. | @@ -495,7 +495,7 @@ Add a new field to an existing object | **required** | `boolean` | optional (default: `false`) | Write-time contract (ADR-0113): an insert must provide a non-null value, and an update may not null it out. On a multi-value lookup (`multiple: true`) required means NON-EMPTY array — an emptied required set fails validation loudly; `[]` does not satisfy it. NOT a column constraint — the physical NOT NULL is a separate explicit opt-in (`storage.notNull`), so tightening this on a deployed object is safe: existing null rows stay readable, and editable as long as the write does not touch this field. | | **storage** | `{ notNull?: boolean }` | optional | Physical storage constraints (ADR-0113). Owns the DDL the write contract deliberately does not imply. Absent = no storage-level constraint requested. | | **searchable** | `boolean` | optional (default: `false`) | Search this field when its object declares no `searchableFields`. The set `search` scans, and the set a `$searchFields` override may name, is then the type-based default (the text-like and select columns) plus every field marked `searchable: true`, of any type except `secret`, `password` and `vector`, which stay excluded (a virtual `formula` field has no stored column, so it is never scanned either). `false`, the default, does not remove a field the type-based default already scans; to choose the set, declare `searchableFields`. When the object declares `searchableFields`, that list is the whole search set and this flag is not read (ADR-0061). | -| **multiple** | `boolean` | optional (default: `false`) | Allow multiple values (Stores as Array/JSON). Declarable ONLY on the multi-capable types — select, lookup, user, file, image — and redundantly on the inherently-multi option types (multiselect, checkboxes, tags); `multiple: true` on any other type, `radio` included, is REFUSED at parse. 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. | +| **multiple** | `boolean` | optional (default: `false`) | Allow multiple values (Stores as Array/JSON). Declarable ONLY on the multi-capable types — select, lookup, user, file, image — and redundantly on the inherently-multi option types (multiselect, checkboxes, tags); `multiple: true` on any other type, `radio` included, is REFUSED at parse. A cleared optional multi-value field is stored as `null`, the same as one never set: an empty list `[]` or an empty string written to it stores `null`, and the rule binds every writer (cascade repair, form clears, API writes). Read `null` and `[]` alike as empty, as the `is_empty` filter (`$empty`) does, since a row stored before this rule can still hold `[]`. On a `required` multi-value field the empty list is refused: `required` means a non-empty list. | | **unique** | `boolean \| 'global' \| 'organization'` | optional | 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'. Omitted ⇒ false, EXCEPT on an `autonumber` field, where omitted ⇒ 'organization' (an auto-number is a business identifier, so the platform makes it unique per organization by default — the same tenant-composite shape an explicit `unique: true` produces). To opt an autonumber field out, write `unique: false` explicitly — legitimate only for a display-only sequence that is not used to identify the record; the platform's duplicate scan (`os migrate duplicates`) still treats every autonumber field as an identifier. | | **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. | | **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. | From 3e61ed737b30c8d0dbea4cf7b0a0fa5b9e335926 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 3 Nov 2026 03:11:59 +0000 Subject: [PATCH 3/3] docs(spec): regenerate the field references after merging main Claude-Session: https://claude.ai/code/session_01WGufzeW5cprrZRccVNgPFr Co-authored-by: Claude --- content/docs/references/data/field.mdx | 2 +- content/docs/references/data/object.mdx | 4 ++-- content/docs/references/system/migration.mdx | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/content/docs/references/data/field.mdx b/content/docs/references/data/field.mdx index 6508cb02e68..4af0c555022 100644 --- a/content/docs/references/data/field.mdx +++ b/content/docs/references/data/field.mdx @@ -78,7 +78,7 @@ const result = CurrencyConfigSchema.parse(data); | **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. Never a stored-metadata table (`sys_metadata`, `sys_metadata_history`): their rows are served only through the metadata API, so the engine never hydrates a reference into one, and `os validate`, `os build` and the object save door refuse it. On a `tree` field it is optional and, if given, must be the declaring object's own name — the object schema refuses any other target, and writes the own name when it is left out. | | **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. | | **referenceDeletedWhen** | `{ field: string; value: string }` | optional | Marks the record a `referenceVia` pointer names as PLATFORM-DELETED on rows where a sibling `select` field holds one declared option — e.g. `record_id` with `{ field: 'cancel_reason', value: 'record_deleted' }`. A renderer then draws a tombstone from that option's own label, reads nothing and links nothing. Valid only beside `referenceVia`; `field` must be a single-value `select` on the same object declaring inline `options`, and `value` one of its option values — any other shape is refused at parse. | -| **referenceNameVisible** | `boolean` | optional | Declared name exposure (`lookup` and `user` fields only). TRUE declares that a principal who can read the record carrying this field may see the referenced record's display name (the target object's `nameField` value) and id even when that principal cannot read the referenced record. Served on `$expand`: a target the reader is refused (the object's read gate or its row scope) answers `{ id, name, readRefused: true }`, per element on a multi-value field; an unexpanded read answers the stored id, as for every reader. Nothing else of the target: `$expand` on this field returns the same name and id only, the field's `displayField` does not widen it, and the name renders as text with no link to the target. A field-level rule on the name field still holds: when the reader may not read that field, it is masked, or it is a formula reading through another reference, the name is the `Record #` floor with the id. A principal who can read the target is served exactly what it is served without the key. Absent or false keeps the restricted-reference posture for a target the reader cannot read. Declared on the referencing field only; there is no target-object switch. Refused on any other type. | +| **referenceNameVisible** | `boolean` | optional | Declared name exposure (`lookup` and `user` fields only). TRUE declares that a principal who can read the record carrying this field may see the referenced record's display name (the target object's `nameField`, else its `displayNameField`, else the derived title field) and id even when that principal cannot read the referenced record. Served on `$expand`: a target the reader is refused (the object's read gate or its row scope) answers `{ id, name, readRefused: true }` (`ReferenceNameOnlyValueSchema`), per element on a multi-value field; an unexpanded read answers the stored id, as for every reader. Nothing else of the target: `$expand` on this field returns the same name and id only, the field's `displayField` does not widen it, and the name renders as text with no link to the target. A field-level rule on the name field still holds, and the name field is read only where nothing the security layer owns must be computed: the name is the `Record #` floor with the id when the reader may not read that field, it declares a `maskingRule`, it is a formula reading through another reference, an explicit `nameField` (or its `displayNameField` alias) points at a column that is neither title-typed nor autonumber, or no field-level answer can be had for the reader. A target with no resolvable or no non-empty name reads as the same floor. A principal who can read the target is served exactly what it is served without the key. Absent or false keeps the restricted-reference posture for a target the reader cannot read. Declared on the referencing field only; there is no target-object switch. Refused on any other type. | | **mailAssociation** | `boolean` | optional | Inbound mail association (`email` fields only): TRUE marks this field as identifying a correspondent. When a mailbox service ingests a message, it associates the message with every record of this object whose value in this field equals one of the message's participant addresses (From, Sender, Reply-To, To and Cc, plus those of a forwarded message it carries; ASCII case-insensitive; the receiving mailbox's own addresses excluded; only within the mailbox's organization). A message no record matches waits in the mailbox's unassociated pool. Refused on any other type. Takes effect only where a mailbox service is composed. | | **deleteBehavior** | `Enum<'set_null' \| 'cascade' \| 'restrict'>` | optional (default: `"set_null"`) | What happens if referenced record is deleted | | **inlineEdit** | `boolean \| Enum<'grid' \| 'form'>` | optional | Edit these child records inline within the parent's form (atomic master-detail). true = auto-pick grid/form by child shape; 'grid' = editable line-item grid; 'form' = list + per-row full form. | diff --git a/content/docs/references/data/object.mdx b/content/docs/references/data/object.mdx index 951db85eb3c..2a18f01b3d7 100644 --- a/content/docs/references/data/object.mdx +++ b/content/docs/references/data/object.mdx @@ -247,7 +247,7 @@ const result = ApiMethod.parse(data); | **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. Never a stored-metadata table (`sys_metadata`, `sys_metadata_history`): their rows are served only through the metadata API, so the engine never hydrates a reference into one, and `os validate`, `os build` and the object save door refuse it. On a `tree` field it is optional and, if given, must be the declaring object's own name — the object schema refuses any other target, and writes the own name when it is left out. | | **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. | | **referenceDeletedWhen** | `{ field: string; value: string }` | optional | Marks the record a `referenceVia` pointer names as PLATFORM-DELETED on rows where a sibling `select` field holds one declared option — e.g. `record_id` with `{ field: 'cancel_reason', value: 'record_deleted' }`. A renderer then draws a tombstone from that option's own label, reads nothing and links nothing. Valid only beside `referenceVia`; `field` must be a single-value `select` on the same object declaring inline `options`, and `value` one of its option values — any other shape is refused at parse. | -| **referenceNameVisible** | `boolean` | optional | Declared name exposure (`lookup` and `user` fields only). TRUE declares that a principal who can read the record carrying this field may see the referenced record's display name (the target object's `nameField` value) and id even when that principal cannot read the referenced record. Served on `$expand`: a target the reader is refused (the object's read gate or its row scope) answers `{ id, name, readRefused: true }`, per element on a multi-value field; an unexpanded read answers the stored id, as for every reader. Nothing else of the target: `$expand` on this field returns the same name and id only, the field's `displayField` does not widen it, and the name renders as text with no link to the target. A field-level rule on the name field still holds: when the reader may not read that field, it is masked, or it is a formula reading through another reference, the name is the `Record #` floor with the id. A principal who can read the target is served exactly what it is served without the key. Absent or false keeps the restricted-reference posture for a target the reader cannot read. Declared on the referencing field only; there is no target-object switch. Refused on any other type. | +| **referenceNameVisible** | `boolean` | optional | Declared name exposure (`lookup` and `user` fields only). TRUE declares that a principal who can read the record carrying this field may see the referenced record's display name (the target object's `nameField`, else its `displayNameField`, else the derived title field) and id even when that principal cannot read the referenced record. Served on `$expand`: a target the reader is refused (the object's read gate or its row scope) answers `{ id, name, readRefused: true }` (`ReferenceNameOnlyValueSchema`), per element on a multi-value field; an unexpanded read answers the stored id, as for every reader. Nothing else of the target: `$expand` on this field returns the same name and id only, the field's `displayField` does not widen it, and the name renders as text with no link to the target. A field-level rule on the name field still holds, and the name field is read only where nothing the security layer owns must be computed: the name is the `Record #` floor with the id when the reader may not read that field, it declares a `maskingRule`, it is a formula reading through another reference, an explicit `nameField` (or its `displayNameField` alias) points at a column that is neither title-typed nor autonumber, or no field-level answer can be had for the reader. A target with no resolvable or no non-empty name reads as the same floor. A principal who can read the target is served exactly what it is served without the key. Absent or false keeps the restricted-reference posture for a target the reader cannot read. Declared on the referencing field only; there is no target-object switch. Refused on any other type. | | **mailAssociation** | `boolean` | optional | Inbound mail association (`email` fields only): TRUE marks this field as identifying a correspondent. When a mailbox service ingests a message, it associates the message with every record of this object whose value in this field equals one of the message's participant addresses (From, Sender, Reply-To, To and Cc, plus those of a forwarded message it carries; ASCII case-insensitive; the receiving mailbox's own addresses excluded; only within the mailbox's organization). A message no record matches waits in the mailbox's unassociated pool. Refused on any other type. Takes effect only where a mailbox service is composed. | | **deleteBehavior** | `Enum<'set_null' \| 'cascade' \| 'restrict'>` | optional (default: `"set_null"`) | What happens if referenced record is deleted | | **inlineEdit** | `boolean \| Enum<'grid' \| 'form'>` | optional | Edit these child records inline within the parent's form (atomic master-detail). true = auto-pick grid/form by child shape; 'grid' = editable line-item grid; 'form' = list + per-row full form. | @@ -618,7 +618,7 @@ const result = ApiMethod.parse(data); | **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. Never a stored-metadata table (`sys_metadata`, `sys_metadata_history`): their rows are served only through the metadata API, so the engine never hydrates a reference into one, and `os validate`, `os build` and the object save door refuse it. On a `tree` field it is optional and, if given, must be the declaring object's own name — the object schema refuses any other target, and writes the own name when it is left out. | | **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. | | **referenceDeletedWhen** | `{ field: string; value: string }` | optional | Marks the record a `referenceVia` pointer names as PLATFORM-DELETED on rows where a sibling `select` field holds one declared option — e.g. `record_id` with `{ field: 'cancel_reason', value: 'record_deleted' }`. A renderer then draws a tombstone from that option's own label, reads nothing and links nothing. Valid only beside `referenceVia`; `field` must be a single-value `select` on the same object declaring inline `options`, and `value` one of its option values — any other shape is refused at parse. | -| **referenceNameVisible** | `boolean` | optional | Declared name exposure (`lookup` and `user` fields only). TRUE declares that a principal who can read the record carrying this field may see the referenced record's display name (the target object's `nameField` value) and id even when that principal cannot read the referenced record. Served on `$expand`: a target the reader is refused (the object's read gate or its row scope) answers `{ id, name, readRefused: true }`, per element on a multi-value field; an unexpanded read answers the stored id, as for every reader. Nothing else of the target: `$expand` on this field returns the same name and id only, the field's `displayField` does not widen it, and the name renders as text with no link to the target. A field-level rule on the name field still holds: when the reader may not read that field, it is masked, or it is a formula reading through another reference, the name is the `Record #` floor with the id. A principal who can read the target is served exactly what it is served without the key. Absent or false keeps the restricted-reference posture for a target the reader cannot read. Declared on the referencing field only; there is no target-object switch. Refused on any other type. | +| **referenceNameVisible** | `boolean` | optional | Declared name exposure (`lookup` and `user` fields only). TRUE declares that a principal who can read the record carrying this field may see the referenced record's display name (the target object's `nameField`, else its `displayNameField`, else the derived title field) and id even when that principal cannot read the referenced record. Served on `$expand`: a target the reader is refused (the object's read gate or its row scope) answers `{ id, name, readRefused: true }` (`ReferenceNameOnlyValueSchema`), per element on a multi-value field; an unexpanded read answers the stored id, as for every reader. Nothing else of the target: `$expand` on this field returns the same name and id only, the field's `displayField` does not widen it, and the name renders as text with no link to the target. A field-level rule on the name field still holds, and the name field is read only where nothing the security layer owns must be computed: the name is the `Record #` floor with the id when the reader may not read that field, it declares a `maskingRule`, it is a formula reading through another reference, an explicit `nameField` (or its `displayNameField` alias) points at a column that is neither title-typed nor autonumber, or no field-level answer can be had for the reader. A target with no resolvable or no non-empty name reads as the same floor. A principal who can read the target is served exactly what it is served without the key. Absent or false keeps the restricted-reference posture for a target the reader cannot read. Declared on the referencing field only; there is no target-object switch. Refused on any other type. | | **mailAssociation** | `boolean` | optional | Inbound mail association (`email` fields only): TRUE marks this field as identifying a correspondent. When a mailbox service ingests a message, it associates the message with every record of this object whose value in this field equals one of the message's participant addresses (From, Sender, Reply-To, To and Cc, plus those of a forwarded message it carries; ASCII case-insensitive; the receiving mailbox's own addresses excluded; only within the mailbox's organization). A message no record matches waits in the mailbox's unassociated pool. Refused on any other type. Takes effect only where a mailbox service is composed. | | **deleteBehavior** | `Enum<'set_null' \| 'cascade' \| 'restrict'>` | optional (default: `"set_null"`) | What happens if referenced record is deleted | | **inlineEdit** | `boolean \| Enum<'grid' \| 'form'>` | optional | Edit these child records inline within the parent's form (atomic master-detail). true = auto-pick grid/form by child shape; 'grid' = editable line-item grid; 'form' = list + per-row full form. | diff --git a/content/docs/references/system/migration.mdx b/content/docs/references/system/migration.mdx index 55f2d11dd6e..00e57e7524c 100644 --- a/content/docs/references/system/migration.mdx +++ b/content/docs/references/system/migration.mdx @@ -81,7 +81,7 @@ Add a new field to an existing object | **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. Never a stored-metadata table (`sys_metadata`, `sys_metadata_history`): their rows are served only through the metadata API, so the engine never hydrates a reference into one, and `os validate`, `os build` and the object save door refuse it. On a `tree` field it is optional and, if given, must be the declaring object's own name — the object schema refuses any other target, and writes the own name when it is left out. | | **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. | | **referenceDeletedWhen** | `{ field: string; value: string }` | optional | Marks the record a `referenceVia` pointer names as PLATFORM-DELETED on rows where a sibling `select` field holds one declared option — e.g. `record_id` with `{ field: 'cancel_reason', value: 'record_deleted' }`. A renderer then draws a tombstone from that option's own label, reads nothing and links nothing. Valid only beside `referenceVia`; `field` must be a single-value `select` on the same object declaring inline `options`, and `value` one of its option values — any other shape is refused at parse. | -| **referenceNameVisible** | `boolean` | optional | Declared name exposure (`lookup` and `user` fields only). TRUE declares that a principal who can read the record carrying this field may see the referenced record's display name (the target object's `nameField` value) and id even when that principal cannot read the referenced record. Served on `$expand`: a target the reader is refused (the object's read gate or its row scope) answers `{ id, name, readRefused: true }`, per element on a multi-value field; an unexpanded read answers the stored id, as for every reader. Nothing else of the target: `$expand` on this field returns the same name and id only, the field's `displayField` does not widen it, and the name renders as text with no link to the target. A field-level rule on the name field still holds: when the reader may not read that field, it is masked, or it is a formula reading through another reference, the name is the `Record #` floor with the id. A principal who can read the target is served exactly what it is served without the key. Absent or false keeps the restricted-reference posture for a target the reader cannot read. Declared on the referencing field only; there is no target-object switch. Refused on any other type. | +| **referenceNameVisible** | `boolean` | optional | Declared name exposure (`lookup` and `user` fields only). TRUE declares that a principal who can read the record carrying this field may see the referenced record's display name (the target object's `nameField`, else its `displayNameField`, else the derived title field) and id even when that principal cannot read the referenced record. Served on `$expand`: a target the reader is refused (the object's read gate or its row scope) answers `{ id, name, readRefused: true }` (`ReferenceNameOnlyValueSchema`), per element on a multi-value field; an unexpanded read answers the stored id, as for every reader. Nothing else of the target: `$expand` on this field returns the same name and id only, the field's `displayField` does not widen it, and the name renders as text with no link to the target. A field-level rule on the name field still holds, and the name field is read only where nothing the security layer owns must be computed: the name is the `Record #` floor with the id when the reader may not read that field, it declares a `maskingRule`, it is a formula reading through another reference, an explicit `nameField` (or its `displayNameField` alias) points at a column that is neither title-typed nor autonumber, or no field-level answer can be had for the reader. A target with no resolvable or no non-empty name reads as the same floor. A principal who can read the target is served exactly what it is served without the key. Absent or false keeps the restricted-reference posture for a target the reader cannot read. Declared on the referencing field only; there is no target-object switch. Refused on any other type. | | **mailAssociation** | `boolean` | optional | Inbound mail association (`email` fields only): TRUE marks this field as identifying a correspondent. When a mailbox service ingests a message, it associates the message with every record of this object whose value in this field equals one of the message's participant addresses (From, Sender, Reply-To, To and Cc, plus those of a forwarded message it carries; ASCII case-insensitive; the receiving mailbox's own addresses excluded; only within the mailbox's organization). A message no record matches waits in the mailbox's unassociated pool. Refused on any other type. Takes effect only where a mailbox service is composed. | | **deleteBehavior** | `Enum<'set_null' \| 'cascade' \| 'restrict'>` | optional (default: `"set_null"`) | What happens if referenced record is deleted | | **inlineEdit** | `boolean \| Enum<'grid' \| 'form'>` | optional | Edit these child records inline within the parent's form (atomic master-detail). true = auto-pick grid/form by child shape; 'grid' = editable line-item grid; 'form' = list + per-row full form. | @@ -515,7 +515,7 @@ Add a new field to an existing object | **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. Never a stored-metadata table (`sys_metadata`, `sys_metadata_history`): their rows are served only through the metadata API, so the engine never hydrates a reference into one, and `os validate`, `os build` and the object save door refuse it. On a `tree` field it is optional and, if given, must be the declaring object's own name — the object schema refuses any other target, and writes the own name when it is left out. | | **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. | | **referenceDeletedWhen** | `{ field: string; value: string }` | optional | Marks the record a `referenceVia` pointer names as PLATFORM-DELETED on rows where a sibling `select` field holds one declared option — e.g. `record_id` with `{ field: 'cancel_reason', value: 'record_deleted' }`. A renderer then draws a tombstone from that option's own label, reads nothing and links nothing. Valid only beside `referenceVia`; `field` must be a single-value `select` on the same object declaring inline `options`, and `value` one of its option values — any other shape is refused at parse. | -| **referenceNameVisible** | `boolean` | optional | Declared name exposure (`lookup` and `user` fields only). TRUE declares that a principal who can read the record carrying this field may see the referenced record's display name (the target object's `nameField` value) and id even when that principal cannot read the referenced record. Served on `$expand`: a target the reader is refused (the object's read gate or its row scope) answers `{ id, name, readRefused: true }`, per element on a multi-value field; an unexpanded read answers the stored id, as for every reader. Nothing else of the target: `$expand` on this field returns the same name and id only, the field's `displayField` does not widen it, and the name renders as text with no link to the target. A field-level rule on the name field still holds: when the reader may not read that field, it is masked, or it is a formula reading through another reference, the name is the `Record #` floor with the id. A principal who can read the target is served exactly what it is served without the key. Absent or false keeps the restricted-reference posture for a target the reader cannot read. Declared on the referencing field only; there is no target-object switch. Refused on any other type. | +| **referenceNameVisible** | `boolean` | optional | Declared name exposure (`lookup` and `user` fields only). TRUE declares that a principal who can read the record carrying this field may see the referenced record's display name (the target object's `nameField`, else its `displayNameField`, else the derived title field) and id even when that principal cannot read the referenced record. Served on `$expand`: a target the reader is refused (the object's read gate or its row scope) answers `{ id, name, readRefused: true }` (`ReferenceNameOnlyValueSchema`), per element on a multi-value field; an unexpanded read answers the stored id, as for every reader. Nothing else of the target: `$expand` on this field returns the same name and id only, the field's `displayField` does not widen it, and the name renders as text with no link to the target. A field-level rule on the name field still holds, and the name field is read only where nothing the security layer owns must be computed: the name is the `Record #` floor with the id when the reader may not read that field, it declares a `maskingRule`, it is a formula reading through another reference, an explicit `nameField` (or its `displayNameField` alias) points at a column that is neither title-typed nor autonumber, or no field-level answer can be had for the reader. A target with no resolvable or no non-empty name reads as the same floor. A principal who can read the target is served exactly what it is served without the key. Absent or false keeps the restricted-reference posture for a target the reader cannot read. Declared on the referencing field only; there is no target-object switch. Refused on any other type. | | **mailAssociation** | `boolean` | optional | Inbound mail association (`email` fields only): TRUE marks this field as identifying a correspondent. When a mailbox service ingests a message, it associates the message with every record of this object whose value in this field equals one of the message's participant addresses (From, Sender, Reply-To, To and Cc, plus those of a forwarded message it carries; ASCII case-insensitive; the receiving mailbox's own addresses excluded; only within the mailbox's organization). A message no record matches waits in the mailbox's unassociated pool. Refused on any other type. Takes effect only where a mailbox service is composed. | | **deleteBehavior** | `Enum<'set_null' \| 'cascade' \| 'restrict'>` | optional (default: `"set_null"`) | What happens if referenced record is deleted | | **inlineEdit** | `boolean \| Enum<'grid' \| 'form'>` | optional | Edit these child records inline within the parent's form (atomic master-detail). true = auto-pick grid/form by child shape; 'grid' = editable line-item grid; 'form' = list + per-row full form. |