fix(ai): also strip sibling type-specific keys during CCA mixed-type collapse

Address review feedback:
- Replace `as string` assertion with typed `chosenType` local
- Strip sibling keys from nextSchema that were copied via
  copySchemaWithout but belong to a type other than the chosen one
  (e.g. sibling `items` on a now-string-typed schema)
- Export ALL_CCA_TYPE_SPECIFIC_KEYS from fields.ts for sibling filtering
- Add regression test for the sibling-key edge case
This commit is contained in:
basedcorp99
2026-06-06 19:08:07 +02:00
parent dee4db1602
commit 7c8fb4d8f6
3 changed files with 50 additions and 3 deletions
+16
View File
@@ -154,6 +154,22 @@ export const CLOUD_CODE_ASSIST_TYPE_SPECIFIC_KEYS: Record<string, Record<string,
null: {},
};
/**
* Flat set of every type-specific key across all CCA types.
* Used to identify sibling keys that need filtering during mixed-type collapse.
*/
export const ALL_CCA_TYPE_SPECIFIC_KEYS: Record<string, true> = buildAllCcaTypeSpecificKeys();
function buildAllCcaTypeSpecificKeys(): Record<string, true> {
const all: Record<string, true> = {};
for (const typeKeys of Object.values(CLOUD_CODE_ASSIST_TYPE_SPECIFIC_KEYS)) {
for (const key in typeKeys) {
all[key] = true;
}
}
return all;
}
/**
* Cloud Code Assist shared schema keys allowed on any type.
* Used alongside CLOUD_CODE_ASSIST_TYPE_SPECIFIC_KEYS for CCA combiner collapsing.
+19 -3
View File
@@ -11,6 +11,7 @@ import { dereferenceJsonSchema } from "./dereference";
import { upgradeJsonSchemaTo202012 } from "./draft";
import { areJsonValuesEqual, mergePropertySchemas } from "./equality";
import {
ALL_CCA_TYPE_SPECIFIC_KEYS,
CLOUD_CODE_ASSIST_SHARED_SCHEMA_KEYS,
CLOUD_CODE_ASSIST_TYPE_SPECIFIC_KEYS,
COMBINATOR_KEYS,
@@ -501,11 +502,26 @@ function collapseMixedTypeCombinerVariants(schema: JsonObject, combiner: "anyOf"
if (variantTypes.length < 2 || variantTypes.every(type => type === "object")) {
return schema;
}
const nextSchema = copySchemaWithout(schema, combiner);
const nonNullTypes = variantTypes.filter(t => t !== "null");
nextSchema.type = nonNullTypes[0] ?? variantTypes[0];
const chosenTypeAllowedKeys = CLOUD_CODE_ASSIST_TYPE_SPECIFIC_KEYS[nextSchema.type as string] ?? {};
const chosenType: string = nonNullTypes[0] ?? variantTypes[0];
nextSchema.type = chosenType;
const chosenTypeAllowedKeys = CLOUD_CODE_ASSIST_TYPE_SPECIFIC_KEYS[chosenType] ?? {};
// Strip sibling keys that were copied from the parent and belong to a
// different type (e.g. `items` sibling on a now-string-typed schema).
for (const key in nextSchema) {
if (!Object.hasOwn(nextSchema, key)) continue;
if (key === "type") continue;
if (
Object.hasOwn(ALL_CCA_TYPE_SPECIFIC_KEYS, key) &&
!Object.hasOwn(chosenTypeAllowedKeys, key) &&
!Object.hasOwn(CLOUD_CODE_ASSIST_SHARED_SCHEMA_KEYS, key)
) {
delete nextSchema[key];
}
}
for (const key in mergedVariantFields) {
if (!Object.hasOwn(mergedVariantFields, key)) continue;
// Drop type-specific keys that don't belong to the chosen type
@@ -979,6 +979,21 @@ describe("normalizeSchemaForCCA", () => {
description: "pr number, url, or branch",
});
});
it("strips sibling type-specific keys copied from parent when mixed-type collapse picks opposing type", () => {
// Edge case: parent has a sibling `items` outside the anyOf,
// and the chosen type is string. The sibling must be stripped.
const normalized = normalizeSchemaForCCA({
anyOf: [{ type: "string" }, { type: "array", items: { type: "number" } }],
items: { type: "string" },
description: "pr number, url, or branch",
});
expect(normalized).toEqual({
type: "string",
description: "pr number, url, or branch",
});
});
});
// ---------------------------------------------------------------------------