Four findings, all valid:
1. Returning `T` was unsound. The collapse genuinely changes two fields —
`type` stops being an array, `anyOf` becomes `undefined` — so a caller
whose `T` typed either that way could keep treating it so afterwards.
The result is now `T | NormalizedNullableUnion<T>`, which states exactly
that: `Omit<T, "type" | "anyOf">` plus a `RenderableType` `type`, a
cleared `anyOf`, and `nullable`. The one assertion left is isolated in a
`collapsed()` helper, needed only because `Omit` stays deferred while `T`
is open.
2. A typeless `enum` does not imply strings. JSON Schema allows
`enum: [1, 2]`, and inferring `"string"` for it would hand numbers to a
`Select` declared `string[]`, and make the TUI submit `"1"` for `1`. Now
only an all-string enum earns the inference; anything else stays on the
JSON fallback. An explicit `type` is still authoritative.
3. `hasMissingRequiredFields` counted every null as missing, so clearing a
*required* nullable enum — newly possible via the clear button — would
disable submit on a value the schema permits. `required` constrains
presence, not content, so null now counts as present exactly when the
field's schema admits it.
4. `collectSchemaDefaults` read the raw union, so a default nested inside a
nullable object's branch was displayed by the form but never seeded into
the submitted values. It normalizes each property first, like the
renderer does.
Signed-off-by: cliffhall <cliff@futurescale.com>
Closes#2015, folded into #1928 rather than left as a follow-up.
The TUI's `schemaToForm` has the same defect the web `SchemaForm` had: its
local `JsonSchemaProperty` declares only `type?: string` — no `anyOf`, no
array-valued `type` — so a `.nullish()` argument has no top-level `type` or
`enum` for the dispatcher to read and falls to the `switch` default, losing
its select. Milder than the web bug (a plain text field, not a corrupting
JSON textarea), but the same class.
Fixing it in a second place is the argument for not having the logic in a
first place. So the collapse moves to `core/json/nullableUnion.ts` as
`normalizeNullableUnion`, generic over a minimal `NullableUnionSchema`
shape — the least a caller must expose to be *recognized* as a nullable
union — so neither client has to adopt the other's schema type. Both form
builders now share one copy and cannot drift on which schemas they render.
- `core/json/nullableUnion.ts` — the shared collapse, plus tests at
`clients/web/src/test/core/nullableUnion.test.ts` (the web suite is what
gates `core/`).
- `clients/web`: `schemaUtils.normalizeUnionType` is gone; `SchemaForm` and
`resolveRefsInMessage` call the core function directly.
- `clients/tui`: `schemaToForm` normalizes each property before dispatch,
and `JsonSchemaProperty` grows the `anyOf` / array-`type` fields the
encoding uses.
Verified by driving the built TUI under a pty against the
`nullable-fields-http.json` showcase server. Before, every argument of
`record_shipment` rendered as a plain text field; after, `direction` is a
select ("No value"), `quantity` an integer, `express` a boolean
("[Not set]").
Signed-off-by: cliffhall <cliff@futurescale.com>