fix: address Copilot review round 37 — stale constants, inherited answers, prefixed labels

- The seeding effect re-applies constants as well as filling absences: an
  in-place schema change can move a `const` the user cannot edit, and the
  read-only field then displayed the new value while `values` still held the
  old one — which is what would be submitted. Ordinary defaults are still only
  ever added, so an edited field keeps what the user put there.
- hasMissingIn checks own-property presence: a required argument legally named
  `constructor` resolved to the inherited one and read as supplied, enabling
  a submit the schema rejects.
- The TUI keeps the declared name as a branch field's fallback title. The
  prefix is an internal field NAME, and buildFields labels from its map key —
  so the showcase displayed `__b0__address` where the schema says
  `address`.

Signed-off-by: cliffhall <cliff@futurescale.com>
This commit is contained in:
cliffhall 2026-08-26 14:50:52 -04:00
commit 85b030cbde
2 changed files with 120 additions and 5 deletions

View file

@ -465,6 +465,35 @@ describe("schemaToForm", () => {
]);
});
it("labels a branch field by the name the schema declared", () => {
// The prefix is an internal field name; `buildFields` falls back to its
// map key for a label, so the user would otherwise see `__b0__address`.
const form = schemaToForm(UNION, "union_tool");
expect(form.sections[1]!.fields[1]).toMatchObject({
name: "__b0__address",
label: "address",
});
});
it("keeps a declared title ahead of the fallback", () => {
const form = schemaToForm(
{
type: "object",
anyOf: [
{
type: "object",
properties: { a: { type: "string", title: "Street address" } },
},
{ type: "object", properties: { b: { type: "string" } } },
],
},
"titled",
);
expect(form.sections[1]!.fields[0]).toMatchObject({
label: "Street address",
});
});
it("offers a variant select listing the alternatives", () => {
const form = schemaToForm(UNION, "union_tool");
expect(form.sections[0]!.fields[0]).toMatchObject({

View file

@ -143,6 +143,19 @@ function branchFields(
return [...new Set([...own, ...sharedFieldNames(base, branches)])];
}
/**
* A property declaration carrying the name it was declared under as its
* fallback `title`, so a renamed field still displays the schema's own name.
*/
function labelled(property: unknown, name: string): unknown {
if (typeof property !== "object" || property === null) {
// JSON Schema's `true` constrains nothing, so a title-only object says the
// same thing and carries the label.
return { title: name };
}
return { title: name, ...property };
}
/** The `const` a property schema pins its value to, if any. */
function constOf(schema: unknown): unknown {
if (typeof schema !== "object" || schema === null) return undefined;
@ -217,7 +230,11 @@ export function schemaToForm(
const properties = Object.fromEntries(
branchFields(base, branches, index).map((name) => [
branchFieldName(prefix, index, name),
branch.schema.properties?.[name],
// The prefix is an internal field NAME, never a label: `buildFields`
// falls back to its map key when a declaration carries no `title`, so
// without this the form would show `__b0__address` where the schema
// says `address`.
labelled(branch.schema.properties?.[name], name),
]),
);
sections.push({