Repository navigation
Commit adad73f
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
File tree
- .changeset
- content/docs/references
- api
- data
- system
- ui
- examples/app-showcase/src/data/objects
- packages
- metadata-core/src
- platform-objects/src/apps/translations
- spec
- api-surface
- authorable-surface
- export-origins
- liveness
- state-counts
- scripts/liveness
- src
- data
- migrations/entries/semantic
- ui
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
963 | 963 | | |
964 | 964 | | |
965 | 965 | | |
966 | | - | |
| 966 | + | |
967 | 967 | | |
968 | 968 | | |
969 | 969 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
201 | 201 | | |
202 | 202 | | |
203 | 203 | | |
| 204 | + | |
204 | 205 | | |
205 | 206 | | |
206 | 207 | | |
| |||
426 | 427 | | |
427 | 428 | | |
428 | 429 | | |
| 430 | + | |
429 | 431 | | |
430 | 432 | | |
431 | 433 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
167 | 167 | | |
168 | 168 | | |
169 | 169 | | |
170 | | - | |
| 170 | + | |
171 | 171 | | |
172 | 172 | | |
173 | 173 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
79 | 79 | | |
80 | 80 | | |
81 | 81 | | |
| 82 | + | |
82 | 83 | | |
83 | 84 | | |
84 | 85 | | |
| |||
104 | 105 | | |
105 | 106 | | |
106 | 107 | | |
| 108 | + | |
107 | 109 | | |
108 | 110 | | |
109 | 111 | | |
| |||
129 | 131 | | |
130 | 132 | | |
131 | 133 | | |
| 134 | + | |
132 | 135 | | |
133 | 136 | | |
134 | 137 | | |
| |||
0 commit comments