diff --git a/docs/current-handoff.md b/docs/current-handoff.md index ef564e7..4fe805a 100644 --- a/docs/current-handoff.md +++ b/docs/current-handoff.md @@ -37,7 +37,8 @@ If YES, the next live experiment is one timed/costed OpenAI UI investigation mea - The OpenAI provider now honors a caller-supplied structured-output schema, applying its existing strict-schema transport projection; absent an alternate schema, initial reconstruction retains its existing strict schema and transport normalization. - The next live run exposed incomplete recursive strict projection: OpenAI rejected `relationships.items` because it lacked `additionalProperties: false`. The projector now recognizes every `type: "object"` node, including property-less objects in array items, and recursively enforces strict object schemas while preserving initial-reconstruction optionality/nullability behavior. -- This deterministically fixes the focused-deconstruction transport failures. Provider and focused-route suites pass with zero live calls. +- The latest Terra request then exposed an inconsistent root `properties`/`required` contract. The projector now derives `required` after projection from the surviving property keys, and recursive tests verify `properties`, `required`, and `additionalProperties` consistency. Property-less object strictness and initial-reconstruction transport behavior remain preserved. +- Provider and focused-route suites pass with zero live calls. - Next boundary: retry one fresh real Playwright-driven OpenAI/Terra investigation; do not reuse the failed focused-deconstruction attempt. ## Repository checkpoint diff --git a/lib/llm/provider.js b/lib/llm/provider.js index 9fa2182..2ac5dea 100644 --- a/lib/llm/provider.js +++ b/lib/llm/provider.js @@ -171,7 +171,6 @@ function projectOpenAIStrictSchema(schema, zodSchema, rootSchema, visited) { } } } - if (resolved.properties) resolved.required = Object.keys(resolved.properties); resolved.additionalProperties = false; } @@ -182,6 +181,7 @@ function projectOpenAIStrictSchema(schema, zodSchema, rootSchema, visited) { for (const [key, property] of Object.entries(resolved.properties)) { projectOpenAIStrictSchema(property, shape?.[key], rootSchema, visited); } + resolved.required = Object.keys(resolved.properties); } for (const branch of [ ...(resolved.anyOf ?? []), diff --git a/tests/llm/provider.test.js b/tests/llm/provider.test.js index adcf95c..9d85d2d 100644 --- a/tests/llm/provider.test.js +++ b/tests/llm/provider.test.js @@ -190,6 +190,30 @@ describe("OllamaLlmProvider chat capability detection", () => { }); describe("OpenAI reconstruction provider experiment seam", () => { + function assertStrictObjectInvariant(schema, root = schema, visited = new Set()) { + if (!schema || typeof schema !== "object" || visited.has(schema)) return; + visited.add(schema); + + if (schema.properties) { + expect(schema.additionalProperties).toBe(false); + expect(new Set(schema.required)).toEqual(new Set(Object.keys(schema.properties))); + Object.values(schema.properties).forEach((property) => + assertStrictObjectInvariant(property, root, visited), + ); + } + if (schema.items) assertStrictObjectInvariant(schema.items, root, visited); + for (const branch of [ + ...(schema.anyOf ?? []), + ...(schema.oneOf ?? []), + ...(schema.allOf ?? []), + ]) { + assertStrictObjectInvariant(branch, root, visited); + } + for (const definition of Object.values(schema.$defs ?? schema.definitions ?? {})) { + assertStrictObjectInvariant(definition, root, visited); + } + } + it("projects canonical optional fields as required but nullable", () => { const nativeSchema = z.toJSONSchema(reconstructionV2Schema); const projectedSchema = createOpenAIStrictSchema( @@ -351,6 +375,10 @@ describe("OpenAI reconstruction provider experiment seam", () => { relationships: [], possibleFollowUpQuestions: [], }); + expect(new Set(schema.required)).toEqual(new Set(Object.keys(schema.properties))); + expect(schema.properties).toHaveProperty("relationships"); + expect(schema.required).toContain("relationships"); + assertStrictObjectInvariant(schema); }); it("recursively projects arbitrary nested object schemas as strict", () => {