Skip to content

spec: refuse an authored radio + multiple: true at the schema/publish layer (maintainer ruling 2026-08-22, Option C) #11437

Description

@claude

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:

SiteSays
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_TYPES includes radio, its own comment conceding radio is 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"radio absent
packages/spec/src/ui/action.zod.ts:453param arity inferred from MULTI_CAPABLE_TYPES

Consumer side, objectui origin/main515f171f3:

  • packages/fields/src/widgets/RadioField.tsx0 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)

  1. Refuse radio + multiple: true at the schema/publish layer, with a diagnostic
    naming the field and prescribing checkboxes / multiselect / tags.
  2. ⛔ 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.
  3. 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

Metadata

Metadata

Assignees

Type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions