fix(confidence-engine): unwrap focused deconstruction response
This commit is contained in:
@@ -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) {
|
||||||
|
|||||||
@@ -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.**
|
||||||
|
|||||||
@@ -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();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user