import { describe, expect, it } from "vitest"; import { makeNode, makeGraph } from "@/lib/graph/schema.js"; import { formulateQuestionForTarget, buildFocusedDeconstructPrompt, validateFocusedDeconstructSchema } from "@/lib/graph/focused-investigation.js"; import { FOCUSED_DECONSTRUCT_REGRESSION_CASES, validateRegressionFixtureIntegrity } from "./focused-deconstruct-regression-cases.mjs"; // ── helpers ────────────────────────────────────────────────────────────── function makeTestGraph() { return makeGraph({ centralStatement: "Should we launch the product now?", currentSummary: "initial summary", nodes: [ makeNode({ id: "a", label: "Market demand signal", description: "Evidence that customers want this.", kind: "unknown", status: "unknown" }), makeNode({ id: "b", label: "Competitor activity", description: "What others are doing.", kind: "observation", status: "supported" }), ], edges: [], resolvedNodeIds: ["b"], }); } // ── Boundary 1: explicit formulation ───────────────────────────────────── describe("formulateQuestionForTarget — explicit node selection", () => { it("validates that the target node exists in the graph", () => { const graph = makeTestGraph(); const result = formulateQuestionForTarget({ situationGraph: graph, targetNodeId: "nonexistent" }); expect(result.success).toBe(false); expect(result.error).toContain("not found"); }); it("validates that the target node is an unknown", () => { const graph = makeTestGraph(); const result = formulateQuestionForTarget({ situationGraph: graph, targetNodeId: "b" }); expect(result.success).toBe(false); expect(result.error).toContain("not an unknown"); }); it("validates that the target node is not resolved", () => { const graph = makeTestGraph(); // Create a resolved unknown node graph.nodes.push(makeNode({ id: "c", label: "Done", description: "Completed item", kind: "unknown", status: "resolved" })); const result = formulateQuestionForTarget({ situationGraph: graph, targetNodeId: "c" }); expect(result.success).toBe(false); expect(result.error).toContain("resolved"); }); it("uses the supplied targetNodeId in the formulation output", () => { const graph = makeTestGraph(); const result = formulateQuestionForTarget({ situationGraph: graph, targetNodeId: "a" }); expect(result.success).toBe(true); expect(result.targetNodeId).toBe("a"); }); it("does not mutate activeUnknownNodeId on the graph", () => { const graph = makeTestGraph(); const before = graph.activeUnknownNodeId; formulateQuestionForTarget({ situationGraph: graph, targetNodeId: "a" }); expect(graph.activeUnknownNodeId).toBe(before); }); it("does not mutate SituationGraph in any way", () => { const graph = makeTestGraph(); const snapshot = JSON.stringify(graph); formulateQuestionForTarget({ situationGraph: graph, targetNodeId: "a" }); expect(JSON.stringify(graph)).toBe(snapshot); }); it("does not invoke global selection (selectActiveUnknownCandidate / determineGraphBackedQuestion)", () => { const graph = makeTestGraph(); const result = formulateQuestionForTarget({ situationGraph: graph, targetNodeId: "a" }); expect(result.success).toBe(true); expect(result.targetNodeId).toBe("a"); expect(result.question).toBeDefined(); expect(typeof result.question).toBe("string"); expect(result.question.length).toBeGreaterThan(0); }); }); // ── Boundary 2: focused answer deconstruction ──────────────────────────── describe("buildFocusedDeconstructPrompt", () => { it("produces a prompt containing the target label", () => { const prompt = buildFocusedDeconstructPrompt({ targetLabel: "Market demand signal", targetDescription: "Evidence that customers want this.", centralStatement: "Should we launch?", question: "What evidence would clarify market demand?", answer: "Some evidence suggests demand.", }); expect(prompt).toContain("Market demand signal"); }); it("produces a prompt containing the answer text", () => { const prompt = buildFocusedDeconstructPrompt({ targetLabel: "X", targetDescription: "Y", centralStatement: "Z", question: "Q?", answer: "Some evidence suggests demand.", }); expect(prompt).toContain("Some evidence suggests demand."); }); it("does NOT output graph-mutation fields (only lists them as forbidden in the instructions)", () => { const prompt = buildFocusedDeconstructPrompt({ targetLabel: "X", targetDescription: "Y", centralStatement: "Z", question: "Q?", answer: "A.", }); // The prompt instructs the model NOT to output these fields. // It mentions them only as forbidden outputs, not as required outputs. expect(prompt).toContain("Do NOT include any of these fields"); expect(prompt).not.toContain("- addedNodes"); expect(prompt).not.toContain("- updatedNodes"); expect(prompt).not.toContain("- removedNodes"); expect(prompt).not.toContain("- resolvedNodeIds"); }); }); describe("buildFocusedDeconstructPrompt — relationship rule boundary lock", () => { it("relationship instruction explicitly prohibits observation restatement", () => { const prompt = buildFocusedDeconstructPrompt({ targetLabel: "X", targetDescription: "Y", centralStatement: "Z", question: "Q?", answer: "A.", }); expect(prompt).toContain("Do not create a relationship by merely restating"); }); it("relationship instruction explicitly prohibits promotion of tentative/conditional meaning", () => { const prompt = buildFocusedDeconstructPrompt({ targetLabel: "X", targetDescription: "Y", centralStatement: "Z", question: "Q?", answer: "A.", }); expect(prompt).toContain("Tentative, speculative, or conditional language must not be promoted into an established relationship"); }); it("relationship instruction requires direct link between two distinct propositions", () => { const prompt = buildFocusedDeconstructPrompt({ targetLabel: "X", targetDescription: "Y", centralStatement: "Z", question: "Q?", answer: "A.", }); expect(prompt).toContain("must connect two distinct propositions that the user's answer itself links"); }); it("relationship instruction explicitly permits relationships: [] when no direct link exists", () => { const prompt = buildFocusedDeconstructPrompt({ targetLabel: "X", targetDescription: "Y", centralStatement: "Z", question: "Q?", answer: "A.", }); expect(prompt).toContain("relationships: []"); }); }); describe("validateFocusedDeconstructSchema", () => { it("passes when all required fields are present", () => { const result = { targetNodeId: "a", observations: [], uncertainties: [], assumptions: [], relationships: [], possibleFollowUpQuestions: [] }; expect(validateFocusedDeconstructSchema(result)).toEqual([]); }); it("fails when a required field is missing", () => { const result = { targetNodeId: "a", observations: [], uncertainties: [], assumptions: [] }; const errors = validateFocusedDeconstructSchema(result); expect(errors.length).toBeGreaterThan(0); expect(errors.some((e) => e.includes("relationships") || e.includes("possibleFollowUpQuestions"))).toBeTruthy(); }); it("rejects results that contain graph-mutation fields", () => { const result = { targetNodeId: "a", observations: [], uncertainties: [], assumptions: [], relationships: [], possibleFollowUpQuestions: [], addedNodes: [{ id: "x" }], }; const errors = validateFocusedDeconstructSchema(result); expect(errors.some((e) => e.includes("addedNodes"))).toBeTruthy(); }); it("rejects results that contain resolvedNodeIds", () => { const result = { targetNodeId: "a", observations: [], uncertainties: [], assumptions: [], relationships: [], possibleFollowUpQuestions: [], resolvedNodeIds: ["x"], }; const errors = validateFocusedDeconstructSchema(result); expect(errors.some((e) => e.includes("resolvedNodeIds"))).toBeTruthy(); }); it("rejects results that contain activeUnknownNodeId", () => { const result = { targetNodeId: "a", observations: [], uncertainties: [], assumptions: [], relationships: [], possibleFollowUpQuestions: [], activeUnknownNodeId: "x", }; const errors = validateFocusedDeconstructSchema(result); expect(errors.some((e) => e.includes("activeUnknownNodeId"))).toBeTruthy(); }); it("rejects results that contain selectedQuestion", () => { const result = { targetNodeId: "a", observations: [], uncertainties: [], assumptions: [], relationships: [], possibleFollowUpQuestions: [], selectedQuestion: "some question", }; const errors = validateFocusedDeconstructSchema(result); expect(errors.some((e) => e.includes("selectedQuestion"))).toBeTruthy(); }); it("all required fields are observations, uncertainties, assumptions, relationships, possibleFollowUpQuestions", () => { const fullResult = { targetNodeId: "a", observations: [], uncertainties: [], assumptions: [], relationships: [], possibleFollowUpQuestions: [] }; const errors = validateFocusedDeconstructSchema(fullResult); expect(errors.length).toBe(0); }); it("focused result contains no graph-mutation fields when valid", () => { const fullResult = { targetNodeId: "a", observations: [], uncertainties: [], assumptions: [], relationships: [], possibleFollowUpQuestions: [] }; const errors = validateFocusedDeconstructSchema(fullResult); expect(errors.length).toBe(0); // Confirm no forbidden keys are in the result const forbidden = ["addedNodes", "updatedNodes", "removedNodes", "addedEdges", "removedEdges", "resolvedNodeIds", "activeUnknownNodeId", "selectedQuestion"]; for (const key of forbidden) { expect(key in fullResult).toBe(false); } }); }); // ── Regression fixture integrity (zero live calls) ────────────────────────── describe("FOCUSED_DECONSTRUCT_REGRESSION_CASES fixture integrity", () => { it("has exactly 7 cases", () => { expect(FOCUSED_DECONSTRUCT_REGRESSION_CASES.length).toBe(7); }); it("all IDs are unique", () => { const ids = FOCUSED_DECONSTRUCT_REGRESSION_CASES.map((c) => c.id); expect(new Set(ids).size).toBe(ids.length); }); it("contains the new structural-frontier case ID", () => { const ids = FOCUSED_DECONSTRUCT_REGRESSION_CASES.map((c) => c.id); expect(ids).toContain("structural-frontier-over-working-example-detail"); }); it("preserves all six original case IDs", () => { const ids = FOCUSED_DECONSTRUCT_REGRESSION_CASES.map((c) => c.id); for (const origId of [ "nearest-frontier-wins", "no-genuine-assumption", "genuine-evidence-sufficiency-assumption", "juxtaposition-must-not-create-link", "tentative-observation-fidelity", "genuine-dependency-frontier", ]) { expect(ids).toContain(origId); } }); it("validateRegressionFixtureIntegrity reports zero errors", () => { const errors = validateRegressionFixtureIntegrity(); expect(errors).toEqual([]); }); });