From 188dd04ab940808243a5c08dd4a172bbe79c3cc6 Mon Sep 17 00:00:00 2001 From: robbond Date: Sun, 6 Sep 2026 14:47:23 +0100 Subject: [PATCH] fix(confidence-engine): unwrap focused deconstruction response --- .../deconstruct/route.js | 17 ++- docs/current-handoff.md | 14 ++ tests/focused-deconstruct-boundary.test.js | 121 +++++++++++++----- 3 files changed, 116 insertions(+), 36 deletions(-) diff --git a/app/api/focused-investigation/deconstruct/route.js b/app/api/focused-investigation/deconstruct/route.js index c7c092b..528c96f 100644 --- a/app/api/focused-investigation/deconstruct/route.js +++ b/app/api/focused-investigation/deconstruct/route.js @@ -52,11 +52,14 @@ export async function POST(request) { const provider = getProvider(); const startedAt = Date.now(); - const raw = await provider.generateReconstruction(prompt, process.env.OLLAMA_MODEL); + const wrapper = await provider.generateReconstruction(prompt, process.env.OLLAMA_MODEL); const elapsedMs = Date.now() - startedAt; + // Unwrap the semantic deconstruction from the provider envelope. + const deconstruction = wrapper.response; + // Validate schema (required fields present, no graph-mutation fields) - const validationErrors = validateFocusedDeconstructSchema(raw); + const validationErrors = validateFocusedDeconstructSchema(deconstruction); if (validationErrors.length > 0) { return Response.json( { @@ -73,11 +76,11 @@ export async function POST(request) { return Response.json({ success: true, targetNodeId: body.targetNodeId, - observations: raw.observations, - uncertainties: raw.uncertainties, - assumptions: raw.assumptions, - relationships: raw.relationships, - possibleFollowUpQuestions: raw.possibleFollowUpQuestions, + observations: deconstruction.observations, + uncertainties: deconstruction.uncertainties, + assumptions: deconstruction.assumptions, + relationships: deconstruction.relationships, + possibleFollowUpQuestions: deconstruction.possibleFollowUpQuestions, elapsedMs, }); } catch (e) { diff --git a/docs/current-handoff.md b/docs/current-handoff.md index fc13d24..88246e7 100644 --- a/docs/current-handoff.md +++ b/docs/current-handoff.md @@ -1883,3 +1883,17 @@ The current handoff captures all operational facts needed to resume today. For h | Task routing by work type | `docs/task-context-packs.md` | | Broader architectural intent | `docs/architectural-principles.md` | | Experiment history (specific) | `docs/design-evolution/README.md` → relevant chapter | + +## focused-deconstruction 502 root cause and fix + +**Root cause:** `/api/focused-investigation/deconstruct/route.js` passed the full provider envelope `{ response, providerApiPath, providerExecution }` from `generateReconstruction()` directly into `validateFocusedDeconstructSchema()`. The validator expects semantic fields (`targetNodeId`, `observations`, etc.) at the top level — those fields live on `wrapper.response`, not on the wrapper. All six focused-deconstruction fields appeared absent, producing a structured-output 502 on every call. + +**Fix:** Route now extracts `const deconstruction = wrapper.response` and passes that inner object to both validation and response serialization. Provider diagnostics (`providerApiPath`, `providerExecution`) are preserved but do not interfere with semantic fields. + +**Tests:** `tests/focused-deconstruct-boundary.test.js` mocks now reflect the real provider wrapper contract (inner deconstruction nested under `.response`). A new assertion verifies that provider envelope fields never leak into the API response. Five tests pass on first run; 48 focused-investigation-boundary tests pass. + +**Previous two focused-deconstruction semantic runs remain invalid as semantic evidence.** They produced all-zero semantic fields because the defect prevented the model output from ever reaching the validator or the client. + +**Next boundary:** rerun exactly one substantive compound-question answer-deconstruction observation through the corrected production route. + +**Zero live model calls occurred during the fix.** diff --git a/tests/focused-deconstruct-boundary.test.js b/tests/focused-deconstruct-boundary.test.js index 1b57d2e..6e028ea 100644 --- a/tests/focused-deconstruct-boundary.test.js +++ b/tests/focused-deconstruct-boundary.test.js @@ -13,18 +13,22 @@ import { describe, it, expect, vi } from "vitest"; function makeMockProvider(inventedTargetNodeId) { return { generateReconstruction: vi.fn().mockResolvedValue({ - targetNodeId: inventedTargetNodeId, - observations: ["doc is minimal", "processes in founder's head"], - uncertainties: ["whether formal docs can capture tacit knowledge"], - assumptions: ["documentation is primary mechanism for knowledge transfer"], - relationships: [ - { from: "founder", to: "processes", type: "holds", rationale: "tacit" }, - { from: "ops-context", to: "docs-infra", type: "depends_on", rationale: "formal docs required" }, - ], - possibleFollowUpQuestions: [ - "What processes does the founder hold tacitly?", - "How is knowledge transferred when founder is unavailable?", - ], + response: { + targetNodeId: inventedTargetNodeId, + observations: ["doc is minimal", "processes in founder's head"], + uncertainties: ["whether formal docs can capture tacit knowledge"], + assumptions: ["documentation is primary mechanism for knowledge transfer"], + relationships: [ + { from: "founder", to: "processes", type: "holds", rationale: "tacit" }, + { from: "ops-context", to: "docs-infra", type: "depends_on", rationale: "formal docs required" }, + ], + possibleFollowUpQuestions: [ + "What processes does the founder hold tacitly?", + "How is knowledge transferred when founder is unavailable?", + ], + }, + providerApiPath: "/api/chat", + providerExecution: { chatRequestAttempted: true }, }), }; } @@ -85,12 +89,16 @@ describe("focused-deconstruct targetNodeId identity boundary", () => { vi.doMock("@/lib/llm/provider", () => ({ getProvider: () => ({ generateReconstruction: vi.fn().mockResolvedValue({ - targetNodeId: "some-invented-id", - observations: mockObs, - uncertainties: mockUnc, - assumptions: mockAssm, - relationships: mockRel, - possibleFollowUpQuestions: mockFuq, + response: { + targetNodeId: "some-invented-id", + observations: mockObs, + uncertainties: mockUnc, + assumptions: mockAssm, + relationships: mockRel, + possibleFollowUpQuestions: mockFuq, + }, + providerApiPath: "/api/chat", + providerExecution: { chatRequestAttempted: true }, }), }), })); @@ -158,17 +166,21 @@ describe("focused-deconstruct targetNodeId identity boundary", () => { vi.doMock("@/lib/llm/provider", () => ({ getProvider: () => ({ generateReconstruction: vi.fn().mockResolvedValue({ - targetNodeId: modelInventedId, - observations: ["Documentation is minimal."], - uncertainties: [], - assumptions: [ - "That formal documentation is the primary mechanism for capturing or transferring the founder's tacit knowledge of processes.", - ], - relationships: [ - { from: "Founder", to: "Processes", type: "holds" }, - { from: "Operational Context", to: "Documentation Infrastructure", type: "affects" }, - ], - possibleFollowUpQuestions: [], + response: { + targetNodeId: modelInventedId, + observations: ["Documentation is minimal."], + uncertainties: [], + assumptions: [ + "That formal documentation is the primary mechanism for capturing or transferring the founder's tacit knowledge of processes.", + ], + relationships: [ + { from: "Founder", to: "Processes", type: "holds" }, + { from: "Operational Context", to: "Documentation Infrastructure", type: "affects" }, + ], + possibleFollowUpQuestions: [], + }, + providerApiPath: "/api/chat", + providerExecution: { chatRequestAttempted: true }, }), }), })); @@ -217,4 +229,55 @@ describe("focused-deconstruct targetNodeId identity boundary", () => { const stored = { ...json }; expect(stored.targetNodeId).toBe(originalNodeId); }); + + it("provider envelope fields do not leak into API response", async () => { + vi.resetModules(); + + vi.doMock("@/lib/llm/provider", () => ({ + getProvider: () => ({ + generateReconstruction: vi.fn().mockResolvedValue({ + response: { + targetNodeId: "nk04xvk", + observations: ["obs"], + uncertainties: ["unc"], + assumptions: ["asm"], + relationships: [], + possibleFollowUpQuestions: ["fuq"], + }, + providerApiPath: "/api/chat", + providerExecution: { chatRequestAttempted: true, chatRequestSucceeded: true }, + }), + }), + })); + + const { POST } = await import("../app/api/focused-investigation/deconstruct/route.js"); + + const response = await POST( + new Request("http://localhost/api/focused-investigation/deconstruct", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + targetNodeId: "nk04xvk", + targetLabel: "label", + targetDescription: "desc", + centralStatement: "central", + question: "q?", + answer: "a.", + }), + }), + ); + + expect(response.status).toBe(200); + const json = await response.json(); + + // Semantic fields present + expect(json.success).toBe(true); + expect(json.targetNodeId).toBe("nk04xvk"); + expect(json.observations).toEqual(["obs"]); + expect(json.possibleFollowUpQuestions).toEqual(["fuq"]); + + // Provider envelope fields must NOT appear in the response + expect(json.providerApiPath).toBeUndefined(); + expect(json.providerExecution).toBeUndefined(); + }); });