fix: address Copilot review round 6 — additionalProperties, annotations, prototype-safe records

- rootUnion: decline a union that adds fields under a restrictive root
  `additionalProperties`. The keyword constrains what its SIBLING
  `properties` does not name, so the original schema admits none of the
  branch fields; flattening moves them beside the keyword, where they read as
  allowed and the form would submit what the schema forbids.
- `default` joins the annotation set: it constrains nothing, so two
  declarations suggesting different initial values no longer conflict.
- Property records are read with `Object.hasOwn` and built with
  `fromEntries`/`defineProperty`. `properties` is a JSON record, so
  `constructor` is a legal argument name that `in` reported as a collision
  the root never made, and `__proto__` is one that assignment would drop
  into the legacy prototype setter instead of keeping as a field.
- SchemaForm: a value survives a branch switch unless BOTH branches declare
  the name. A root argument the outgoing branch merely specialized is still a
  root argument in a branch that inherits it, so dropping it erased a valid
  value.

Signed-off-by: cliffhall <cliff@futurescale.com>
This commit is contained in:
cliffhall 2026-08-26 02:56:55 -04:00
commit c15e30414f

View file

@ -165,7 +165,12 @@ function coercionProperties(
return { ...matching[0].schema.properties };
}
const properties: Record<string, unknown> = { ...base.properties };
// `hasOwn`/`fromEntries` rather than `in`/assignment throughout: `properties`
// is a JSON record, so `constructor` and `__proto__` are legal argument names
// that the prototype chain and the legacy setter would otherwise mishandle.
const properties: Record<string, unknown> = Object.fromEntries(
Object.entries(base.properties ?? {}),
);
for (const name of new Set(branches.flatMap((b) => b.declaredFields))) {
// Only the branches that *declare* the name have an opinion about it — a
// branch that merely inherited the root's declaration is not a second,
@ -176,7 +181,12 @@ function coercionProperties(
.map((branch) => branch.schema.properties?.[name]);
const types = new Set(declarations.map((schema) => typeNameOf(schema)));
if (types.size === 1) {
properties[name] = declarations[0];
Object.defineProperty(properties, name, {
value: declarations[0],
writable: true,
enumerable: true,
configurable: true,
});
} else {
delete properties[name];
}