import { describe, expect, it } from "vitest"; import { assessChildUnknownQuality, applyValidatedProposal, MAX_DECOMPOSITION_DEPTH, } from "@/lib/graph/apply-proposal.js"; import { makeGraph, makeNode } from "@/lib/graph/schema.js"; function makeParentGraph({ centralStatement, parentLabel, parentDescription, observations = [], }) { const parent = makeNode({ id: "n-parent", label: parentLabel, description: parentDescription, kind: "unknown", status: "unknown", confidence: "medium", }); return { parent, graph: makeGraph({ centralStatement, nodes: [parent, ...observations], edges: [], activeUnknownNodeId: parent.id, resolvedNodeIds: [], currentSummary: "Decomposition quality graph", }), }; } describe("assessChildUnknownQuality", () => { it("rejects 'Timing or measurement basis' as compound", () => { const { parent, graph } = makeParentGraph({ centralStatement: "Revenue increased by 18%, but cash in the bank fell over the same period.", parentLabel: "Explanation for why revenue increased by 18%, but cash in the bank fell over the same period", parentDescription: "Need to understand what change or event could explain why these observations differ, because that is needed to investigate their relationship.", }); const child = makeNode({ id: "n-child", label: "Timing or measurement basis", description: "Need evidence about whether a timing or measurement-basis difference could explain the observations, because that would change how they should be interpreted.", kind: "unknown", status: "unknown", confidence: "medium", parentId: parent.id, }); const result = assessChildUnknownQuality({ parentNode: parent, childNode: child, siblingNodes: [child], graph, }); expect(result.valid).toBe(false); expect(result.compoundSignals).toContain("timing_or_measurement_basis"); expect(result.reasons).toContain("compound_child"); }); it("accepts a child with one directly answerable uncertainty", () => { const { parent, graph } = makeParentGraph({ centralStatement: "Traffic increased, but sales stayed flat.", parentLabel: "What explains why more website traffic did not produce more sales?", parentDescription: "Need an explanation because the observations moved differently.", }); const child = makeNode({ id: "n-child", label: "Different measurement basis between the two observations", description: "Need evidence about whether the two observations use different measurement bases, because that could help explain the difference.", kind: "unknown", status: "unknown", confidence: "medium", parentId: parent.id, }); const result = assessChildUnknownQuality({ parentNode: parent, childNode: child, siblingNodes: [child], graph, }); expect(result.valid).toBe(true); expect(result.atomic).toBe(true); expect(result.directlyAnswerable).toBe(true); expect(result.narrowerThanParent).toBe(true); }); it("rejects sibling duplicates", () => { const { parent, graph } = makeParentGraph({ centralStatement: "Production increased, but defects also increased.", parentLabel: "What explains why output and defects both increased?", parentDescription: "Need an explanation because both observations increased.", }); const childA = makeNode({ id: "n-child-a", label: "Different timing between the two observations", description: "Need evidence about whether the two observations reflect different timing, because that could help explain the difference.", kind: "unknown", status: "unknown", confidence: "medium", parentId: parent.id, }); const childB = makeNode({ id: "n-child-b", label: "Different timing between the two observations", description: "Need evidence about whether the two observations reflect different timing, because that could help explain the difference.", kind: "unknown", status: "unknown", confidence: "medium", parentId: parent.id, }); const result = assessChildUnknownQuality({ parentNode: parent, childNode: childA, siblingNodes: [childA, childB], graph, }); expect(result.valid).toBe(false); expect(result.duplicateSiblingIds).toContain("n-child-b"); }); it("rejects parent paraphrases", () => { const { parent, graph } = makeParentGraph({ centralStatement: "Customer satisfaction scores increased, but complaints also increased.", parentLabel: "What explains why satisfaction and complaints both increased?", parentDescription: "Need a broad explanation because the observations moved differently.", }); const child = makeNode({ id: "n-child", label: "What explains why satisfaction and complaints both increased?", description: "Need a broad explanation because the observations moved differently.", kind: "unknown", status: "unknown", confidence: "medium", parentId: parent.id, }); const result = assessChildUnknownQuality({ parentNode: parent, childNode: child, siblingNodes: [child], graph, }); expect(result.valid).toBe(false); expect(result.reasons).toContain("not_narrower_than_parent"); }); }); describe("decomposition stopping conditions", () => { function makeMeaningfulNoOpProposal() { return { addedNodes: [ makeNode({ id: "n-anchor", label: "Update anchor", description: "Anchor state introduced by the answer because the update must contain a meaningful change.", kind: "state", status: "known", confidence: "low", }), ], updatedNodes: [], addedEdges: [], removedEdgeIds: [], resolvedUnknownNodeIds: [], affectedNodeIds: [], selectedQuestion: null, }; } it("does not decompose an atomic selected unknown", () => { const atomic = makeNode({ id: "n-atomic", label: "Were both figures measured over the same accounting period?", description: "Need to know whether both figures cover the same accounting period because that determines whether they are directly comparable.", kind: "unknown", status: "unknown", confidence: "high", }); const graph = makeGraph({ centralStatement: "Revenue increased by 18%, but cash in the bank fell over the same period.", nodes: [atomic], edges: [], activeUnknownNodeId: atomic.id, resolvedNodeIds: [], currentSummary: "Atomic selected node graph", }); const result = applyValidatedProposal({ situationGraph: graph, proposal: makeMeaningfulNoOpProposal(), }); expect(result.success).toBe(true); expect(result.decompositionAttempted).toBe(false); expect(result.decompositionStoppedReason).toBe( "Selected unknown is already atomic.", ); }); it("stops once a directly answerable child is selected", () => { const { parent, graph } = makeParentGraph({ centralStatement: "Traffic increased, but sales stayed flat.", parentLabel: "What explains why more website traffic did not produce more sales?", parentDescription: "Need an explanation because the observations moved differently.", observations: [ makeNode({ id: "n-traffic", label: "Website traffic increased.", description: "Website traffic increased.", kind: "observation", status: "supported", confidence: "high", }), makeNode({ id: "n-sales", label: "Sales stayed flat.", description: "Sales stayed flat.", kind: "observation", status: "supported", confidence: "high", }), ], }); const result = applyValidatedProposal({ situationGraph: graph, proposal: makeMeaningfulNoOpProposal(), }); expect(result.success).toBe(true); expect(result.decompositionAttempted).toBe(true); expect(result.decompositionAccepted).toBe(true); expect(result.selectedQuestion).toMatchObject({ nodeId: expect.any(String), question: "What evidence would clarify how the two observations were measured?", }); expect(result.selectedChildNodeId).toBe(result.selectedQuestion?.nodeId); expect(result.decompositionStoppedReason).toBe( "Selected child is atomic and directly answerable.", ); }); it("exposes the configured maximum decomposition depth", () => { expect(MAX_DECOMPOSITION_DEPTH).toBeGreaterThanOrEqual(2); expect(MAX_DECOMPOSITION_DEPTH).toBeLessThanOrEqual(3); }); });