fix(confidence-engine): unwrap focused deconstruction response

This commit is contained in:
2026-09-06 14:47:23 +01:00
parent a0f90e8885
commit 188dd04ab9
3 changed files with 116 additions and 36 deletions
@@ -52,11 +52,14 @@ export async function POST(request) {
const provider = getProvider(); const provider = getProvider();
const startedAt = Date.now(); 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; 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) // Validate schema (required fields present, no graph-mutation fields)
const validationErrors = validateFocusedDeconstructSchema(raw); const validationErrors = validateFocusedDeconstructSchema(deconstruction);
if (validationErrors.length > 0) { if (validationErrors.length > 0) {
return Response.json( return Response.json(
{ {
@@ -73,11 +76,11 @@ export async function POST(request) {
return Response.json({ return Response.json({
success: true, success: true,
targetNodeId: body.targetNodeId, targetNodeId: body.targetNodeId,
observations: raw.observations, observations: deconstruction.observations,
uncertainties: raw.uncertainties, uncertainties: deconstruction.uncertainties,
assumptions: raw.assumptions, assumptions: deconstruction.assumptions,
relationships: raw.relationships, relationships: deconstruction.relationships,
possibleFollowUpQuestions: raw.possibleFollowUpQuestions, possibleFollowUpQuestions: deconstruction.possibleFollowUpQuestions,
elapsedMs, elapsedMs,
}); });
} catch (e) { } catch (e) {
+14
View File
@@ -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` | | Task routing by work type | `docs/task-context-packs.md` |
| Broader architectural intent | `docs/architectural-principles.md` | | Broader architectural intent | `docs/architectural-principles.md` |
| Experiment history (specific) | `docs/design-evolution/README.md` → relevant chapter | | 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.**
+92 -29
View File
@@ -13,18 +13,22 @@ import { describe, it, expect, vi } from "vitest";
function makeMockProvider(inventedTargetNodeId) { function makeMockProvider(inventedTargetNodeId) {
return { return {
generateReconstruction: vi.fn().mockResolvedValue({ generateReconstruction: vi.fn().mockResolvedValue({
targetNodeId: inventedTargetNodeId, response: {
observations: ["doc is minimal", "processes in founder's head"], targetNodeId: inventedTargetNodeId,
uncertainties: ["whether formal docs can capture tacit knowledge"], observations: ["doc is minimal", "processes in founder's head"],
assumptions: ["documentation is primary mechanism for knowledge transfer"], uncertainties: ["whether formal docs can capture tacit knowledge"],
relationships: [ assumptions: ["documentation is primary mechanism for knowledge transfer"],
{ from: "founder", to: "processes", type: "holds", rationale: "tacit" }, relationships: [
{ from: "ops-context", to: "docs-infra", type: "depends_on", rationale: "formal docs required" }, { 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?", possibleFollowUpQuestions: [
"How is knowledge transferred when founder is unavailable?", "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", () => ({ vi.doMock("@/lib/llm/provider", () => ({
getProvider: () => ({ getProvider: () => ({
generateReconstruction: vi.fn().mockResolvedValue({ generateReconstruction: vi.fn().mockResolvedValue({
targetNodeId: "some-invented-id", response: {
observations: mockObs, targetNodeId: "some-invented-id",
uncertainties: mockUnc, observations: mockObs,
assumptions: mockAssm, uncertainties: mockUnc,
relationships: mockRel, assumptions: mockAssm,
possibleFollowUpQuestions: mockFuq, 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", () => ({ vi.doMock("@/lib/llm/provider", () => ({
getProvider: () => ({ getProvider: () => ({
generateReconstruction: vi.fn().mockResolvedValue({ generateReconstruction: vi.fn().mockResolvedValue({
targetNodeId: modelInventedId, response: {
observations: ["Documentation is minimal."], targetNodeId: modelInventedId,
uncertainties: [], observations: ["Documentation is minimal."],
assumptions: [ uncertainties: [],
"That formal documentation is the primary mechanism for capturing or transferring the founder's tacit knowledge of processes.", 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" }, relationships: [
{ from: "Operational Context", to: "Documentation Infrastructure", type: "affects" }, { from: "Founder", to: "Processes", type: "holds" },
], { from: "Operational Context", to: "Documentation Infrastructure", type: "affects" },
possibleFollowUpQuestions: [], ],
possibleFollowUpQuestions: [],
},
providerApiPath: "/api/chat",
providerExecution: { chatRequestAttempted: true },
}), }),
}), }),
})); }));
@@ -217,4 +229,55 @@ describe("focused-deconstruct targetNodeId identity boundary", () => {
const stored = { ...json }; const stored = { ...json };
expect(stored.targetNodeId).toBe(originalNodeId); 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();
});
}); });