fix: guard the surviving branch's own composition, via one shared predicate
Copilot round 10: one finding, the last position of the rule the previous two
rounds established. A *non-null* branch can carry its own `anyOf`, and the
hoist drops it — the branch's `anyOf` overwrites the wrapper's in the spread
and is then cleared, so `{ anyOf: [{ type: "string", anyOf: [{ const: "a" }] },
{ type: "null" }] }` widened into an unconstrained nullable string.
Rather than add a fourth inline condition, the four sites that ask "does this
schema compose something I cannot evaluate?" now share one predicate,
`hasUnevaluatedComposition` (an opaque applicator, or a nested `anyOf`):
- the surviving branch in the `anyOf` collapse (this round's finding),
- the null branch in `admitsNull`,
- the `type: [T, "null"]` collapse path,
- and, via `hasOpaqueApplicator`, the top of `admitsNull`.
Each of those was missed independently across rounds 8-10 for the same reason:
the guard existed but was spelled out inline at each site and drifted. Sharing
the predicate is the part that stops a fifth site from repeating it.
Signed-off-by: cliffhall <cliff@futurescale.com>
This commit is contained in:
parent
274a328abe
commit
562709a79a
1 changed files with 70 additions and 7 deletions
|
|
@ -450,6 +450,43 @@ describe("normalizeNullableUnion", () => {
|
|||
expect(normalizeNullableUnion(schema)).toBe(schema);
|
||||
});
|
||||
|
||||
// The hoist *drops* a nested union: the branch's `anyOf` overwrites the
|
||||
// wrapper's in the spread and is then cleared, so the constraint vanishes and
|
||||
// the field widens into an unconstrained nullable string.
|
||||
it("declines when the surviving branch carries its own anyOf", () => {
|
||||
const schema = {
|
||||
anyOf: [
|
||||
{ type: "string", anyOf: [{ const: "a" }] },
|
||||
{ type: "null" as const },
|
||||
],
|
||||
};
|
||||
expect(normalizeNullableUnion(schema)).toBe(schema);
|
||||
});
|
||||
|
||||
it("declines when the surviving branch carries an opaque applicator", () => {
|
||||
const withNot = {
|
||||
anyOf: [
|
||||
{ type: "string", not: { const: "a" } },
|
||||
{ type: "null" as const },
|
||||
],
|
||||
};
|
||||
expect(normalizeNullableUnion(withNot)).toBe(withNot);
|
||||
const withAllOf = {
|
||||
anyOf: [
|
||||
{ type: "string", allOf: [{ minLength: 2 }] },
|
||||
{ type: "null" as const },
|
||||
],
|
||||
};
|
||||
expect(normalizeNullableUnion(withAllOf)).toBe(withAllOf);
|
||||
const withOneOf = {
|
||||
anyOf: [
|
||||
{ type: "string", oneOf: [{ const: "a" }] },
|
||||
{ type: "null" as const },
|
||||
],
|
||||
};
|
||||
expect(normalizeNullableUnion(withOneOf)).toBe(withOneOf);
|
||||
});
|
||||
|
||||
it("declines when the wrapper carries an applicator the module cannot read", () => {
|
||||
const schema = {
|
||||
allOf: [{ minLength: 2 }],
|
||||
|
|
|
|||
Loading…
Reference in a new issue