Found in the v2.5.0 milestone-merge review (#2215), in code that shipped on v2/main during the milestone (#2137). Filed here rather than fixed in the merge PR, whose tree is byte-identical to origin/v2/main.
The bug
core/json/rootUnion.ts's isOfferable decides whether a root-level anyOf/oneOf branch may be shown in the form's branch picker. It checks that the merged branch has at least one property and that every property value is readable — but it never checks that each name in required actually has a property to render.
So a branch like:
{ "properties": { "kind": { "const": "a" } }, "required": ["kind", "payload"] }
is offered. The form renders kind and nothing for payload, because there is no schema for it. missingRequiredFields (clients/tui/src/utils/schemaToForm.ts, called from ToolTestModal.tsx:135) then correctly reports payload missing at submit — permanently, since the user has no field in which to supply it. The picker offers an option that cannot be completed, and the only escape is to pick a different branch.
This is the gap the function already reasons about, one case short
The existing comment on isOfferable works through exactly this hazard for the JSON Schema boolean form:
JSON Schema's boolean form is legal, but only true is harmless — it constrains nothing and answers every keyword lookup with undefined, while false admits no value whatsoever, so a field declared with it can never be filled and a required one makes the whole branch unsatisfiable.
An undeclared required name has the identical outcome — a required field that can never be filled, so the branch is unsatisfiable — and is not checked. The reasoning is right; its coverage is one case short.
Fix
Add the check to isOfferable: every name in the merged required array must resolve to a renderable property in the merged properties. A branch that fails it is not offerable, for the same reason a false-schema required field is not.
Then make the fallback explicit rather than silent. Declining every branch of a union currently leaves the user with a picker and no valid options, which is a different dead end from the one being fixed. The form should fall back to raw JSON input for the whole argument when no branch is offerable — the raw-JSON toggle from #2151 is already there to be reached for, and #2171 / #2175 settled how raw values are handled without being retyped by the schema.
Note this affects both form builders — the shared core/json helper feeds the web and TUI schema forms — so the fix lands once but wants a test on each side. Cover: a branch with an undeclared required name is not offered; a union where every branch fails falls through to raw input rather than an empty picker; and a branch that declares required over names the root already provides stays offerable (the anyOf: [{ required: ["email"] }, …] case the function's comment calls out, which must not regress).
Reported by Copilot on #2215.
Found in the v2.5.0 milestone-merge review (#2215), in code that shipped on
v2/mainduring the milestone (#2137). Filed here rather than fixed in the merge PR, whose tree is byte-identical toorigin/v2/main.The bug
core/json/rootUnion.ts'sisOfferabledecides whether a root-levelanyOf/oneOfbranch may be shown in the form's branch picker. It checks that the merged branch has at least one property and that every property value is readable — but it never checks that each name inrequiredactually has a property to render.So a branch like:
{ "properties": { "kind": { "const": "a" } }, "required": ["kind", "payload"] }is offered. The form renders
kindand nothing forpayload, because there is no schema for it.missingRequiredFields(clients/tui/src/utils/schemaToForm.ts, called fromToolTestModal.tsx:135) then correctly reportspayloadmissing at submit — permanently, since the user has no field in which to supply it. The picker offers an option that cannot be completed, and the only escape is to pick a different branch.This is the gap the function already reasons about, one case short
The existing comment on
isOfferableworks through exactly this hazard for the JSON Schema boolean form:An undeclared required name has the identical outcome — a required field that can never be filled, so the branch is unsatisfiable — and is not checked. The reasoning is right; its coverage is one case short.
Fix
Add the check to
isOfferable: every name in the mergedrequiredarray must resolve to a renderable property in the mergedproperties. A branch that fails it is not offerable, for the same reason afalse-schema required field is not.Then make the fallback explicit rather than silent. Declining every branch of a union currently leaves the user with a picker and no valid options, which is a different dead end from the one being fixed. The form should fall back to raw JSON input for the whole argument when no branch is offerable — the raw-JSON toggle from #2151 is already there to be reached for, and #2171 / #2175 settled how raw values are handled without being retyped by the schema.
Note this affects both form builders — the shared
core/jsonhelper feeds the web and TUI schema forms — so the fix lands once but wants a test on each side. Cover: a branch with an undeclared required name is not offered; a union where every branch fails falls through to raw input rather than an empty picker; and a branch that declaresrequiredover names the root already provides stays offerable (theanyOf: [{ required: ["email"] }, …]case the function's comment calls out, which must not regress).Reported by Copilot on #2215.