Skip to content

Commit adad73f

Browse files
feat(spec): a select option declares its lifecycle kind (open / won / lost); every stage-field option must declare it (#23846)
Fixes #23809 Clause-②: yes (narrowing) ## What A select option declares its lifecycle `kind`, one closed vocabulary for every object: `open` (a step on the way), `won` (the successful end), `lost` (the unsuccessful end). Every option of the field an object's `stageField` names must declare it. - `packages/spec/src/data/field.zod.ts`: `SelectOptionSchema` gains `kind: z.enum(['open', 'won', 'lost']).optional()`, documented with the definition (a task's Done and a case's Resolved are `won`; Cancelled / Unqualified / Closed Lost are `lost`). Absent stays absent at parse (reads as `open`; no default is written in). `FieldSchema` refuses `kind` at `options.N.kind` on any field that is not a single-value `select` / `radio`. Exported type `SelectOptionKind`. The `dueLike` / `settledWhen` docblock now says options carry a `kind` but overdue is still judged by `settledWhen` alone (objectui#11815 ruling D unchanged; nothing is wired). - `packages/spec/src/data/object.zod.ts`: `refuseStageFieldWithoutKinds`, an `ObjectSchema` parse-time check beside `refuseDishonestReferenceDeletedWhen`. One located issue per option without `kind` at `fields.FIELD.options.N.kind`, message naming the field and the option. Also refused at `stageField`: a picklist-backed stage field, and a stage field that has options but holds no single option code per record (`multiselect`, `multiple: true`). Left as they were: `stageField: false`, a pointer to no declared field (lint `semantic-roles` owns it), a field with no options. The `stageField` describe states exactly this scope. - `packages/spec/src/ui/view.zod.ts`: `FormSelectOptionSchema` is derived from the option shape, so it now leaves out `kind` as well as `default`, with a guidance line pointing at the object field. Without that, `kind` would have been accepted on form-view options with nothing reading it. - Forms: the object designer's options repeater offers `kind` (a select of the three values); the field form's repeater lists it. The platform-objects metadata-form bundles carry the new row, translated into zh-CN / ja-JP / es-ES. - Liveness: `field/options` is now **drilled**. Six children are `live` with their read points, and `kind` is `planned` (no `authorWarn`, because the key is mandatory under `stageField`). The `field/options` row leaves `undrilled-containers.baseline.json`. `picklist/options.kind` is `planned`. - ADR-0087: the D3 entry `stage-field-option-kind-required`. It has no conversion because the ruling withholds a default. - `examples/app-showcase/.../semantic-zoo.object.ts`: the only `stageField: 'status'` in the corpus now declares `open` / `open` / `won`. - `packages/metadata-core/src/object-schema-fls-contract.ts`: the published contract fixture `FLS_CONTRACT_OBJECT` names `salary_grade` as its `stageField`, so its two options declare `kind: 'open'`. It is pinned in `object-schema-fls-references.test.ts`, which asserts no stage-field issue and has a positive control. - platform-objects catalog count pins follow the new `kind` rows: `field-panel-echo-decisions` 10 to 11, `object-collapsed-sections-echo-decisions` 116 to 118, `object-lifecycle-panel-echo-decisions` 672 to 674. - Changeset `.changeset/23809-select-option-kind.md`: `@objectstack/spec` minor, `@objectstack/platform-objects` patch (metadata-form bundles), `@objectstack/metadata-core` patch (contract fixture), `Clause-②: yes (narrowing)`, with the migration. ## Vocabulary ruling: premise reading The seat ruled `open | won | lost` on the premise that `record:path` `stages[].terminal: z.enum(['won', 'lost'])` is a live platform vocabulary. Measured at objectui `main` @63d00da: `packages/plugin-detail/src/renderers/record-path.tsx` declares `PathStage.terminal?: 'won' | 'lost'`, and its `classify` returns `s.terminal` first (line 221), ahead of the token heuristic. The premise holds. `component.zod.ts` is untouched. ## Acceptance pins (`packages/spec/src/data/select-option-kind.test.ts`) 1. An option with a terminal kind parses: `an option with a terminal kind parses, and keeps it`. 2. An unknown kind is refused: `an unknown kind is refused at the option, located` (`done`, `cancelled`, `closed`, `WON`, empty). 3. A plain select without kinds parses and reads as open: `a plain select without kinds parses, and an absent kind stays absent (reads as open)`. 4. A stage field with one option lacking `kind` is refused, naming the field and option: `one option lacking kind is refused at publish, naming the field and the option` (path `fields.status.options.3.kind`), plus `each option lacking kind is its own located issue`, `create() refuses the same object`. 5. `kind` on a non-select field is refused: `is refused on a \`%s\` field` over multiselect, checkboxes, tags, `multiple: true` select/radio, lookup, text. Updated pins: `field-rows-option-description.test.ts` (the repeater's input list gains `kind`), `form-select-option.test.ts` (Omit minus `default` and `kind`; the `kind` refusal names the object field). ## Verification (round 1 at head 4274c65; patch round at head 4475469) - `@objectstack/spec` full vitest: 21996 passed, 2 failed. Both failures were the two pins above, updated afterwards. The re-run of those files, the new file and `src/migrations`: 279/279. `pnpm typecheck` exit 0. `check:generated`: all 14 artifacts up to date. - **Ablation** (`scripts/ablation-replace.mjs`, mutation proved on disk, restored blob == HEAD with an empty `git diff HEAD`): deleting the `refuseStageFieldWithoutKinds(...)` call turns 6 stage pins red. Turning the `FieldSchema` off-type guard into `if (false)` turns 7 off-type pins red. Both are green again after restore. - Downstream: rest FLS / save-gate suites 125/125, runtime `meta-object-fls` 85/85, metadata-core `object-schema-fls-references` 53/53. Showcase `validate`, `build` and `typecheck` all exit 0. - **Patch round, head 4475469** (contract review FAIL `6211666660`, three items closed): spec `check:generated` all 14 up to date after `--fix`; `select-option-kind` + `object.test` 232/232. platform-objects 1092/1092 (3 count pins updated, as above); metadata-core 735/735 and typecheck exit 0; rest FLS 125/125; runtime meta-object-fls 85/85. `dispatch-gates --commands`: the derived list is byte-identical to round 1. 116 run, all exit 0 after the spec-dist prerequisite re-runs, with the same 2 NOT MEASURED left to CI. - `dispatch-gates --commands` (round 1): 116 families derived, 116 run. All exit 0 except: `check:i18n` exit 1, a real finding (bundles drifted), fixed by regenerating and translating, now 0. `check:skill-examples`, `check:i18n`, `check:i18n-coverage` re-run green after a dist rebuild. Two families are **NOT MEASURED** locally, exit 3 (prerequisite): `check-plugin-teardown-shape --self-test` (the shallow clone cannot reach its pinned fixture) and `check:dual-build-cjs-loads` (needs the whole workspace built). Both are left to CI. ## Acceptance notes - **Picklist-backed stage field**: refused at `stageField` rather than judged, because `ObjectSchema` cannot see a picklist's options. Zero instances in the corpus. The alternative, judging the resolved list where `os validate` resolves picklist references, would cover only the CLI doors and not the save door. This is recorded in the report's open questions. - The object designer's options repeater is shown for `multiselect` / `checkboxes` too, and the form DSL has no parent-scoped row predicate. So `kind` is offered there and the publish door refuses it. The help text says "single-value select / radio only". - Readers follow in their own cards: the objectui record path / kanban, the Studio inspector column, and the HotCRM declarations. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --- _Generated by [Claude Code](https://claude.ai/code/session_017qmjVjz61scwQFLfspdBX6)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent c5cf546 commit adad73f

34 files changed

Lines changed: 611 additions & 31 deletions
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
---
2+
'@objectstack/spec': minor
3+
'@objectstack/platform-objects': patch
4+
'@objectstack/metadata-core': patch
5+
---
6+
7+
feat(spec)!: a select option declares its lifecycle `kind` (`open` / `won` / `lost`), and every option of an object's `stageField` must declare it
8+
9+
Clause-②: yes (narrowing)
10+
11+
`SelectOptionSchema` gains one optional key, `kind`, a closed vocabulary of three lifecycle categories that means the same on every object: `open` (a step on the way), `won` (the successful end: a deal's Closed Won, a lead's Converted, a task's Done, a case's Resolved) and `lost` (the unsuccessful end: Closed Lost, Unqualified, Cancelled). It is the spelling `record:path` `stages[].terminal` already takes, so the data layer and the page say one thing one way. Example: `{ label: 'Closed Lost', value: 'closed_lost', kind: 'lost' }`. The exported type `SelectOptionKind` names the three values.
12+
13+
- On a plain single-value `select` / `radio` the key is optional, and an absent kind reads as `open`. Nothing is written in at parse: an option without `kind` parses without it.
14+
- On any other field's options (`multiselect`, `checkboxes`, `tags`, a `multiple: true` select, a lookup's static options) `FieldSchema` refuses it at `options.<i>.kind`: a lifecycle is one value per record.
15+
- A shared `picklist`'s options accept it (the same option shape). A form view's option list does not (`FormSelectOptionSchema`): it is refused there as an unknown key whose message points at the object field's own option.
16+
17+
**BREAKING (authoring):** when an object's `stageField` names a field, every option of that field must declare `kind`. `ObjectSchema` refuses the object otherwise, at every door that parses one (`ObjectSchema.create()`, `defineStack`, `os validate`, `os build`, the metadata save door), with one issue per option at `fields.<field>.options.<i>.kind` naming the field and the option. There is no default under `stageField`. Also refused, at `stageField`: a stage field that takes its options from a `picklist` (the object cannot see them to check), and a stage field carrying options that holds no single option code per record (`multiselect`, a `multiple: true` select). Unchanged: `stageField: false`, a `stageField` naming no declared field, and one naming a field with no options.
18+
19+
Migration: add `kind` to each option of the stage field, marking each terminal state `won` or `lost` and every other state `open`:
20+
21+
- `status: Field.select({ options: [{ label: 'New', value: 'new' }, { label: 'Converted', value: 'converted' }, { label: 'Unqualified', value: 'unqualified' }] })` with `stageField: 'status'` → the same options with `kind: 'open'`, `kind: 'won'` and `kind: 'lost'`.
22+
- A picklist-backed stage field → declare its options inline on the field.
23+
- An object with no linear lifecycle → `stageField: false`.
24+
25+
No conversion makes this edit, because which option ends a record's life successfully is the author's to say. `os migrate meta` lists the D3 entry `stage-field-option-kind-required` with the prescription.
26+
27+
The Studio object designer's options repeater offers `kind`; the field form's options repeater lists it.
28+
29+
The other packages follow the new key:
30+
31+
- **`@objectstack/platform-objects`.** The metadata-form translation bundles (`./metadata-translations`) carry the options repeater's new `kind` row, with its label and help text in zh-CN, ja-JP and es-ES.
32+
- **`@objectstack/metadata-core`.** The published contract fixture `FLS_CONTRACT_OBJECT` (`@objectstack/metadata-core/testing`) names `salary_grade` as its `stageField`, so its two options now declare `kind: 'open'`, and the fixture meets the stage-field rule: `ObjectSchema` raises no issue at `stageField` or under `fields.salary_grade.options`. The fixture is not otherwise a spec-valid object (its `script` actions carry no `body` or `target`), and this change leaves that as it was.
33+
34+
<!-- adr-0087: registered stage-field-option-kind-required -->

‎content/docs/references/api/metadata.mdx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -963,7 +963,7 @@ Metadata query with filtering, sorting, and pagination
963963
| **titleFormat** | `string \| { dialect: 'template'; source?: string; ast?: any; meta?: object }` | optional | [DEPRECATED → nameField (ADR-0079)] Render-only title template; the server cannot return or query it, and an explicit nameField now takes precedence. Migrate a single-field title to nameField, a composite to a formula field designated as nameField. Placeholders may be written `{{field}}` or `{field}` — the title renderers treat the two as equivalent, normalizing `{{field}}` to `{field}` before substituting; neither spelling is judged at parse time. |
964964
| **imageField** | `string` | optional | The record's picture: names the field the record page header (record chrome) draws beside the title — one object-level declaration every record detail page reads, not a per-page header prop. Must name a field of this object whose type is `image` or `avatar`; any other name is refused. A record whose field is empty shows no picture (no initials or placeholder). |
965965
| **highlightFields** | `string[]` | optional | [ADR-0085] Ordered most-important fields; first entry wins where only one fits. Drives default columns, cards, previews, detail highlight strip. Renamed from compactLayout. |
966-
| **stageField** | `string \| false` | optional | [ADR-0085] Lifecycle stage field (linear/ordered), or false to declare the status field non-linear and suppress stage heuristics. Absent = heuristic detection allowed. |
966+
| **stageField** | `string \| false` | optional | [ADR-0085] Lifecycle stage field (linear/ordered), or false to declare the status field non-linear and suppress stage heuristics. Absent = heuristic detection allowed. When the named field carries options, it must be a single-value `select` / `radio` with inline `options`, each declaring its `kind` ('open' / 'won' / 'lost'); a picklist-backed or multi-value stage field is refused. A name that resolves to no declared field, or a field with no options, is not judged at parse. |
967967
| **editMode** | `Enum<'modal' \| 'page'>` | optional | Edit-interaction intent for records of this object: 'modal' opens the edit form as a dialog over the current view; 'page' navigates to a dedicated full-page edit route. Absent = the renderer picks its own default (objectui defaults to modal). Cross-renderer intent, not pixel styling (family). |
968968
| **listViews** | `Record<string, { name?: string; label?: string \| Record<string, string>; type?: Enum<'grid' \| 'kanban' \| 'gallery' \| 'calendar' \| 'timeline' \| 'gantt' \| 'map' \| 'chart' \| 'tree'>; data?: object \| object \| object; … }>` | optional | Built-in named list views (segmented tabs) shipped with the object schema — "views" mode, dropdown userFilters allowed, no page-only tabs (ADR-0047) |
969969
| **searchableFields** | `string[]` | optional | Fields the `$search` query matches against (ADR-0061). Canonical default for the record picker, list quick-search and global search; views may narrow it. When unset, search auto-defaults to the name/title field plus short-text fields. Entries must name a STORED column: a virtual `formula` field is computed on read and materializes no column, so searching it can never match and it is refused — mirror the value onto a stored text field and declare that. |

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,7 @@ const result = CurrencyConfigSchema.parse(data);
201201
| **color** | `string` | optional | Color code for badges/charts |
202202
| **default** | `boolean` | optional | Is default option |
203203
| **visibleWhen** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source: string; ast?: any; meta?: object }` | optional | Per-option visibility predicate (CEL) — option is offered only when TRUE (else omitted). Env: the live `record` plus the host predicate scope, which binds `current_user`. The one VISIBILITY predicate the SERVER also enforces — the rule validator refuses a write of a value whose predicate is false, or cannot be evaluated — so a user-gated CHOICE belongs here. e.g. P`record.country == 'cn'` or P`'admin' in current_user.positions`. On an OBJECT field's option it reads the record's OWN columns: the server never reads a related record there, so a read THROUGH a reference field (`record.account.tier`) would fault and every write picking the option would be refused, and `objectstack validate` refuses it — enforce such a restriction with a `validations[]` `script` rule, whose `condition` is read one hop through a reference. |
204+
| **kind** | `Enum<'open' \| 'won' \| 'lost'>` | optional | Lifecycle category of this option: 'open' (a step on the way), 'won' (the successful end: a deal's Closed Won, a task's Done, a case's Resolved) or 'lost' (the unsuccessful end: Closed Lost, Cancelled, Unqualified). Absent reads as 'open'. Required on every option of the field an object's `stageField` names. Only on a single-value `select` / `radio` field. |
204205

205206
### Nested Shape: `Field.referenceDeletedWhen`
206207

@@ -426,6 +427,7 @@ Allowed Values: `phone`, `id_card`, `bank_account`, `email`, `name`
426427
| **color** | `string` | optional | Color code for badges/charts |
427428
| **default** | `boolean` | optional | Is default option |
428429
| **visibleWhen** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source: string; ast?: any; meta?: object }` | optional | Per-option visibility predicate (CEL) — option is offered only when TRUE (else omitted). Env: the live `record` plus the host predicate scope, which binds `current_user`. The one VISIBILITY predicate the SERVER also enforces — the rule validator refuses a write of a value whose predicate is false, or cannot be evaluated — so a user-gated CHOICE belongs here. e.g. P`record.country == 'cn'` or P`'admin' in current_user.positions`. On an OBJECT field's option it reads the record's OWN columns: the server never reads a related record there, so a read THROUGH a reference field (`record.account.tier`) would fault and every write picking the option would be refused, and `objectstack validate` refuses it — enforce such a restriction with a `validations[]` `script` rule, whose `condition` is read one hop through a reference. |
430+
| **kind** | `Enum<'open' \| 'won' \| 'lost'>` | optional | Lifecycle category of this option: 'open' (a step on the way), 'won' (the successful end: a deal's Closed Won, a task's Done, a case's Resolved) or 'lost' (the unsuccessful end: Closed Lost, Cancelled, Unqualified). Absent reads as 'open'. Required on every option of the field an object's `stageField` names. Only on a single-value `select` / `radio` field. |
429431

430432

431433
---

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,7 @@ const result = ApiMethod.parse(data);
167167
| **titleFormat** | `string \| { dialect: 'template'; source?: string; ast?: any; meta?: object }` | optional | [DEPRECATED → nameField (ADR-0079)] Render-only title template; the server cannot return or query it, and an explicit nameField now takes precedence. Migrate a single-field title to nameField, a composite to a formula field designated as nameField. Placeholders may be written `{{field}}` or `{field}` — the title renderers treat the two as equivalent, normalizing `{{field}}` to `{field}` before substituting; neither spelling is judged at parse time. |
168168
| **imageField** | `string` | optional | The record's picture: names the field the record page header (record chrome) draws beside the title — one object-level declaration every record detail page reads, not a per-page header prop. Must name a field of this object whose type is `image` or `avatar`; any other name is refused. A record whose field is empty shows no picture (no initials or placeholder). |
169169
| **highlightFields** | `string[]` | optional | [ADR-0085] Ordered most-important fields; first entry wins where only one fits. Drives default columns, cards, previews, detail highlight strip. Renamed from compactLayout. |
170-
| **stageField** | `string \| false` | optional | [ADR-0085] Lifecycle stage field (linear/ordered), or false to declare the status field non-linear and suppress stage heuristics. Absent = heuristic detection allowed. |
170+
| **stageField** | `string \| false` | optional | [ADR-0085] Lifecycle stage field (linear/ordered), or false to declare the status field non-linear and suppress stage heuristics. Absent = heuristic detection allowed. When the named field carries options, it must be a single-value `select` / `radio` with inline `options`, each declaring its `kind` ('open' / 'won' / 'lost'); a picklist-backed or multi-value stage field is refused. A name that resolves to no declared field, or a field with no options, is not judged at parse. |
171171
| **editMode** | `Enum<'modal' \| 'page'>` | optional | Edit-interaction intent for records of this object: 'modal' opens the edit form as a dialog over the current view; 'page' navigates to a dedicated full-page edit route. Absent = the renderer picks its own default (objectui defaults to modal). Cross-renderer intent, not pixel styling (family). |
172172
| **listViews** | `Record<string, { name?: string; label?: string \| Record<string, string>; type?: Enum<'grid' \| 'kanban' \| 'gallery' \| 'calendar' \| 'timeline' \| 'gantt' \| 'map' \| 'chart' \| 'tree'>; data?: object \| object \| object; … }>` | optional | Built-in named list views (segmented tabs) shipped with the object schema — "views" mode, dropdown userFilters allowed, no page-only tabs (ADR-0047) |
173173
| **searchableFields** | `string[]` | optional | Fields the `$search` query matches against (ADR-0061). Canonical default for the record picker, list quick-search and global search; views may narrow it. When unset, search auto-defaults to the name/title field plus short-text fields. Entries must name a STORED column: a virtual `formula` field is computed on read and materializes no column, so searching it can never match and it is refused — mirror the value onto a stored text field and declare that. |

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ A shared option list that select fields reference by name instead of copying opt
7979
| **color** | `string` | optional | Color code for badges/charts |
8080
| **default** | `boolean` | optional | Is default option |
8181
| **visibleWhen** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source: string; ast?: any; meta?: object }` | optional | Per-option visibility predicate (CEL) — option is offered only when TRUE (else omitted). Env: the live `record` plus the host predicate scope, which binds `current_user`. The one VISIBILITY predicate the SERVER also enforces — the rule validator refuses a write of a value whose predicate is false, or cannot be evaluated — so a user-gated CHOICE belongs here. e.g. P`record.country == 'cn'` or P`'admin' in current_user.positions`. On an OBJECT field's option it reads the record's OWN columns: the server never reads a related record there, so a read THROUGH a reference field (`record.account.tier`) would fault and every write picking the option would be refused, and `objectstack validate` refuses it — enforce such a restriction with a `validations[]` `script` rule, whose `condition` is read one hop through a reference. |
82+
| **kind** | `Enum<'open' \| 'won' \| 'lost'>` | optional | Lifecycle category of this option: 'open' (a step on the way), 'won' (the successful end: a deal's Closed Won, a task's Done, a case's Resolved) or 'lost' (the unsuccessful end: Closed Lost, Cancelled, Unqualified). Absent reads as 'open'. Required on every option of the field an object's `stageField` names. Only on a single-value `select` / `radio` field. |
8283
8384
8485
---
@@ -104,6 +105,7 @@ Options added to a picklist owned by another package (additive only — the `obj
104105
| **color** | `string` | optional | Color code for badges/charts |
105106
| **default** | `boolean` | optional | Is default option |
106107
| **visibleWhen** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source: string; ast?: any; meta?: object }` | optional | Per-option visibility predicate (CEL) — option is offered only when TRUE (else omitted). Env: the live `record` plus the host predicate scope, which binds `current_user`. The one VISIBILITY predicate the SERVER also enforces — the rule validator refuses a write of a value whose predicate is false, or cannot be evaluated — so a user-gated CHOICE belongs here. e.g. P`record.country == 'cn'` or P`'admin' in current_user.positions`. On an OBJECT field's option it reads the record's OWN columns: the server never reads a related record there, so a read THROUGH a reference field (`record.account.tier`) would fault and every write picking the option would be refused, and `objectstack validate` refuses it — enforce such a restriction with a `validations[]` `script` rule, whose `condition` is read one hop through a reference. |
108+
| **kind** | `Enum<'open' \| 'won' \| 'lost'>` | optional | Lifecycle category of this option: 'open' (a step on the way), 'won' (the successful end: a deal's Closed Won, a task's Done, a case's Resolved) or 'lost' (the unsuccessful end: Closed Lost, Cancelled, Unqualified). Absent reads as 'open'. Required on every option of the field an object's `stageField` names. Only on a single-value `select` / `radio` field. |
107109
108110
109111
---
@@ -129,6 +131,7 @@ The served form of a picklist-bound field: `options` resolved, `picklist` kept
129131
| **color** | `string` | optional | Color code for badges/charts |
130132
| **default** | `boolean` | optional | Is default option |
131133
| **visibleWhen** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source: string; ast?: any; meta?: object }` | optional | Per-option visibility predicate (CEL) — option is offered only when TRUE (else omitted). Env: the live `record` plus the host predicate scope, which binds `current_user`. The one VISIBILITY predicate the SERVER also enforces — the rule validator refuses a write of a value whose predicate is false, or cannot be evaluated — so a user-gated CHOICE belongs here. e.g. P`record.country == 'cn'` or P`'admin' in current_user.positions`. On an OBJECT field's option it reads the record's OWN columns: the server never reads a related record there, so a read THROUGH a reference field (`record.account.tier`) would fault and every write picking the option would be refused, and `objectstack validate` refuses it — enforce such a restriction with a `validations[]` `script` rule, whose `condition` is read one hop through a reference. |
134+
| **kind** | `Enum<'open' \| 'won' \| 'lost'>` | optional | Lifecycle category of this option: 'open' (a step on the way), 'won' (the successful end: a deal's Closed Won, a task's Done, a case's Resolved) or 'lost' (the unsuccessful end: Closed Lost, Cancelled, Unqualified). Absent reads as 'open'. Required on every option of the field an object's `stageField` names. Only on a single-value `select` / `radio` field. |
132135
133136
134137
---

0 commit comments

Comments
 (0)