Uh oh!
There was an error while loading. Please reload this page.
feat(spec): refuse authored radio + multiple:true at the schema layer (ruled Option C, objectui#4015) - #11560
Conversation
… (ruled Option C) FieldSchema's superRefine now rejects the authored combination with a diagnostic naming the field, the illegal pair, and the three correctly-named multi-choice types (checkboxes / multiselect / tags). MULTI_CAPABLE_TYPES and isMultiValueField stay untouched per the same ruling (at-rest data keeps its read path; a pin trips a future cleanup), and the objectql record-validator select/radio branch stays as a data-safety fallback. multiple materializes .default(false), so the refusal can only fire on an authored true and parse(parse(x)) stays stable (pinned). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01T9cDbY2NBiVJWYx3BpWfH2
📓 Docs Drift CheckThis PR changes 1 package(s): 10 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 1 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 126 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin d1b9cd46f235fc28d96e9fc0478b72c77ef6846f && git checkout d1b9cd46f235fc28d96e9fc0478b72c77ef6846f
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 033a34c7c42cdae90bc4d18ca0bdcbd725df631b 72b3d9c8865cc80a76757878e12e4e48a8012280 && git checkout -B drift-repro 033a34c7c42cdae90bc4d18ca0bdcbd725df631b && git merge --no-ff 72b3d9c8865cc80a76757878e12e4e48a8012280
node scripts/docs-audit/affected-docs.mjs --json 033a34c7c42cdae90bc4d18ca0bdcbd725df631b
|
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
What this PR does
Implements the maintainer ruling recorded 2026-08-22 on objectui#4015 (decision-inbox digest, batch accepted verbatim: 「接受所有」):
objectui#4015 remains open as the decision record — nothing in this PR closes it.
The defect being ruled on
An author could declare
{ type: 'radio', multiple: true }. The data layer honoured it (stored an array, validated as multi, split on import, inferred action-param arity) while the widget rendered a single-value radio group with zero diagnostics — declared multi, rendered single. The producer contradicted itself aboutradio(re-verified at this branch's merge based10e214626, matching the card's measurement at5d163792c):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:204-206MULTI_CAPABLE_TYPESincludesradio, its own comment conceding it "shares the select branch"packages/spec/src/data/field-value.zod.ts:224-226isMultiValueFieldpromotes anyMULTI_CAPABLE_TYPESmember onmultiple: truepackages/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, read-only, verified at the local checkout):
RadioField.tsxhas 0 occurrences ofmultiple(control probe 23 forRadioGroup|value), andparamValueShape.ts:142pinsradioascardinality: 'scalar'.Occurrence re-measurement at the merge base: both repos scanned; 18
type: 'radio'declarations found by this branch's scanner, 0 carryingmultiple: truein a ±6-line window. The single window hit is the knownselect + multipleline adjacent to aradioentry in objectuiparamValueShape.test.ts:93— the same false positive the card documents. The flip-to-B condition (deployed tenant metadata with stored data) has not been triggered by anything measurable from here.Where the refusal homes, and the diagnostic
FieldSchema's.superRefineinpackages/spec/src/data/field.zod.ts— the same seam that carries the module's sibling authored-combination refusals (referenceViaon a non-text type,referenceVia+reference,storage.notNull+requiredWhen, the #7918 currency-precision contradiction). This is the parse path every publish crosses, measured rather than assumed:kernel/metadata-type-schemas.ts:91registersfield: FieldSchemafor the standalone metadata type, andObjectSchema'sfieldsrecord embedsFieldSchemadirectly (object.zod.ts:1894), so both publish shapes hit the refinement. No.extend/.omit/.pickderivative ofFieldSchemaexists that could strip it.The diagnostic (issue
path: ['multiple'], verified verbatim against the built dist):It names the field, names the illegal pair, and prescribes all three correctly-named multi-choice types.
Parse-idempotency (#9689 class — mandated check)
multipleis declared.default(false)(field.zod.ts:818), i.e. the default materializes at parse — but it materializesfalse, nevertrue. The.superRefineruns over the parsed object, so amultiple: trueit observes can only ever be authored; a defaulted value can never trip the refusal. Pinned by test:parse(parse(validRadioField))is deep-equal stable, and the materializedmultiple: falsere-parses cleanly as an authoredfalse. No mainline parse path materializestrue, so the refusal required no authored-vs-defaulted machinery (unlike the #7918.overwrite()relocation one block below it).Untouched halves (binding constraints of the ruling)
MULTI_CAPABLE_TYPESandisMultiValueField(field-value.zod.ts) — untouched. At-rest data keeps its read path; no stored-shape migration is paid. A new test pinsMULTI_CAPABLE_TYPES.has('radio')andisMultiValueField({ type: 'radio', multiple: true }) === trueso a future "cleanup" trips loudly instead of landing as Option B by accident.packages/objectql/src/validation/record-validator.ts's(t === 'select' || t === 'radio')branch (line 656) — untouched, stays as a data-safety fallback for stock.field-type-alias.ts:28-37prose correction the ruling assigns to the landing fork is deliberately not in this PR: it is objectui-side, and the PM relays it at ACCEPT per the dispatch brief.multiple's published.describe()("Applicable for select, lookup, file, image") — untouched:radiowas already absent from it, consistent with the refusal; nothing in it is falsified by this change (confirmed bycheck:docsgreen with no regeneration needed).Changeset reasoning
.changeset/radio-multiple-refused-at-schema.md—@objectstack/spec: minorwith an explicit BREAKING body, per the launch-window convention (mirroring the #11124/#11492 spelling). ADR-0087 disposition:not-required (no-migration-prescription)— the ruling explicitly declines a migration (that is why C beat B), re-measurement found zero occurrences of the refused combination in either repo, and a conversion rewriting the pair would itself be the stored-shape migration Option C was chosen to avoid.node scripts/check-adr-0087-registration.mjsaccepts the disposition (exit 0). This follows the closest sibling precedent (#7918: a ruled contradiction-refusal in the same superRefine, registered nothing) rather than #8321 (which registered a conversion because it had stored rows to keep loading — the ruling here forbids exactly that move).Verification (all at commit
72b3d9c886, via the shared verify lock)pnpm --filter @objectstack/spec build— VERDICT command-exit 0.pnpm --filter @objectstack/spec test— 420 files / 11213 tests passed, VERDICT command-exit 0, run after the final commit.pnpm --filter @objectstack/spec typecheck— green (tsc + scripts + test programs; the check:test-typecheck self-test line printed its pass).pnpm --filter @objectstack/spec check:generated— "All 14 generated artifacts are up to date" (no artifact moved: the refusal adds no describe, no export, no authorable key).git checkout HEAD, porcelain clean, marker 2, empty diff vs HEAD), re-run 9/9 green.@objectstack/spec/datapackage exports from a consumer package: radio+multiple red with the verbatim prescriptive message on path['multiple']; radio-without-multiple green withmultiple: falsematerialized; untouched-half probes true.node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack(derivation attested at72b3d9c886,--repoassertion held): 26 derived/convention gates run, 25 to verdict green — includingcheck:adr-0087-registration,check:changeset-no-major,check:empty-changeset,check:cross-package-test-inputs,check:engine-double-contract,check:where-matcher,check:test-source-alias,check:nul-bytes, andcheck:doc-formula-expressions(after building@objectstack/formula, whose missing dist was worktree build state, not this diff).check:dev-prereqs(wants all 67 package dists on disk; spec's own dist is built and content-fresh — its spec-freshness half is the only half this diff touches) andcheck:type-check-debt --re-measure(needs the full workspace closure built; the slice this diff can move — spec's own programs including the new test code — is proven green by spec'stypecheck).Review status
Draft, held for the PM's contract review (clause-②) — this PR changes accept/reject behaviour on the contract surface and does not land until that review; please do not flip it ready or queue it.
Fixes#11437
Generated by Claude Code