Skip to content

Commit c52c49d

Browse files
feat(spec)!: FieldSchema refuses a select / radio with neither options nor picklist (#21390)
Fixes #20827 Clause-②: yes Ruling A (record `5910124148`), step two: `FieldSchema` refuses a `select` / `radio` field with neither `options` nor `picklist` at parse, on the `lookup`-without-`reference` precedent (`0fb8760bec`). Step one (`5917187437`) measured the census and the Studio order; the objectui half landed and `.objectui-sha` `31971ff1e28f` carries it (`5947557233`). ## The door - `packages/spec/src/data/field.zod.ts`, `FieldSchema`'s superRefine, beside the `reference` check: a `custom` issue on the `options` path whose message names the field type and both remedies (`options: [{ label, value }]`, or `picklist: 'NAME'` for a shared list), says what the hole costs (an empty control, and no server-side value validation), and offers `text` when any value is meant to be allowed. No tracker number in the runtime string. - **One predicate, not a second one.** The door applies the ADR-0078 completeness predicate itself: `checkFieldCompleteness(field)` has a `field/choice-without-options` finding at `error` severity. That module imports nothing at runtime, so the new edge (data to kernel) closes no cycle, and no export is added. Three facts follow from it rather than being restated: `options: []` is the same hole as a missing key; a `picklist` reference is a source; the types are exactly `select` / `radio` (`checkboxes` stays a warning, `multiselect` / `tags` stay free-form). The same reading is what objectui's guard derivation test uses (`deriveChoiceTypesRequiringOptions`). - The exclusivity block's comment no longer claims "neither" is only the completeness finding. The `options` and `picklist` TSDoc and `.describe()` texts state the rule; the reference pages were regenerated through `check:generated --fix` (only `check:docs` was stale). - ADR-0078's author-time rule and the registration warning are not edited. The door is one more gate. ## Fixture census, measured with the door on Re-measured at base `5fd4855a9a` with the full `@objectstack/spec` suite (6 red in 5 files) and the full `@objectstack/metadata-protocol` suite (9 red in 2 files). That matches step one exactly. | Red with the door on | Why it went red | Disposition | |:--|:--|:--| | `field.test.ts:2155` (radio + `multiple: false`) | oversight: pins `multiple`, not choices | one option added | | `field-autonumber-default-unique.test.ts:45` (`minimalField`) | oversight: "minimal valid input per type" | `select` / `radio` get one option | | `filter-number-comparand-declared-type.ts` fixture (`fixtureFieldFor`) | oversight: "each is a legal FieldSchema input" | single-choice types get one option (`SINGLE_OPTION_TYPES`); the fixture interface gains an optional `options` member | | `filter-text-operator-declared-type.ts` fixture (`fixtureFieldFor`) | same | same | | `picklist.test.ts:129`, 2 tests | **pinned the old acceptance by design** ("neither is the completeness gate's error, not a parse refusal") | flipped into a refusal pin; its `checkFieldCompleteness` assertions are kept unchanged | | `metadata-protocol`: 9 tests from `legacyObjectRow`'s `status: { type: 'select' }` (`protocol.stored-conversions.test.ts`, `protocol.stored-migration.test.ts`) | oversight: the row exists to test `conditionalRequired` | one option added (`sent`, the value its `requiredWhen` reads) | One fixture beyond the census: `canonicalObjectRow` in `protocol.stored-migration.test.ts` carries the same optionless `select`. It caused no red (the pass never validates a row that needs no conversion), but its doc says "already canonical — the shape every row ends up in", which the door makes false, so it got the same option. ## Stored rows: the disposition No migration can invent the options an author meant, so a stored row is read and named, never rewritten. That was step one's reading, and it is now pinned in `@objectstack/metadata-protocol`: - `getMetaItem` still serves such a row, with `_diagnostics.valid: false` naming `fields.status.options`; - `loadMetaFromDb` counts it `invalid: 1` and still registers it; - `migrateStoredMetadata({ apply: true })` on a legacy row that also carries an optionless `select` reports it `failed` (spec validation), writes no history row, and leaves the stored bytes as they were. The changeset tells an operator to find such rows through `GET /api/v1/meta/diagnostics` or the boot log's `field/choice-without-options` lines, and says the `os migrate meta --stored` preview does not find them, because it does not validate. **ADR-0087 marker:** `not-required (no-migration-prescription)`. Two existing keys are narrowed in validity, and none is removed, renamed or re-shaped. No conversion entry can supply the missing intent, and the gate's other categories are closed on facts (the marker says why). That is the precedent's category. Production `sys_metadata` cannot be measured from here, so the marker text takes the ruling's "some rows exist" arm and names the read-and-name path, not a migration. ## Semver `@objectstack/spec`: `minor` with a **BREAKING** header, as the precedent shipped. The changeset is `.changeset/20827-choice-door-select-radio-needs-options.md`. Gate lines: - `check-adr-0087-registration`: `1 declared-breaking changeset(s), each carrying an ADR-0087 disposition` · `[BREAKING+bang] not-required (no-migration-prescription)` - `check-changeset-no-major`: `This diff introduces no major bump` - `check-empty-changeset`: `No empty-frontmatter changeset introduced by this diff (1 declaring changeset(s) added)` No other package publishes a change: the `metadata-protocol` edits are tests only, and `content/docs` does not publish. ## Reverse verification (ablation) The door's predicate was mutated through `scripts/ablation-replace.mjs` (`FIELD_CHOICE_WITHOUT_OPTIONS` to the literal `'ablation-20827-never'`; anchor 1 to 0, blob `ea9313c768` to `9654443823`). Then `@objectstack/spec` was rebuilt, and `ablation-dist-preflight` found the marker in 24 built files. - **Mutate leg: red.** 8 spec pins red: the 6 refusal pins in `field.test.ts` and the 2 rewritten pins in `picklist.test.ts`. 3 `metadata-protocol` pins red: the two `stored-conversions` pins and the `stored-migration` `failed` pin. The positive controls stayed green (one option, `picklist` only, and `multiselect` / `checkboxes` / `tags` with neither). - **Restore leg: green.** Restored with `git checkout HEAD --`: blob equal to HEAD and `git diff HEAD` empty. After a rebuild, `--absent` reported the marker in 0 of 230 built files and the tree clean. Then 325/325 spec tests and 56/56 `metadata-protocol` tests passed. ## Tests (head `ee5b089bbe`, after merging `origin/main`) - `@objectstack/spec` full suite: 646 files, 18391 passed, 1 todo. - `@objectstack/spec` `typecheck`: green, including `check:test-typecheck`. - `@objectstack/spec` `check:generated`: all 15 artifacts up to date. - `@objectstack/metadata-protocol` `typecheck`: green. - These consumer suites ran with the door on, before the merge, at `a9473b8d91`. The merge brought no change to these packages' sources. - `@objectstack/metadata-protocol`: 201 files, 2983 passed, 19 skipped. - `@objectstack/objectql`: 363 files, 7277 passed. This includes the two engine door suites that consume the changed spec fixtures. - `@objectstack/lint`: 119 files, 5575 passed. - `@objectstack/metadata`: 56 files, 836 passed. - `@objectstack/runtime`: 306 files, 5081 passed, 11 skipped. - `dispatch-gates --ran`: 110 derived families, 109 run, 1 NOT MEASURED, 0 unrun. The one: `check:dual-build-cjs-loads` exits 3 because it needs every package's `dist`. As a declared narrowing, all 19 `require` entries of `@objectstack/spec` load under CJS, and the door is live through `dist/data/index.js`. - `check:doc-authoring` and `check:nul-bytes` are green. ## Acceptance notes - **Blueprint (A6).** `packages/spec/src/ai/solution-blueprint.zod.ts`, `BlueprintFieldSchema.options` (optional) and `StrictField.options` (`.nullable()`), still let a blueprint `select` / `radio` carry no options. Inside objectstack and objectui, nothing expands a blueprint into `FieldSchema` input. `apply_blueprint` lives outside both repositories; objectui only renders its progress and plan cards. The module's own header says that the expansion validates against the per-type schema at write time. So once this door ships, an optionless blueprint `select` is refused loudly at that write, not silently stored. The gap is that the blueprint accepts what the door refuses, so the refusal lands one step late, on the expanded artifact, not on the AI's structured output. **Not edited here.** For the PM to file: align `BlueprintFieldSchema` / `StrictField` `options` with the door for `select` / `radio`. - **objectui pin (A8).** `packages/data-objectstack/src/object-metadata-write-guard.derivation.test.ts`, the describe "the installed server still ACCEPTS a choice with no options", goes red when objectui next bumps `@objectstack/spec` to a release carrying this door. Its own comment prescribes the follow-up: rewrite the guard's docblock paragraph into the relationship form and turn the block into a refusal pin at `options`. That is the objectui lane's follow-up. No objectui edit here. - **`skills/**` hit, not edited (governed surface).** `skills/objectstack-upgrade/references/examples-upgrade.md:105` teaches `status: { type: 'select', required: true, storage: { notNull: true } }` inside `ObjectSchema.create(...)`. With this door that example is refused at parse. It needs one `options` entry, in a skills-lane PR. - **Docs.** `content/docs/deployment/troubleshooting.mdx` had an entry quoting an error message no code emits ("Required property missing: options") and listing `multiselect` / `checkboxes` as refused. It now quotes the real refusal, the right two types and the stored-row reading. `validation-rules.mdx` called `options` "Required" on all four option types; `multiselect` / `checkboxes` now read optional, as the runtime treats them. `field-types.mdx` names `picklist` as the alternative. The `formulas.mdx` `ObjectSchema.create` example got options. No content page said an optionless `select` parses or is only a lint finding. - **The Clause-② line** is copied from the claim (`yes`). The diff narrows `FieldSchema`'s accept set, which is BREAKING and is carried by the changeset banner. It also widens one published type: the two exported door-fixture interfaces gain an optional `options` member. Read together, `yes (narrowing)` is the fuller spelling. The claim's spelling is kept, and the two gates pass on it. --- _Generated by [Claude Code](https://claude.ai/code/session_01YDt3PzwfrkuFzUBF89WPmM)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 69a12a0 commit c52c49d

16 files changed

Lines changed: 307 additions & 37 deletions
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
---
2+
"@objectstack/spec": minor
3+
---
4+
5+
fix(spec)!: `FieldSchema` refuses a `select` / `radio` field with neither `options` nor `picklist` (#20827)
6+
7+
Clause-②: yes
8+
9+
<!-- adr-0087: not-required (no-migration-prescription) A validity narrowing over two existing keys: `options` and `picklist` are not removed, renamed or re-shaped, so there is no tombstone and nothing mechanical for `objectstack migrate meta` to rewrite. Which choices a `select` / `radio` with no option source was meant to offer is authoring intent that no conversion entry can invent. Stored rows follow the ruling's arm for rows that exist (production `sys_metadata` is not measurable from this repository, so the disposition assumes some do): new writes are refused at the parse site with the remedy, and a stored row keeps its bytes, is still served with `_diagnostics.valid: false`, is listed by `/meta/diagnostics`, and is counted invalid and named by the `field/choice-without-options` boot line until an option or a picklist is added (pinned in `@objectstack/metadata-protocol`). The in-tree authored population is zero (examples and platform objects); the fixture census is on the PR. The other categories are closed on facts: the package publishes (not unpublished); no ADR-0087 id covers the choice-source rule and this diff adds none (not registered / already-registered); and the change narrows a metadata schema's accept set, not a runtime interface or a type surface alone (not runtime-interface-only / type-surface-only). -->
10+
11+
**BREAKING** accept-set narrowing on `FieldSchema`, shipped as `minor` under the
12+
repo's launch-window convention for breaking changes — the grade the `reference`
13+
precedent shipped with (a `lookup` / `master_detail` without `reference`, refused
14+
at parse as a `minor` with the **BREAKING** header).
15+
16+
**What was accepted before.** A `select` or `radio` field with no `options` key,
17+
with `options: []`, and with no `picklist` parsed cleanly. It is a choice with
18+
nothing to choose: the form control offers nothing, and server-side value
19+
validation is off (the record validator checks membership only against a
20+
non-empty allowed list), so any value writes through the API. The author-time
21+
completeness gate (ADR-0078, `field/choice-without-options`, used by `os build`,
22+
`os validate` and `os lint`) already graded it an error, and registration warns on
23+
it; a runtime-API or Studio save was the one door that let it through.
24+
25+
**What is refused now.** At parse, on the `options` path, with a `custom` issue
26+
that names the field type and both remedies: a `select` / `radio` whose `options`
27+
is absent or empty and whose `picklist` is absent. The predicate is the
28+
completeness gate's own, so the two cannot disagree.
29+
30+
**The fix.** Declare `options: [{ label, value }]` with at least one entry, or
31+
`picklist: 'industry'` (the name of any shared list) to offer a shared list —
32+
never both (that pair stays refused as before). If any value is meant to be allowed, use a `text` field instead.
33+
34+
**Unchanged.** `multiselect` and `tags` keep parsing without options (free-form
35+
by design), and `checkboxes` keeps parsing with a completeness warning. A
36+
`select` / `radio` with at least one option, or with a `picklist`, parses as
37+
before. The ADR-0078 author-time rule and the registration warning are
38+
unchanged — this door is one more gate, not a replacement. `Field.select()`
39+
called with an empty list emits `options: []`, which is now refused at parse.
40+
41+
**Stored rows.** No conversion can supply the missing options, so a row saved
42+
before this release is not rewritten. It is still served — with
43+
`_diagnostics.valid: false` naming `fields.FIELD.options` — and still
44+
registered at boot (counted invalid); a later save of its object is refused
45+
until an option or a `picklist` is added. To find such rows, read
46+
`GET /api/v1/meta/diagnostics`, or the boot log's `field/choice-without-options`
47+
lines. The `os migrate meta --stored` preview does not validate bodies, so it
48+
does not find them: it counts such a row canonical, or — when the row also
49+
carries an older spelling to lower — pending, and the apply then reports that
50+
row failed and leaves its bytes as they were.

‎content/docs/data-modeling/field-types.mdx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -234,7 +234,7 @@ Single-choice dropdown.
234234

235235
| Property | Type | Default | Description |
236236
|:---|:---|:---|:---|
237-
| `options` | `SelectOption[]` | **required** | List of available options |
237+
| `options` | `SelectOption[]` | **required** (or `picklist`) | List of available options — non-empty. Name a shared list with `picklist: 'NAME'` instead, never both. With neither, a `select` (or `radio`) is refused at parse. |
238238

239239
**SelectOption properties:**
240240

‎content/docs/data-modeling/formulas.mdx‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -290,6 +290,7 @@ ObjectSchema.create({
290290
// Raw (non-factory) spelling — the map key is the field name.
291291
rating: {
292292
type: 'select',
293+
options: [{ label: 'Hot', value: 'hot' }, { label: 'Cold', value: 'cold' }],
293294
visibleWhen: P`record.status == 'qualified'`,
294295
},
295296
},

‎content/docs/data-modeling/validation-rules.mdx‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -215,7 +215,7 @@ These properties apply to **all** field types and are validated by the base `Fie
215215

216216
| Property | Type | Default | Validation Behavior |
217217
|:---|:---|:---|:---|
218-
| `options` | `SelectOption[]` | — | **Required.** Static option list |
218+
| `options` | `SelectOption[]` | — | **Required** — a non-empty static option list, or a `picklist` naming a shared list instead. With neither (or `options: []`) the field is refused at parse. |
219219
| `defaultValue` | `string` | — | Must match an option `value` |
220220

221221
**Option validation:** Each option `value` must be a lowercase system identifier — starts with a letter, then letters/digits/underscores/dots (`^[a-z][a-z0-9_.]*$`), minimum 2 characters.
@@ -224,23 +224,23 @@ These properties apply to **all** field types and are validated by the base `Fie
224224

225225
| Property | Type | Default | Validation Behavior |
226226
|:---|:---|:---|:---|
227-
| `options` | `SelectOption[]` | — | **Required.** Static option list |
227+
| `options` | `SelectOption[]` | — | Static option list. Optional: without options the field takes free-form values (tags mode). |
228228

229229
**Default constraints:** Stores array of selected option values. Each value validated against options.
230230

231231
### `radio`
232232

233233
| Property | Type | Default | Validation Behavior |
234234
|:---|:---|:---|:---|
235-
| `options` | `SelectOption[]` | — | **Required.** Static option list |
235+
| `options` | `SelectOption[]` | — | **Required** — as for `select`: a non-empty list or a `picklist`, refused at parse with neither. |
236236

237237
**Default constraints:** Single-value selection. Same validation as `select` with radio button UI.
238238

239239
### `checkboxes`
240240

241241
| Property | Type | Default | Validation Behavior |
242242
|:---|:---|:---|:---|
243-
| `options` | `SelectOption[]` | — | **Required.** Static option list |
243+
| `options` | `SelectOption[]` | — | Static option list. Parses without one, but draws a completeness warning: a checkbox group with no boxes renders nothing. |
244244

245245
**Default constraints:** Multi-value selection. Same validation as `multiselect` with checkbox group UI.
246246

‎content/docs/deployment/troubleshooting.mdx‎

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -51,15 +51,15 @@ The custom error map provides "Did you mean?" suggestions for common typos.
5151

5252
---
5353

54-
### "Required property missing: options"
54+
### "A `select` field needs its choices"
5555

56-
**Symptom:** A `select`, `multiselect`, `radio`, or `checkboxes` field fails validation.
56+
**Symptom:** A `select` or `radio` field is refused at parse (on save, publish, `os validate` or `os build`), with the issue on its `options` path.
5757

58-
**Cause:** Selection-type fields require an `options` array.
58+
**Cause:** A single-choice field needs an option source: a non-empty `options` array, or a `picklist` naming a shared list. With neither — and `options: []` counts as neither — the form control would offer nothing and server-side value validation would be off, so any value would write through. `multiselect`, `tags` and `checkboxes` may omit both (the first two are free-form without options; `checkboxes` draws a completeness warning).
5959

6060
**Fix:**
6161
```typescript
62-
// ❌ Missing options
62+
// ❌ No option source
6363
{ name: 'status', type: 'select' }
6464

6565
// ✅ With options
@@ -70,8 +70,13 @@ The custom error map provides "Did you mean?" suggestions for common typos.
7070
{ label: 'Closed', value: 'closed' }
7171
]
7272
}
73+
74+
// ✅ Or with a shared list
75+
{ name: 'industry', type: 'select', picklist: 'industry' }
7376
```
7477

78+
A row stored before this check keeps its bytes: it is still served, with `_diagnostics` naming the field, listed by `/meta/diagnostics`, and named at boot by the `field/choice-without-options` log line. A later save of its object is refused until an option or a `picklist` is added.
79+
7580
---
7681

7782
### "Required property missing: reference"

‎content/docs/references/data/field.mdx‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -74,8 +74,8 @@ const result = CurrencyConfigSchema.parse(data);
7474
| **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. |
7575
| **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. |
7676
| **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. |
77-
| **options** | `{ label: string; value: string; description?: string; color?: string; … }[]` | optional | Static options for select/multiselect |
78-
| **picklist** | `string` | optional | Name of a shared `picklist` whose options this field offers — instead of `options`, never with it. Option types only (select, radio, multiselect, checkboxes, tags). |
77+
| **options** | `{ label: string; value: string; description?: string; color?: string; … }[]` | optional | Static options for the option types. A `select` / `radio` field needs a non-empty list here or a `picklist` — with neither (or `options: []`) it is refused at parse. |
78+
| **picklist** | `string` | optional | Name of a shared `picklist` whose options this field offers — instead of `options`, never with it. Option types only (select, radio, multiselect, checkboxes, tags). A select / radio declares this or a non-empty `options`; with neither it is refused at parse. |
7979
| **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. 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. |
8080
| **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. |
8181
| **deleteBehavior** | `Enum<'set_null' \| 'cascade' \| 'restrict'>` | optional (default: `"set_null"`) | What happens if referenced record is deleted |

‎content/docs/references/data/object.mdx‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -238,8 +238,8 @@ const result = ApiMethod.parse(data);
238238
| **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. |
239239
| **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. |
240240
| **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. |
241-
| **options** | `{ label: string; value: string; description?: string; color?: string; … }[]` | optional | Static options for select/multiselect |
242-
| **picklist** | `string` | optional | Name of a shared `picklist` whose options this field offers — instead of `options`, never with it. Option types only (select, radio, multiselect, checkboxes, tags). |
241+
| **options** | `{ label: string; value: string; description?: string; color?: string; … }[]` | optional | Static options for the option types. A `select` / `radio` field needs a non-empty list here or a `picklist` — with neither (or `options: []`) it is refused at parse. |
242+
| **picklist** | `string` | optional | Name of a shared `picklist` whose options this field offers — instead of `options`, never with it. Option types only (select, radio, multiselect, checkboxes, tags). A select / radio declares this or a non-empty `options`; with neither it is refused at parse. |
243243
| **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. 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. |
244244
| **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. |
245245
| **deleteBehavior** | `Enum<'set_null' \| 'cascade' \| 'restrict'>` | optional (default: `"set_null"`) | What happens if referenced record is deleted |
@@ -572,8 +572,8 @@ const result = ApiMethod.parse(data);
572572
| **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. |
573573
| **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. |
574574
| **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. |
575-
| **options** | `{ label: string; value: string; description?: string; color?: string; … }[]` | optional | Static options for select/multiselect |
576-
| **picklist** | `string` | optional | Name of a shared `picklist` whose options this field offers — instead of `options`, never with it. Option types only (select, radio, multiselect, checkboxes, tags). |
575+
| **options** | `{ label: string; value: string; description?: string; color?: string; … }[]` | optional | Static options for the option types. A `select` / `radio` field needs a non-empty list here or a `picklist` — with neither (or `options: []`) it is refused at parse. |
576+
| **picklist** | `string` | optional | Name of a shared `picklist` whose options this field offers — instead of `options`, never with it. Option types only (select, radio, multiselect, checkboxes, tags). A select / radio declares this or a non-empty `options`; with neither it is refused at parse. |
577577
| **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. 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. |
578578
| **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. |
579579
| **deleteBehavior** | `Enum<'set_null' \| 'cascade' \| 'restrict'>` | optional (default: `"set_null"`) | What happens if referenced record is deleted |

0 commit comments

Comments
 (0)