What was ruled
Maintainer ruling recorded 2026-08-22 on objectstack-ai/objectui#4015 (decision-inbox
digest with the skills seat; the 46-card batch was accepted verbatim: 「接受所有」):
Ruled: Option C — reject at the entrance: refuse radio + multiple: true at the
schema/publish layer with a diagnostic prescribing the correctly-named multi-choice
types (checkboxes/multiselect/tags), while leaving MULTI_CAPABLE_TYPES and
isMultiValueField untouched so at-rest data keeps its read path and no stored-shape
migration is paid. Option A (rendering the combination as checkboxes) is explicitly not
taken. […] The implementation lands on the spec side (objectstack, domain:spec lane).
This card carries that implementation. It is filed unassigned, by an os-dev seat that was
dispatched on the objectui card and stopped rather than implementing a renderer-side
variant the ruling does not authorize.
The defect being ruled on
An author may declare { type: 'radio', multiple: true }. The data layer honours it
(stores an array, validates as multi, splits on import, infers action-param arity), while
the widget renders a single-value radio group with zero diagnostics — declared multi,
rendered single.
The producer contradicts itself about radio, all on origin/main5d163792c:
| Site | Says |
|---|
packages/spec/src/data/field-value.zod.ts:79-81 | SINGLE_OPTION_TYPES = ['select','radio'] — "Single-choice option types." |
packages/spec/src/data/field-value.zod.ts:198-206 | MULTI_CAPABLE_TYPES includes radio, its own comment conceding radio is there because it "shares the select branch" |
packages/spec/src/data/field-value.zod.ts:226 | isMultiValueField = MULTI_CAPABLE_TYPES.has(type) && multiple === true (ADR-0104 D1) |
packages/spec/src/data/field.zod.ts:818 | multiple's published description: "Applicable for select, lookup, file, image" — radio absent |
packages/spec/src/ui/action.zod.ts:453 | param arity inferred from MULTI_CAPABLE_TYPES |
Consumer side, objectui origin/main515f171f3:
packages/fields/src/widgets/RadioField.tsx — 0 occurrences of multiple
(control probe: 23 for RadioGroup|value, so the file really was read).packages/app-shell/src/utils/paramValueShape.ts:142 —
radio: { base: 'string', cardinality: 'scalar', note: 'Single option value string.' },
where 'scalar' is defined as "always a single base value". This is a landed,
drift-tested declaration (paramValueShape.test.ts:93 pins radio -> string) that
radio does not promote on multiple — which is why Option A (render as checkboxes)
would have contradicted a pinned contract in that repo, independently of the ruling.
Scope of the ruled change (Option C)
- Refuse
radio + multiple: true at the schema/publish layer, with a diagnostic
naming the field and prescribing checkboxes / multiselect / tags. - ⛔ Do not touch
MULTI_CAPABLE_TYPES or isMultiValueField — at-rest data keeps its
read path, and no stored-shape migration is paid. That is the whole reason C was
preferred over B. packages/objectql/src/validation/record-validator.ts's (t === 'select' || t === 'radio')
branch stays as a data-safety fallback for stock, not as a capability.
Flip condition to B, attached by the ruling
If deployed tenant metadata carrying { type: 'radio', multiple: true }with stored
data is found, entrance rejection alone strands those tenants and Option B (narrow the
spec sets themselves, with a migration path) becomes required rather than optional. The
ruling's own stated blind spot is that only the two code repositories were measured.
Occurrence measurement, re-measured 2026-08-23
Both repos at objectui 515f171f3 / objectstack 5d163792c: 10,977 files scanned, 21
type: 'radio' declarations found (control probe — the scanner reads real declarations),
0 of them carrying multiple: true. Examples, showcase, dogfood matrix, docs, fixtures
and tests are all clean. The single window match was select + multiple on a line adjacent
to a radio entry in objectui paramValueShape.test.ts.
Owed by the landing fork, named by the ruling
objectui packages/fields/src/field-type-alias.ts:28-37 states that "every other member
renders both arities INSIDE one widget: LookupField, FileField and ImageField" —
silently omitting radio, the one member with no renderer for its multi arity. That
sentence is what made this gap invisible; the ruling assigns its correction to whichever
fork lands.
Decision anchor: objectstack-ai/objectui#4015 — it stays open as the decision record
until this ships. ⛔ Do not put a closing keyword next to that number from here.
Generated by Claude Code
Generated by Claude Code
What was ruled
Maintainer ruling recorded 2026-08-22 on objectstack-ai/objectui#4015 (decision-inbox
digest with the skills seat; the 46-card batch was accepted verbatim: 「接受所有」):
This card carries that implementation. It is filed unassigned, by an
os-devseat that wasdispatched on the objectui card and stopped rather than implementing a renderer-side
variant the ruling does not authorize.
The defect being ruled on
An author may declare
{ type: 'radio', multiple: true }. The data layer honours it(stores an array, validates as multi, splits on import, infers action-param arity), while
the widget renders a single-value radio group with zero diagnostics — declared multi,
rendered single.
The producer contradicts itself about
radio, all onorigin/main5d163792c:packages/spec/src/data/field-value.zod.ts:79-81SINGLE_OPTION_TYPES = ['select','radio']— "Single-choice option types."packages/spec/src/data/field-value.zod.ts:198-206MULTI_CAPABLE_TYPESincludesradio, its own comment concedingradiois there because it "shares the select branch"packages/spec/src/data/field-value.zod.ts:226isMultiValueField=MULTI_CAPABLE_TYPES.has(type) && multiple === true(ADR-0104 D1)packages/spec/src/data/field.zod.ts:818multiple's published description: "Applicable for select, lookup, file, image" —radioabsentpackages/spec/src/ui/action.zod.ts:453MULTI_CAPABLE_TYPESConsumer side, objectui
origin/main515f171f3:packages/fields/src/widgets/RadioField.tsx— 0 occurrences ofmultiple(control probe: 23 for
RadioGroup|value, so the file really was read).packages/app-shell/src/utils/paramValueShape.ts:142—radio: { base: 'string', cardinality: 'scalar', note: 'Single option value string.' },where
'scalar'is defined as "always a singlebasevalue". This is a landed,drift-tested declaration (
paramValueShape.test.ts:93pinsradio -> string) thatradiodoes not promote onmultiple— which is why Option A (render as checkboxes)would have contradicted a pinned contract in that repo, independently of the ruling.
Scope of the ruled change (Option C)
radio+multiple: trueat the schema/publish layer, with a diagnosticnaming the field and prescribing
checkboxes/multiselect/tags.MULTI_CAPABLE_TYPESorisMultiValueField— at-rest data keeps itsread path, and no stored-shape migration is paid. That is the whole reason C was
preferred over B.
packages/objectql/src/validation/record-validator.ts's(t === 'select' || t === 'radio')branch stays as a data-safety fallback for stock, not as a capability.
Flip condition to B, attached by the ruling
If deployed tenant metadata carrying
{ type: 'radio', multiple: true }with storeddata is found, entrance rejection alone strands those tenants and Option B (narrow the
spec sets themselves, with a migration path) becomes required rather than optional. The
ruling's own stated blind spot is that only the two code repositories were measured.
Occurrence measurement, re-measured 2026-08-23
Both repos at objectui
515f171f3/ objectstack5d163792c: 10,977 files scanned, 21type: 'radio'declarations found (control probe — the scanner reads real declarations),0 of them carrying
multiple: true. Examples, showcase, dogfood matrix, docs, fixturesand tests are all clean. The single window match was
select + multipleon a line adjacentto a
radioentry in objectuiparamValueShape.test.ts.Owed by the landing fork, named by the ruling
objectui
packages/fields/src/field-type-alias.ts:28-37states that "every other memberrenders both arities INSIDE one widget:
LookupField,FileFieldandImageField" —silently omitting
radio, the one member with no renderer for its multi arity. Thatsentence is what made this gap invisible; the ruling assigns its correction to whichever
fork lands.
Decision anchor: objectstack-ai/objectui#4015 — it stays open as the decision record
until this ships. ⛔ Do not put a closing keyword next to that number from here.
Generated by Claude Code
Generated by Claude Code