diff --git a/src/provider-schema.ts b/src/provider-schema.ts index a43bba7..84eb765 100644 --- a/src/provider-schema.ts +++ b/src/provider-schema.ts @@ -40,6 +40,11 @@ function stripProviderUnsupportedSchemaKeywords(value: unknown): unknown { } output[key] = stripProviderUnsupportedSchemaKeywords(item); } + const nullableType = nullableAnyOfType(output["anyOf"]); + if (nullableType !== null && Object.keys(output).length === 1) { + output["type"] = [nullableType, "null"]; + delete output["anyOf"]; + } if (output["type"] === "object" && isRecord(output["properties"])) { output["additionalProperties"] = false; output["required"] = Object.keys(output["properties"]); @@ -47,6 +52,22 @@ function stripProviderUnsupportedSchemaKeywords(value: unknown): unknown { return output; } +function nullableAnyOfType(value: unknown): string | null { + if (!Array.isArray(value) || value.length !== 2) { + return null; + } + const typeSchemas = value.filter(isSingleTypeSchema); + const nullable = typeSchemas.find((schema) => schema["type"] === "null"); + const typed = typeSchemas.find((schema) => schema["type"] !== "null"); + return typeSchemas.length === 2 && nullable !== undefined && typeof typed?.["type"] === "string" + ? typed["type"] + : null; +} + +function isSingleTypeSchema(value: unknown): value is { type: string } { + return isRecord(value) && typeof value["type"] === "string" && Object.keys(value).length === 1; +} + function isRecord(value: unknown): value is Record { return typeof value === "object" && value !== null && !Array.isArray(value); } diff --git a/src/provider.test.ts b/src/provider.test.ts index da89419..510ece5 100644 --- a/src/provider.test.ts +++ b/src/provider.test.ts @@ -384,6 +384,20 @@ describe("providerJsonSchema", () => { } }); + it("normalizes nullable evidence line schemas for Codex strict schemas", () => { + const schema = reviewJsonSchema as Record; + const findings = propertySchema(schema, "findings"); + const finding = itemSchema(findings); + const evidence = itemSchema(propertySchema(finding, "evidence")); + const endLine = propertySchema(evidence, "endLine"); + + expect(endLine["anyOf"]).toBeUndefined(); + expect(endLine["type"]).toEqual(["integer", "null"]); + expect(schemaKeys(endLine)).not.toContain("minimum"); + expect(evidence["additionalProperties"]).toBe(false); + expect(evidence["required"]).toEqual(Object.keys(propertiesOf(evidence))); + }); + it("keeps object schemas strict even when parser input fields are optional", () => { const schema = providerJsonSchema(reviewOutputSchema) as Record; const findings = propertySchema(schema, "findings");