fix: address Copilot review round 2 — conjunctive merges, generated-name collisions, CLI branch selection

- rootUnion: a name both the root and a branch declare is now MERGED rather
  than replaced, so a root `minimum` survives a branch `maximum`; a branch
  that contradicts the root on a property's `type` describes an unsatisfiable
  value and is declined outright. Branch offerability also requires that
  `properties` really is an object (a malformed `properties: null` threw)
  and that the member's own `type` admits objects.
- rootUnion: `ownFields` becomes `declaredFields` — a branch commonly
  specializes a root property, and consumers must render the specialization.
- TUI: the generated select and branch prefixes step aside from any name the
  schema itself declares (JSON has no reserved namespace), decode filters them
  by exact name, a branch's specialization renders in its own section, and a
  `const` is rendered as a one-option select whose typed value is restored
  from the schema on submit.
- convertToolParameters: identify the branch from the discriminator constants
  the call supplies; when none identifies one, coerce only names every
  declaring branch types the same way rather than picking an arbitrary branch.

Signed-off-by: cliffhall <cliff@futurescale.com>
This commit is contained in:
cliffhall 2026-08-26 01:41:17 -04:00
commit 84cd0c10f0

View file

@ -304,9 +304,11 @@ The **TUI** has the same gap and is worth checking against the same server (`--t
The sections are not as independent as they look, which is why the select is not cosmetic: ink-form keeps one value object for the whole form, keyed by field name alone, so two branches both declaring `kind` would be **one** field and the later section's initial value would decide what the earlier one submits. Each branch's fields are therefore rendered under a prefixed name and translated back on submit, where every branch but the chosen one is dropped.
The **CLI** has no form at all, but the same flattening decides how `--tool-arg` values are typed: a branch's `count: { "type": "number" }` is what turns `--tool-arg count=3` into `3` rather than `"3"`. All three read one helper, [`core/json/rootUnion.ts`](./core/json/rootUnion.ts), so they cannot drift on which schemas they can render.
The **CLI** has no form at all, but the same flattening decides how `--tool-arg` values are typed: a branch's `count: { "type": "number" }` is what turns `--tool-arg count=3` into `3` rather than `"3"`. Which branch is *inferred* rather than chosen — a discriminated union pins its discriminator with `const`, so the supplied arguments either identify one branch or they do not. When nothing identifies one, only the names every branch that declares them types the same way are coerced; a name one branch calls a number and another a boolean is passed through as the string it was typed as, rather than run through an arbitrary branch's schema.
Three things it deliberately does **not** do, each falling back to whatever the root `properties` describe rather than claiming something untrue. A union whose members are not all field-carrying object schemas is left alone rather than offered as a picker with options that render nothing. A schema carrying **both** `oneOf` and `anyOf` is declined outright: they are independent keywords a value satisfies together, not two spellings of one union, so reading one and dropping the other builds a form that silently omits real constraints — and satisfying both honestly means offering the cross product of their alternatives, which no real schema has yet asked for. And `not` is not interpreted at all: there is no faithful form for "anything except this".
All three read one helper, [`core/json/rootUnion.ts`](./core/json/rootUnion.ts), so they cannot drift on which schemas they can render.
Four things it deliberately does **not** do, each falling back to whatever the root `properties` describe rather than claiming something untrue. A union whose members are not all field-carrying object schemas is left alone rather than offered as a picker with options that render nothing — as is one whose member `type` rules objects out, since tool arguments are a JSON object and such a member can never match. A union whose branch **contradicts** the root about a property's `type` is declined too: the two constraints are conjunctive, so `string` under a root `number` describes a value that cannot exist, and rendering either type would accept what the schema rejects. (A property both declare *compatibly* is merged rather than replaced, so a root's `minimum` survives a branch's `maximum`.) A schema carrying **both** `oneOf` and `anyOf` is declined outright: they are independent keywords a value satisfies together, not two spellings of one union, so reading one and dropping the other builds a form that silently omits real constraints — and satisfying both honestly means offering the cross product of their alternatives, which no real schema has yet asked for. And `not` is not interpreted at all: there is no faithful form for "anything except this".
#### Unportable tool schemas