Filed unassigned as a sub-issue of objectui#7014, whose Tier-1 row 3 already records the TYPE-level half of this (the module-local interface Option in ObjectFieldInspector.tsx declares value / label / color and neither default nor visibleWhen). What this card adds is the RUNTIME consequence, and one measurement that changes where the repair has to start.
Raised by the domain:ui execution seat while implementing objectui#7014 Q1, and deliberately NOT folded into that PR — the reasoning is in the Q1 report on objectui#7014 and repeated at the bottom here.
The defect
packages/app-shell/src/views/metadata-admin/inspectors/ObjectFieldInspector.tsx. An author opens any picklist field in the metadata-admin designer, edits any option, and saves. Both default and visibleWhen are gone from the written document. It is not a 422 — the payload is perfectly valid — it is silent data loss of two keys the platform honours.
Where the loss actually happens — the reader, before the writer ever sees it
The writer (patchOptions, around line 383) projects to three keys:
const clean = next.map((o) => {
const out: Option = { value: o.value };
if (o.label) out.label = o.label;
if (o.color) out.color = o.color;
return out;
});
but the values are already gone by then. readOptions (around line 76) is the projection that loses them:
return raw.map((o: any) => ({
value: String(o?.value ?? ''),
label: typeof o?.label === 'string' ? o.label : undefined,
color: typeof o?.color === 'string' ? o.color : undefined,
}));
That matters for how the card is sized, and it is the reason a writer-only patch cannot close it: whatever patchOptions is taught to carry, it can only carry what readOptions handed it, and readOptions hands it three keys. The repair spans reader plus local type plus writer, not the writer alone.
It also means this is untouched by, and does not conflict with, the patchOptions label-guard change in PR #7536 — different keys, different mechanism, and that fix stays correct as it stands.
The measurement
Re-measured by me on the installed @objectstack/spec 17.2.0, with both controls lit before any row was trusted:
CONTROL clean { value:'alpha', label:'A' } ACCEPT parsed={"label":"A","value":"alpha"}
NEG-CONTROL { value:'a', label:'A' } REJECT too_small@[value]
default: true ACCEPT parsed={…,"default":true}
visibleWhen: "x > 1" (string) ACCEPT parsed={…,"visibleWhen":{"dialect":"cel","source":"x > 1"}}
visibleWhen: {dialect:'cel', source:'x > 1'} ACCEPT parsed={…,"visibleWhen":{"dialect":"cel","source":"x > 1"}}
visibleWhen: true (boolean) REJECT invalid_union@[visibleWhen]
CONTROL unknown key { …, zzz:1 } REJECT unrecognized_keys@[](zzz)
Two usage notes worth carrying, because both cost a reading when they were missed:
- A select option's
value must be at least 2 characters. A one-character value poisons every row with too_small@[value], and the rows that are actually about visibleWhen then say nothing at all. Use something like alpha, and make the clean control ACCEPT before trusting any other line.
- The boolean form of
visibleWhen is REFUSED, not accepted. It is the STRING form that is accepted and canonicalized into the expression envelope. An earlier writeup attributed that canonicalization to the boolean row; if you quote this behaviour, quote the table above.
So both dropped keys are keys the spec ACCEPTS, and default is ruled enforce on the object-field face (objectstack#7246, implemented by PR #7388) — the engine seeds the insert path from the option marked default: true. An author who sets a picklist default in the designer, or who authored one in a *.form.ts, loses it the next time anyone edits that field in the UI.
What has to be decided before this is implementable
The mechanical part is small. The part that is not mechanical, and the reason this is a card rather than a drive-by:
The inspector has no per-option UI for either key. The CelPredicateField uses in that file are FIELD-level — they write through patchDef({ visibleWhen: … }) — so there is no per-option control to wire up. That leaves at least two shapes, and they are different products:
- Preserve what the editor does not display.
readOptions keeps the whole authored option and patchOptions writes back the untouched keys, so the editor stops being lossy for anything it has no opinion about. Cheapest, and it also protects any key added later.
- Author them. Give the option row real per-option controls for
default (a radio across the option list, since at most one can hold it) and visibleWhen (a CelPredicateField per option). Much larger, and it is a genuine design question about that panel.
Related history worth reading before choosing: objectui#6247 covered per-option visibleWhen in metadata-admin from the renderer side.
Whichever way it goes, note the shape to avoid: widening the local Option type WITHOUT moving the reader and the writer would declare two keys the editor still cannot carry — a declared-but-not-carried divergence, which is the exact shape objectui#7014 exists to remove.
Why this is not in the Q1 PR (objectui#7538)
Q1's ruled scope is the Tier-1 named types, and its diff lives entirely in @object-ui/types. Taking this on would have: put a second in-flight claim on a file another seat is already changing (PR #7536, not yet merged); moved the diff into @object-ui/app-shell, a different package with its own type-check, its own large suite and its own gate face that the Q1 diff does not otherwise carry; and required the design call above, which is not a mechanical repair. Recorded here instead of done quietly.
Generated by Claude Code
Filed unassigned as a sub-issue of objectui#7014, whose Tier-1 row 3 already records the TYPE-level half of this (the module-local
interface OptioninObjectFieldInspector.tsxdeclaresvalue/label/colorand neitherdefaultnorvisibleWhen). What this card adds is the RUNTIME consequence, and one measurement that changes where the repair has to start.Raised by the
domain:uiexecution seat while implementing objectui#7014 Q1, and deliberately NOT folded into that PR — the reasoning is in the Q1 report on objectui#7014 and repeated at the bottom here.The defect
packages/app-shell/src/views/metadata-admin/inspectors/ObjectFieldInspector.tsx. An author opens any picklist field in the metadata-admin designer, edits any option, and saves. BothdefaultandvisibleWhenare gone from the written document. It is not a 422 — the payload is perfectly valid — it is silent data loss of two keys the platform honours.Where the loss actually happens — the reader, before the writer ever sees it
The writer (
patchOptions, around line 383) projects to three keys:but the values are already gone by then.
readOptions(around line 76) is the projection that loses them:That matters for how the card is sized, and it is the reason a writer-only patch cannot close it: whatever
patchOptionsis taught to carry, it can only carry whatreadOptionshanded it, andreadOptionshands it three keys. The repair spans reader plus local type plus writer, not the writer alone.It also means this is untouched by, and does not conflict with, the
patchOptionslabel-guard change in PR #7536 — different keys, different mechanism, and that fix stays correct as it stands.The measurement
Re-measured by me on the installed
@objectstack/spec17.2.0, with both controls lit before any row was trusted:Two usage notes worth carrying, because both cost a reading when they were missed:
valuemust be at least 2 characters. A one-character value poisons every row withtoo_small@[value], and the rows that are actually aboutvisibleWhenthen say nothing at all. Use something likealpha, and make the clean control ACCEPT before trusting any other line.visibleWhenis REFUSED, not accepted. It is the STRING form that is accepted and canonicalized into the expression envelope. An earlier writeup attributed that canonicalization to the boolean row; if you quote this behaviour, quote the table above.So both dropped keys are keys the spec ACCEPTS, and
defaultis ruledenforceon the object-field face (objectstack#7246, implemented by PR #7388) — the engine seeds the insert path from the option markeddefault: true. An author who sets a picklist default in the designer, or who authored one in a*.form.ts, loses it the next time anyone edits that field in the UI.What has to be decided before this is implementable
The mechanical part is small. The part that is not mechanical, and the reason this is a card rather than a drive-by:
The inspector has no per-option UI for either key. The
CelPredicateFielduses in that file are FIELD-level — they write throughpatchDef({ visibleWhen: … })— so there is no per-option control to wire up. That leaves at least two shapes, and they are different products:readOptionskeeps the whole authored option andpatchOptionswrites back the untouched keys, so the editor stops being lossy for anything it has no opinion about. Cheapest, and it also protects any key added later.default(a radio across the option list, since at most one can hold it) andvisibleWhen(aCelPredicateFieldper option). Much larger, and it is a genuine design question about that panel.Related history worth reading before choosing: objectui#6247 covered per-option
visibleWhenin metadata-admin from the renderer side.Whichever way it goes, note the shape to avoid: widening the local
Optiontype WITHOUT moving the reader and the writer would declare two keys the editor still cannot carry — a declared-but-not-carried divergence, which is the exact shape objectui#7014 exists to remove.Why this is not in the Q1 PR (objectui#7538)
Q1's ruled scope is the Tier-1 named types, and its diff lives entirely in
@object-ui/types. Taking this on would have: put a second in-flight claim on a file another seat is already changing (PR #7536, not yet merged); moved the diff into@object-ui/app-shell, a different package with its own type-check, its own large suite and its own gate face that the Q1 diff does not otherwise carry; and required the design call above, which is not a mechanical repair. Recorded here instead of done quietly.Generated by Claude Code