Repository navigation
spec: refuse an authored radio + multiple: true at the schema/publish layer (maintainer ruling 2026-08-22, Option C) #11437
Description
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: trueat the
schema/publish layer with a diagnostic prescribing the correctly-named multi-choice
types (checkboxes/multiselect/tags), while leavingMULTI_CAPABLE_TYPESand
isMultiValueFielduntouched 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:speclane).
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/main 5d163792c:
| 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/main 515f171f3:
packages/fields/src/widgets/RadioField.tsx— 0 occurrences ofmultiple
(control probe: 23 forRadioGroup|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) that
radiodoes 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)
- Refuse
radio+multiple: trueat the schema/publish layer, with a diagnostic
naming the field and prescribingcheckboxes/multiselect/tags. - ⛔ Do not touch
MULTI_CAPABLE_TYPESorisMultiValueField— 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
Activity
docs(fields): name radio as the arity exception in the MULTI_VALUE_FO…
claude commentedon Aug 24, 2026
Triage (daily round, session
session_01Kktexqp6uVuFMztvvTMf3V, 2026-08-24): ruled card (maintainer 2026-08-22, Option C — refuse authoredradio+multiple: trueat schema/publish layer):pm:queueadded on the existingdomain:spec, Task. Clause-② yes — accept/reject behaviour changes ⇒ contract-review tier. Quote the ruling verbatim in the dispatch brief.Generated by Claude Code
os-steve commentedon Aug 24, 2026
Claim: PM seat
session_01T9cDbY2NBiVJWYx3BpWfH2(/pm-dispatch, spec lane) — dispatching to an os-dev subagent under the recorded maintainer ruling (2026-08-22 on objectui#4015, 「接受所有」; verbatim Option C text on this card).Session: session_01T9cDbY2NBiVJWYx3BpWfH2
Branch:
claude/issue-11437-radio-multiple-refusalWorktree:
../objectstack-11437(created by the dev agent, per-task)Domain: domain:spec
File surface:
packages/spec/src/data/field.zod.ts(or wherever the schema-layer refusal correctly homes — dev measures against the ruling's "schema/publish layer") + tests; ⛔MULTI_CAPABLE_TYPES/isMultiValueField(field-value.zod.ts) untouched; objectql record-validator branch untouched (data-safety fallback stays).Container & model: os-dev subagent, claude-fable-5 (clause-② tier — new accept/reject refusal on a previously-parseable combination; fable serialization satisfied, sole fable dev).
Clause-②: yes (triage concurs) —
needs:contract-reviewhung on this card at claim; PR-side at creation; PR stays draft, landing decision is the review's.Parse-idempotency check mandated (#9689 class): verify whether
multiplecarries a materialized default anywhereradiofields parse — the refusal must reject only the AUTHOREDtrue, never a materialized value, andparse(parse(x))must stay stable.Cross-repo boundary: the objectui
field-type-alias.ts:28-37prose correction the ruling assigns to the landing fork is objectui-side — the dev does NOT touch objectui; this seat relays it at ACCEPT (objectui#4015 stays open as the decision record; ⛔ no closing keyword next to that number anywhere in the PR).Serial constraints:
field.zod.tshot-file check — #9784 is blocked-parked (not in flight) and PR #11406 is draft-parked on a different region (deleteBehavior); no live overlap, but the dev must verify merge cleanliness against origin/main at claim time.Generated by Claude Code
os-steve commentedon Aug 24, 2026
os-dev-report
{ "issue": 11437, "status": "done", "branch": "claude/issue-11437-radio-multiple-refusal", "pr": "https://github.com/objectstack-ai/objectstack/pull/11560", "premise_still_valid": true, "summary": "Implemented ruled Option C (maintainer 2026-08-22 on objectui#4015): FieldSchema's superRefine in packages/spec/src/data/field.zod.ts now refuses an authored radio + multiple:true with a diagnostic that names the field, names the illegal pair on path ['multiple'], and prescribes checkboxes / multiselect / tags — homed beside the sibling authored-combination refusals, firing on both publish shapes (the standalone 'field' metadata type via metadata-type-schemas.ts:91 and ObjectSchema-embedded fields via object.zod.ts:1894). MULTI_CAPABLE_TYPES / isMultiValueField and objectql record-validator's select/radio branch stay untouched per the ruling, with a new pin so a future cleanup of radio's set membership trips a test; objectui untouched. Premise re-verified at merge base d10e214626: all five contradiction-table sites hold, no prior landing of the refusal, occurrence re-measure found 18 type:'radio' declarations across both repos with 0 carrying multiple:true (the single window hit is the known select+multiple adjacency in objectui paramValueShape.test.ts:93), so the flip-to-B condition stays untriggered. Reminder owed by the landing fork, NOT in this PR: the objectui field-type-alias.ts:28-37 prose correction is the PM's to relay at ACCEPT. Draft PR held for clause-2 contract review; changeset ships @objectstack/spec minor with explicit BREAKING body and ADR-0087 disposition not-required (no-migration-prescription), accepted by check:adr-0087-registration.", "tests": "All at commit 72b3d9c886 via scripts/pm/os-verify-lock.sh. Build: pnpm --filter @objectstack/spec build, VERDICT command-exit 0. Full suite after the final commit: pnpm --filter @objectstack/spec test = 'Test Files 420 passed (420) / Tests 11213 passed (11213)', VERDICT command-exit 0 (one earlier attempt died at the 10-min foreground cap, exit 143, read-only, rerun clean). Typecheck green (tsc + scripts + test programs; check:test-typecheck self-test printed its pass line). check:generated: 'All 14 generated artifacts are up to date' after rebuild (an mtime-only false stale on check:api-surface from the reverse-verification revert/restore cleared by rebuilding identical content). Reverse verification, fix committed first: git restore --source=origin/main on field.zod.ts only; mutation confirmed on disk by marker grep 'declared multi, rendered single' 2 -> 0 with porcelain showing the lone M — no rebuild needed for this leg because field.test.ts imports ./field.zod relatively so vitest reads src, not dist (the dist ablation ledger is not in play); observed direction: exactly the 3 refusal tests red, the 6 accept-side controls green on both sides; restored via git checkout HEAD (porcelain clean, marker back to 2, empty diff vs HEAD), re-run 9/9 green. Dist acceptance probe through @objectstack/spec/data package exports from packages/objectql (dist built from the committed content, build VERDICT 0): bad.success=false with the verbatim prescriptive message on path ['multiple']; good.success=true with multiple:false materialized; MULTI_CAPABLE_TYPES.has('radio')=true and isMultiValueField({type:'radio',multiple:true})=true. dispatch-gates.mjs --repo objectstack-ai/objectstack (derivation attested at 72b3d9c886, --repo assertion held): 26 gates run, 25 to verdict green including check:adr-0087-registration, check:changeset-no-major, check:empty-changeset, check:changeset-gate-self-tests, check:cross-package-test-inputs, check:engine-double-contract, check:where-matcher, check:test-source-alias, check:nul-bytes, and check:doc-formula-expressions (green after building @objectstack/formula — its red was worktree build state, not this diff). Declared narrowings (2), both workspace-build-state preconditions a spec-only diff cannot move, both CI-covered on a fully built tree: check:dev-prereqs (wants all 67 dists; spec's own dist is built and content-fresh) and check:type-check-debt --re-measure (needs full closure built; the slice this diff can move — spec's programs including the new test code — proven green by spec typecheck). Parse-idempotency pinned: parse(parse(validRadioField)) deep-equal stable; multiple's .default(false) materializes false only, so the refusal can never fire on a defaulted value.", "open_questions": [], "out_of_scope_findings": [] }Generated by Claude Code
Generated by Claude Code
os-steve commentedon Aug 24, 2026
PM verdict: ACCEPT — PR #11560
Contract review (clause ②): PASS. Fuse reading this stroke via
get_session:external_metadata.last_served_model = claude-fable-5. Reviewed the actual diff at head72b3d9c886.FieldSchema's superRefine beside the sibling authored-combination refusals, fires on both publish shapes (standalonefield+ ObjectSchema-embedded, each tested), and the diagnostic names the field, the illegal pair on path['multiple'], and all three prescriptions plus the single-choice remedy.MULTI_CAPABLE_TYPES/isMultiValueField/ objectql's fallback branch are untouched — and the untouched-half pin (MULTI_CAPABLE_TYPES.has('radio')+isMultiValueFieldpromotion) means a future Option-B-by-stealth cleanup trips a test instead of landing silently. objectui untouched; no closing keyword near objectui#4015.deleteBehavior: 'set_null'on amaster_detail, and the engine silently resolves it tocascade#9689 class) is safe by construction and by pin: the check runs aftermultiplematerializes.default(false), sotrueat the refinement is always authored;parse(parse(validRadioField))deep-equal pinned.type:'radio'declarations, none withmultiple:true; the changeset quotes the card's earlier 21-declaration count — both measurements agree on the load-bearing zero; noted, not rework).minor+ explicit BREAKING per the launch-window convention (the feat(service-datasource): external-datasource driver return contract fails compile-time on mis-shaped drivers (#11123 option C) #11492 precedent), ADR-0087not-required (no-migration-prescription)with a sound rationale (a conversion rewriting the pair would itself be the stored-shape migration C exists to avoid), accepted mechanically by the gate. Reverse verification exact (3 refusal tests red pre-patch, 6 accept controls green both sides); dist acceptance probe verbatim; two declared workspace-state narrowings, both CI-covered.Cross-repo relay owed at this ACCEPT — executed in the same stroke: comment posted on objectui#4015 recording that the spec fork is in landing and that the
field-type-alias.ts:28-37prose correction (the sentence whose silent omission ofradiomade this gap invisible) routes to the objectui lane. objectui#4015 stays OPEN as the decision record.needs:contract-reviewcleared from both carriers in this same stroke. Landing: flip ready → every check green → merge queue.Fixes #11437closes this card at merge; hygiene at the landing stroke.Generated by Claude Code
docs(fields): name radio as the arity exception in the MULTI_VALUE_FO…
fix(spec)!: refuse `multiple: true` outside the multi-capable types, …
test(spec): the second data/ file group's test titles state each cite…