From 2843b62e7bfcac967bc80c5b8a802385800dd73e Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 31 Aug 2026 12:21:06 +0000 Subject: [PATCH] refactor(components): derive metadata-viewer's option shape from the spec MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `metadata-viewer.tsx` declared a module-local `interface SelectOption` — four of the five keys `@objectstack/spec/data`'s `SelectOption` declares, with `visibleWhen` dropped by silence: no `Omit` naming the narrowing, no comment saying the drop was deliberate. It is the same defect class this repo has been closing for several cards running (one contract, several hand-written descriptions, each only ever reconciled against itself). Derived now, as `type StateOption = Omit` — the `Omit`-with-named-narrowings form `metadata-admin/form-spec.ts` already uses for `FormFieldSpec.options`. The dropped key and the retained `default` are each written out beside their reason. Zero behaviour change: the type is module-local, the value it annotates arrives from `useMetadataItem` as `any`, and `StateMachineView`'s `labelOf` / `colorOf` / `initial` logic is untouched. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB --- ...87-metadata-viewer-derive-select-option.md | 19 +++++++ .../src/renderers/basic/metadata-viewer.tsx | 53 ++++++++++++++++--- 2 files changed, 65 insertions(+), 7 deletions(-) create mode 100644 .changeset/6887-metadata-viewer-derive-select-option.md diff --git a/.changeset/6887-metadata-viewer-derive-select-option.md b/.changeset/6887-metadata-viewer-derive-select-option.md new file mode 100644 index 0000000000..6575ce14d6 --- /dev/null +++ b/.changeset/6887-metadata-viewer-derive-select-option.md @@ -0,0 +1,19 @@ +--- +--- + +Declaration-only: `metadata-viewer`'s state-machine view no longer hand-writes +the select-option shape. + +`packages/components/src/renderers/basic/metadata-viewer.tsx` declared its own +module-local `interface SelectOption` — four of the five keys +`@objectstack/spec/data`'s `SelectOption` declares, with `visibleWhen` dropped +by silence: no `Omit` naming the narrowing and no comment saying the drop was +deliberate. It is now derived, `type StateOption = Omit`, the same `Omit`-with-named-narrowings form +`metadata-admin/form-spec.ts` uses for `FormFieldSpec.options`, with the drop +and the retained `default` each written out next to its reason. + +No behaviour change and no released surface moves: the type is module-local, +the value it annotates arrives from `useMetadataItem` as `any`, and +`StateMachineView`'s `labelOf` / `colorOf` / `initial` logic is untouched — the +object-field `default` it reads stays the ruled-`enforce` key it always was. diff --git a/packages/components/src/renderers/basic/metadata-viewer.tsx b/packages/components/src/renderers/basic/metadata-viewer.tsx index 82a1dcbcb5..35a8621d45 100644 --- a/packages/components/src/renderers/basic/metadata-viewer.tsx +++ b/packages/components/src/renderers/basic/metadata-viewer.tsx @@ -32,6 +32,7 @@ import { Minus, AlertTriangle, } from 'lucide-react'; +import type { SelectOption as SpecSelectOption } from '@objectstack/spec/data'; import { cn } from '../../lib/utils'; import { readProps } from './readProps'; @@ -106,12 +107,50 @@ function Placeholder({ tone = 'muted', children }: { tone?: 'muted' | 'warn'; ch // state_machine — live transition graph from a `state_machine` validation rule // --------------------------------------------------------------------------- -interface SelectOption { - label?: string; - value: string; - color?: string; - default?: boolean; -} +/** + * One declared option of the object field a state machine runs on — DERIVED + * from the spec's own `SelectOption` (`@objectstack/spec/data`), with the one + * narrowing this projection needs named in the `Omit` below (objectui#6887). + * + * It was a hand-written `{ label?; value; color?; default? }` quadruple: four of + * the spec's five keys, with the fifth dropped by SILENCE — no `Omit` naming it, + * no comment saying the drop was deliberate. Nothing type-checks author-written + * metadata against this declaration (the value arrives from `useMetadataItem` + * as `any`), so unlike the authoring-side copies this one could not mis-accept + * anything; what it could do is go on compiling while the spec's option shape + * moved, and silently answer an older question. Deriving is what stops that, + * and it is the same form `form-spec.ts` uses one package over for + * `FormFieldSpec.options` (objectui#6247 / PR #6618). + * + * - `visibleWhen` is DROPPED, and now says so. This view draws the DECLARED + * state set — every option the field declares becomes a chip, and the + * transition graph is read from the `state_machine` rule, not from who is + * looking. Per-option `visibleWhen` withdraws an option from a PICKER for + * one record and one `current_user` (ADR-0068); a diagram of the machine + * has no such subject, so there is nothing here to evaluate it against. + * Carrying the key would advertise a gate this projection does not apply. + * + * - `default` is KEPT, and is read at `initial` below. That is deliberate and + * is the whole reason this is a spec derivation rather than a narrower + * local shape: `default` on the OBJECT-field face is ruled `enforce` + * (objectstack#7246, implemented by PR #7388) — the engine seeds the insert + * path from the option marked `default: true` — so the state this view + * calls "initial" is the same one the platform actually writes. + * + * `SelectOption` (`z.input`) rather than `SelectOptionParsed` (`z.infer`) even + * though this is a read model: on the four retained keys the two are the SAME + * type — none of `label` / `value` / `color` / `default` carries a `.default()` + * or a transform — and the entire input/output difference lives on + * `visibleWhen`, the key omitted above. + * + * ⚠️ Structurally this admits one shape LESS than the quadruple it replaces: + * the spec makes `label` REQUIRED and the hand copy had relaxed it to optional, + * by silence, the same way it dropped `visibleWhen`. Nothing turned on the + * relaxation — `labelOf` falls back to the raw value for any state the option + * list does not describe at all, which is the case that fallback exists for + * (transition sources and targets are not required to be declared options). + */ +type StateOption = Omit; function StateMachineView({ object, name }: ViewerProps) { const { item: obj, loading, error } = useMetadataItem('object', object ?? null); @@ -141,7 +180,7 @@ function StateMachineView({ object, name }: ViewerProps) { ? rule.transitions : {}; const field = getField(obj, rule.field); - const options: SelectOption[] = Array.isArray(field?.options) ? field.options : []; + const options: StateOption[] = Array.isArray(field?.options) ? field.options : []; const optByValue = new Map(options.map((o) => [o.value, o])); const labelOf = (v: string) => optByValue.get(v)?.label ?? v; const colorOf = (v: string) => optByValue.get(v)?.color;