import { describe, expect, it } from "vitest"; import { applyValidatedProposal } from "@/lib/graph/apply-proposal.js"; import { makeEdge, makeGraph, makeNode } from "@/lib/graph/schema.js"; import { validateGraphReferences } from "@/lib/graph/utils.js"; function makeComparabilityUpdateFixture() { const comparabilityUnknown = makeNode({ id: "n-comparability-unknown", label: "Whether the figures are comparable", description: "Need to know whether the figures use the same period, basis, and scale before comparing them.", kind: "unknown", status: "unknown", confidence: "high", }); const revenueObservation = makeNode({ id: "n-revenue-observation", label: "Revenue increased by 18%.", description: "Revenue increased by 18%.", kind: "observation", status: "supported", confidence: "high", }); const cashObservation = makeNode({ id: "n-cash-observation", label: "Cash in the bank decreased over the same period.", description: "Cash in the bank decreased over the same period.", kind: "observation", status: "supported", confidence: "high", }); const unrelatedNode = makeNode({ id: "n-unrelated", label: "Board update", description: "A separate unchanged note.", kind: "state", status: "known", confidence: "low", }); const graph = makeGraph({ centralStatement: "Revenue increased by 18%, but cash in the bank fell over the same period.", nodes: [ comparabilityUnknown, revenueObservation, cashObservation, unrelatedNode, ], edges: [ makeEdge({ id: "e-revenue-comparability", fromNodeId: revenueObservation.id, toNodeId: comparabilityUnknown.id, relationship: "supports", confidence: "medium", description: "Revenue observation requires comparability confirmation.", }), makeEdge({ id: "e-cash-comparability", fromNodeId: cashObservation.id, toNodeId: comparabilityUnknown.id, relationship: "supports", confidence: "medium", description: "Cash observation requires comparability confirmation.", }), ], activeUnknownNodeId: comparabilityUnknown.id, resolvedNodeIds: [], currentSummary: "Initial comparability fixture", reasoningState: { comparabilityStatus: "uncertain", comparabilityReason: "Comparability between the observations is not yet established across period, scale, or measurement basis.", comparabilityEvidence: [], relationshipStatus: "insufficient_information", relationshipReason: "Relationship classification is deferred until comparability is established.", relationshipAssessed: false, contradictionReasoningAllowed: false, reasoningStages: [ { stage: "comparability", status: "uncertain", outcome: "Comparability between the observations is not yet established across period, scale, or measurement basis.", }, { stage: "relationship", status: "insufficient_information", outcome: "not assessed until comparability is established", }, ], }, }); const proposal = { addedNodes: [], updatedNodes: [ { nodeId: comparabilityUnknown.id, previousStatus: "unknown", newStatus: "resolved", previousValue: null, newValue: "Both figures cover the same accounting period and are taken from the same management accounts.", reason: "The answer confirms the figures are comparable.", }, ], addedEdges: [], removedEdgeIds: [], resolvedUnknownNodeIds: [comparabilityUnknown.id], affectedNodeIds: [], selectedQuestion: null, }; return { graph, proposal, comparabilityUnknownId: comparabilityUnknown.id }; } function makeApplicationFixture() { const complaintRateUnknown = makeNode({ id: "n-complaint-rate-unknown", label: "Complaint rate", description: "Need the complaint rate per 100 units", kind: "unknown", status: "unknown", confidence: "high", affects: ["n-quality-deterioration"], }); const staffingUnknown = makeNode({ id: "n-staffing-unknown", label: "Staffing change", description: "Need to know if staffing changed", kind: "unknown", status: "unknown", confidence: "medium", }); const qualityDeterioration = makeNode({ id: "n-quality-deterioration", label: "Quality deterioration conclusion", description: "Conclusion that quality deteriorated", kind: "conclusion", status: "supported", confidence: "medium", dependsOn: ["n-complaint-rate-unknown"], }); const complaintCount = makeNode({ id: "n-complaint-count", label: "Complaint count observation", description: "Complaint count increased", kind: "observation", status: "supported", confidence: "high", value: 135, unit: "count", }); const productionCount = makeNode({ id: "n-production-count", label: "Production count observation", description: "Production increased", kind: "observation", status: "supported", confidence: "high", value: 7100, unit: "units", }); const graph = makeGraph({ centralStatement: "Complaints rose while production also rose.", nodes: [ complaintRateUnknown, staffingUnknown, qualityDeterioration, complaintCount, productionCount, ], edges: [ makeEdge({ id: "e-quality-depends-rate", fromNodeId: complaintRateUnknown.id, toNodeId: qualityDeterioration.id, relationship: "supports", confidence: "medium", description: "The rate informs the quality conclusion", }), ], activeUnknownNodeId: complaintRateUnknown.id, resolvedNodeIds: [], currentSummary: "Initial summary", }); const proposal = { addedNodes: [], updatedNodes: [ { nodeId: complaintRateUnknown.id, previousStatus: "unknown", newStatus: "resolved", previousValue: "2.0 complaints per 100 units", newValue: "1.9 complaints per 100 units", reason: "The answer provides the updated normalized complaint rate.", }, { nodeId: qualityDeterioration.id, previousStatus: "supported", newStatus: "weakened", previousValue: null, newValue: null, reason: "The improved rate weakens the deterioration conclusion.", }, ], addedEdges: [], removedEdgeIds: [], resolvedUnknownNodeIds: [complaintRateUnknown.id], affectedNodeIds: [qualityDeterioration.id], selectedQuestion: null, }; return { graph, proposal, ids: { complaintRateUnknown: complaintRateUnknown.id, staffingUnknown: staffingUnknown.id, qualityDeterioration: qualityDeterioration.id, complaintCount: complaintCount.id, productionCount: productionCount.id, }, }; } const COMMERCIAL_SCENARIO = "I have developed a new reasoning method that aims to help people determine whether they have enough justified confidence to make a decision. I believe it could become a commercial product, but I do not yet know whether it solves a genuine problem, whether people would value it enough to pay for it, or whether it is fundamentally different from existing AI tools. Before investing significant time and money into building it further, I want to determine whether continuing development is commercially justified."; function makeCommercialUpdateFixture() { const parent = makeNode({ id: "n-commercial-parent", label: "Commercial justification for whether continuing development is commercially justified", description: "Need to know whether this solves a genuine problem, whether people would value it enough to pay for it, and whether it is commercially justified before continuing development.", kind: "unknown", status: "unknown", confidence: "medium", }); return makeGraph({ centralStatement: COMMERCIAL_SCENARIO, nodes: [parent], edges: [], activeUnknownNodeId: parent.id, resolvedNodeIds: [], currentSummary: "Commercial update fixture", }); } 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, }; } describe("applyValidatedProposal", () => { it("applies a valid proposal successfully", () => { const { graph, proposal, ids } = makeApplicationFixture(); const result = applyValidatedProposal({ situationGraph: graph, proposal, }); expect(result).toMatchObject({ success: true, graphUpdate: proposal, resolvedUnknownNodeIds: [ids.complaintRateUnknown], previousActiveUnknownNodeId: ids.complaintRateUnknown, newActiveUnknownNodeId: ids.staffingUnknown, }); expect( result.updatedSituationGraph.nodes.find( (node) => node.id === ids.complaintRateUnknown, )?.status, ).toBe("resolved"); expect( result.updatedSituationGraph.nodes.find( (node) => node.id === ids.qualityDeterioration, )?.status, ).toBe("weakened"); }); it("rejects an invalid graph before application", () => { const { graph, proposal } = makeApplicationFixture(); graph.nodes[0].dependsOn.push("missing-node"); const original = JSON.parse(JSON.stringify(graph)); const result = applyValidatedProposal({ situationGraph: graph, proposal, }); expect(result.success).toBe(false); expect(result.stage).toBe("graph_validation"); expect(graph).toEqual(original); }); it("rejects updates referencing nonexistent nodes", () => { const { graph, proposal } = makeApplicationFixture(); const result = applyValidatedProposal({ situationGraph: graph, proposal: { ...proposal, updatedNodes: [ ...proposal.updatedNodes, { nodeId: "ghost-node", previousStatus: "unknown", newStatus: "resolved", previousValue: null, newValue: null, reason: "Invalid reference", }, ], }, }); expect(result).toMatchObject({ success: false, stage: "proposal_compatibility", }); expect(result.errors).toEqual( expect.arrayContaining([ expect.stringContaining( 'Cannot update non-existent node: "ghost-node"', ), ]), ); }); it("rejects added edges with invalid references", () => { const { graph, proposal } = makeApplicationFixture(); const result = applyValidatedProposal({ situationGraph: graph, proposal: { ...proposal, addedEdges: [ makeEdge({ id: "e-invalid", fromNodeId: "missing-node", toNodeId: "n-quality-deterioration", relationship: "supports", confidence: "medium", description: "Invalid edge", }), ], }, }); expect(result.success).toBe(false); expect(result.stage).toBe("proposal_compatibility"); }); it("rejects duplicate IDs", () => { const { graph, proposal, ids } = makeApplicationFixture(); const result = applyValidatedProposal({ situationGraph: graph, proposal: { ...proposal, addedNodes: [ makeNode({ id: ids.qualityDeterioration, label: "Duplicate", description: "Duplicate node id", }), ], }, }); expect(result.success).toBe(false); expect(result.stage).toBe("proposal_compatibility"); expect(result.errors.join(" ")).toContain("duplicate node ID"); }); it("preserves unrelated nodes byte-for-byte", () => { const { graph, proposal, ids } = makeApplicationFixture(); const originalComplaintCount = JSON.stringify( graph.nodes.find((node) => node.id === ids.complaintCount), ); const originalProductionCount = JSON.stringify( graph.nodes.find((node) => node.id === ids.productionCount), ); const result = applyValidatedProposal({ situationGraph: graph, proposal, }); expect(result.success).toBe(true); expect( JSON.stringify( result.updatedSituationGraph.nodes.find( (node) => node.id === ids.complaintCount, ), ), ).toBe(originalComplaintCount); expect( JSON.stringify( result.updatedSituationGraph.nodes.find( (node) => node.id === ids.productionCount, ), ), ).toBe(originalProductionCount); }); it("adds resolved unknowns to resolvedNodeIds", () => { const { graph, proposal, ids } = makeApplicationFixture(); const result = applyValidatedProposal({ situationGraph: graph, proposal, }); expect(result.success).toBe(true); expect(result.updatedSituationGraph.resolvedNodeIds).toContain( ids.complaintRateUnknown, ); expect( result.updatedSituationGraph.nodes.find( (node) => node.id === ids.complaintRateUnknown, )?.status, ).toBe("resolved"); }); it("rejects resolvedUnknownNodeIds that do not reference actual unknown nodes", () => { const { graph, proposal, ids } = makeApplicationFixture(); const result = applyValidatedProposal({ situationGraph: graph, proposal: { ...proposal, resolvedUnknownNodeIds: [ids.qualityDeterioration], }, }); expect(result.success).toBe(false); expect(result.stage).toBe("proposal_compatibility"); expect(result.errors.join(" ")).toContain( "Resolved unknown must reference an existing unknown node", ); }); it("rejects a duplicate semantic node without resolution", () => { const { graph, proposal } = makeApplicationFixture(); const result = applyValidatedProposal({ situationGraph: graph, proposal: { ...proposal, resolvedUnknownNodeIds: [], updatedNodes: proposal.updatedNodes.filter( (update) => update.nodeId !== "n-complaint-rate-unknown", ), addedNodes: [ makeNode({ id: "n-parallel-rate", label: "Complaint rate", description: "Need the complaint rate per 100 units", kind: "observation", status: "supported", confidence: "medium", }), ], }, }); expect(result.success).toBe(false); expect(result.stage).toBe("proposal_compatibility"); expect(result.errors.join(" ")).toContain( "duplicating unresolved unknown meaning", ); }); it("keeps the active unknown when it remains unresolved", () => { const { graph, ids } = makeApplicationFixture(); const proposal = { addedNodes: [], updatedNodes: [ { nodeId: ids.qualityDeterioration, previousStatus: "supported", newStatus: "weakened", previousValue: null, newValue: null, reason: "Only the conclusion changes", }, ], addedEdges: [], removedEdgeIds: [], resolvedUnknownNodeIds: [], affectedNodeIds: [ids.qualityDeterioration], }; const result = applyValidatedProposal({ situationGraph: graph, proposal, }); expect(result.success).toBe(true); expect(result.previousActiveUnknownNodeId).toBe(ids.complaintRateUnknown); expect(result.newActiveUnknownNodeId).toBe(ids.complaintRateUnknown); }); it("reports affected node ids", () => { const { graph, proposal, ids } = makeApplicationFixture(); const result = applyValidatedProposal({ situationGraph: graph, proposal, }); expect(result.success).toBe(true); expect(result.affectedNodeIds).toEqual( expect.arrayContaining([ ids.complaintRateUnknown, ids.qualityDeterioration, ]), ); }); it("revalidates the completed graph references", () => { const { graph, proposal } = makeApplicationFixture(); const result = applyValidatedProposal({ situationGraph: graph, proposal, }); expect(result.success).toBe(true); expect(validateGraphReferences(result.updatedSituationGraph)).toEqual({ valid: true, errors: [], }); }); it("is atomic on failure", () => { const { graph, proposal } = makeApplicationFixture(); const originalGraph = JSON.parse(JSON.stringify(graph)); const result = applyValidatedProposal({ situationGraph: graph, proposal: { ...proposal, addedEdges: [ makeEdge({ id: "e-bad", fromNodeId: "missing-node", toNodeId: "n-quality-deterioration", relationship: "supports", confidence: "medium", description: "Invalid edge", }), ], }, }); expect(result.success).toBe(false); expect(graph).toEqual(originalGraph); }); it("rejects a proposal with no meaningful change", () => { const { graph } = makeApplicationFixture(); const result = applyValidatedProposal({ situationGraph: graph, proposal: { addedNodes: [], updatedNodes: [ { nodeId: "n-quality-deterioration", previousStatus: null, newStatus: null, previousValue: null, newValue: null, reason: "No change", }, ], addedEdges: [], removedEdgeIds: [], resolvedUnknownNodeIds: [], affectedNodeIds: [], selectedQuestion: null, }, }); expect(result).toMatchObject({ success: false, stage: "proposal_compatibility", }); expect(result.errors).toEqual( expect.arrayContaining([expect.stringContaining("no meaningful change")]), ); }); it("resolves one unknown and adds consequential unknowns with one selected question", () => { const { graph, ids } = makeApplicationFixture(); const proposal = { addedNodes: [ makeNode({ id: "n-commercial-value", label: "Commercial value definition", description: "Need a concrete definition of commercial value because the decision depends on it.", kind: "unknown", status: "unknown", confidence: "high", }), makeNode({ id: "n-demand-evidence", label: "Evidence of demand", description: "Need evidence of demand because it matters to the build decision.", kind: "unknown", status: "unknown", confidence: "medium", }), makeNode({ id: "n-build-decision", label: "Build Confidence Engine decision", description: "Decision situation introduced by the answer.", kind: "state", status: "supported", confidence: "medium", }), ], updatedNodes: [ { nodeId: ids.complaintRateUnknown, previousStatus: "unknown", newStatus: "resolved", previousValue: null, newValue: "Decision whether to build Confidence Engine", reason: "The answer resolves the original context unknown.", }, ], addedEdges: [ makeEdge({ id: "e-build-commercial-value", fromNodeId: "n-build-decision", toNodeId: "n-commercial-value", relationship: "depends_on", confidence: "medium", description: "The decision depends on defining commercial value.", }), makeEdge({ id: "e-build-demand-evidence", fromNodeId: "n-build-decision", toNodeId: "n-demand-evidence", relationship: "depends_on", confidence: "medium", description: "The decision depends on evidence of demand.", }), ], removedEdgeIds: [], resolvedUnknownNodeIds: [ids.complaintRateUnknown], affectedNodeIds: [], selectedQuestion: { nodeId: "n-commercial-value", question: "How should commercial value be defined for this decision?", reason: "This is the most consequential unresolved unknown introduced by the answer.", }, }; const result = applyValidatedProposal({ situationGraph: graph, proposal }); expect(result.success).toBe(true); expect(result.updatedSituationGraph.resolvedNodeIds).toContain( ids.complaintRateUnknown, ); expect( result.updatedSituationGraph.nodes.some( (node) => node.id === "n-commercial-value", ), ).toBe(true); expect( result.updatedSituationGraph.nodes.some( (node) => node.id === "n-demand-evidence", ), ).toBe(true); expect(result.newActiveUnknownNodeId).toBe("n-commercial-value"); expect(result.selectedQuestion?.nodeId).toBe("n-commercial-value"); expect(result.selectedQuestion?.question).toMatch(/\?$/); expect(result.selectedQuestion?.question.length).toBeGreaterThan(20); expect(result.selectedQuestion?.question.toLowerCase()).not.toContain( "price", ); expect(result.selectedQuestion?.question.toLowerCase()).not.toContain( "how should uncertainty regarding", ); }); it("rejects more than 3 added unknowns", () => { const { graph, proposal, ids } = makeApplicationFixture(); const result = applyValidatedProposal({ situationGraph: graph, proposal: { ...proposal, addedNodes: [1, 2, 3, 4].map((index) => makeNode({ id: `n-unknown-${index}`, label: `Unknown ${index}`, description: `Need unknown ${index} because it matters to the decision.`, kind: "unknown", status: "unknown", confidence: "medium", }), ), addedEdges: [1, 2, 3, 4].map((index) => makeEdge({ id: `e-unknown-${index}`, fromNodeId: ids.complaintRateUnknown, toNodeId: `n-unknown-${index}`, relationship: "depends_on", confidence: "medium", description: `Links unknown ${index}`, }), ), selectedQuestion: { nodeId: "n-unknown-1", question: "What is unknown 1?", reason: "Follow-up required.", }, }, }); expect(result.success).toBe(false); expect(result.errors.join(" ")).toContain("too many unknown nodes"); }); it("rejects unrelated added unknowns", () => { const { graph, proposal } = makeApplicationFixture(); const result = applyValidatedProposal({ situationGraph: graph, proposal: { ...proposal, addedNodes: [ makeNode({ id: "n-unrelated", label: "Office rent", description: "Need office rent because it matters to a different branch.", kind: "unknown", status: "unknown", confidence: "low", }), ], selectedQuestion: { nodeId: "n-unrelated", question: "What is the office rent?", reason: "Unrelated test.", }, }, }); expect(result.success).toBe(false); expect(result.errors.join(" ")).toContain( "explicitly related to an answer-derived node", ); }); it("accepts a newly added unknown explicitly linked through answer-derived node fields", () => { const { graph, ids } = makeApplicationFixture(); const proposal = { addedNodes: [ makeNode({ id: "n-answer-context", label: "Build Confidence Engine decision", description: "Decision context introduced by the answer.", kind: "state", status: "supported", confidence: "medium", childIds: ["n-commercial-value"], }), makeNode({ id: "n-commercial-value", label: "Commercial value definition", description: "Need commercial value definition because the decision depends on it.", kind: "unknown", status: "unknown", confidence: "high", dependsOn: ["n-answer-context"], }), ], updatedNodes: [ { nodeId: ids.complaintRateUnknown, previousStatus: "unknown", newStatus: "resolved", previousValue: null, newValue: "Decision whether to build Confidence Engine", reason: "The answer resolves the original context unknown.", }, ], addedEdges: [], removedEdgeIds: [], resolvedUnknownNodeIds: [ids.complaintRateUnknown], affectedNodeIds: [], selectedQuestion: { nodeId: "n-commercial-value", question: "How should commercial value be defined for this decision?", reason: "A consequential unknown remains unresolved.", }, }; const result = applyValidatedProposal({ situationGraph: graph, proposal }); expect(result.success).toBe(true); expect(result.selectedQuestion?.nodeId).toBe("n-commercial-value"); }); it("rejects a newly added unknown linked only to the original unresolved node when that node is not answer-derived", () => { const { graph, ids } = makeApplicationFixture(); const result = applyValidatedProposal({ situationGraph: graph, proposal: { addedNodes: [ makeNode({ id: "n-commercial-value", label: "Commercial value definition", description: "Need commercial value definition because the decision depends on it.", kind: "unknown", status: "unknown", confidence: "high", dependsOn: [ids.complaintRateUnknown], }), ], updatedNodes: [], addedEdges: [ makeEdge({ id: "e-legacy-unknown-commercial-value", fromNodeId: ids.complaintRateUnknown, toNodeId: "n-commercial-value", relationship: "depends_on", confidence: "medium", description: "Links only to the original unresolved unknown.", }), ], removedEdgeIds: [], resolvedUnknownNodeIds: [], affectedNodeIds: [], selectedQuestion: { nodeId: "n-commercial-value", question: "How should commercial value be defined for this decision?", reason: "A consequential unknown remains unresolved.", }, }, }); expect(result.success).toBe(false); expect(result.errors.join(" ")).toContain( "explicitly related to an answer-derived node", ); }); it("rejects a floating emergent unknown with no explicit relationship", () => { const { graph } = makeApplicationFixture(); const result = applyValidatedProposal({ situationGraph: graph, proposal: { addedNodes: [ makeNode({ id: "n-floating", label: "Floating unknown", description: "Need this because it matters to the decision.", kind: "unknown", status: "unknown", confidence: "medium", }), ], updatedNodes: [], addedEdges: [], removedEdgeIds: [], resolvedUnknownNodeIds: [], affectedNodeIds: [], selectedQuestion: { nodeId: "n-floating", question: "What would resolve Floating unknown?", reason: "Test case for floating unknown rejection.", }, }, }); expect(result.success).toBe(false); expect(result.errors.join(" ")).toContain( "explicitly related to an answer-derived node", ); }); it("accepts the reported live-shaped commercial-value proposal when the linkage is explicit in node references", () => { const { graph, ids } = makeApplicationFixture(); const proposal = { addedNodes: [ makeNode({ id: "answer_context_build", label: "Build Confidence Engine decision context", description: "The answer introduces a concrete decision about whether to build Confidence Engine.", kind: "state", status: "known", confidence: "high", dependsOn: [ids.complaintRateUnknown, "nu_commercial_val"], childIds: ["nu_commercial_val"], affects: ["nu_commercial_val"], }), makeNode({ id: "nu_commercial_val", label: "Commercial viability assessment of Confidence Engine", description: "The commercial viability of Confidence Engine remains unknown because resolving it is needed to decide whether building it is justified.", kind: "unknown", status: "unknown", confidence: "medium", dependsOn: ["answer_context_build"], childIds: ["answer_context_build"], }), ], updatedNodes: [ { nodeId: ids.complaintRateUnknown, previousStatus: "unknown", newStatus: "resolved", previousValue: null, newValue: "Deciding whether to build the Confidence Engine due to uncertainty about its commercial value.", reason: "The answer resolves the original context unknown.", }, ], addedEdges: [], removedEdgeIds: [], resolvedUnknownNodeIds: [ids.complaintRateUnknown], affectedNodeIds: [ ids.complaintRateUnknown, "answer_context_build", "nu_commercial_val", ], selectedQuestion: { nodeId: "nu_commercial_val", question: "How should commercial viability be defined for this decision?", reason: "A foundational commercial-value unknown remains unresolved.", }, }; const result = applyValidatedProposal({ situationGraph: graph, proposal }); expect(result.success).toBe(true); expect(result.updatedSituationGraph.resolvedNodeIds).toContain( ids.complaintRateUnknown, ); expect( result.updatedSituationGraph.nodes.some( (node) => node.id === "nu_commercial_val", ), ).toBe(true); }); it("rejects selected question referencing resolved node", () => { const { graph, proposal, ids } = makeApplicationFixture(); const result = applyValidatedProposal({ situationGraph: graph, proposal: { ...proposal, selectedQuestion: { nodeId: ids.complaintRateUnknown, question: "What is the complaint rate?", reason: "Invalid reselection.", }, }, }); expect(result.success).toBe(false); expect(result.errors.join(" ")).toContain( "selectedQuestion must reference an unresolved node", ); }); it("active unknown matches selected question node", () => { const { graph, ids } = makeApplicationFixture(); const proposal = { addedNodes: [ makeNode({ id: "n-success-threshold", label: "Success threshold", description: "Need a success threshold because the decision depends on it.", kind: "unknown", status: "unknown", confidence: "high", }), makeNode({ id: "n-build-decision", label: "Build Confidence Engine decision", description: "Decision introduced by the answer.", kind: "state", status: "supported", confidence: "medium", }), ], updatedNodes: [ { nodeId: ids.complaintRateUnknown, previousStatus: "unknown", newStatus: "resolved", previousValue: null, newValue: "Decision whether to build Confidence Engine", reason: "The answer resolves the original unknown.", }, ], addedEdges: [ makeEdge({ id: "e-build-success-threshold", fromNodeId: "n-build-decision", toNodeId: "n-success-threshold", relationship: "depends_on", confidence: "medium", description: "The decision depends on a success threshold.", }), ], removedEdgeIds: [], resolvedUnknownNodeIds: [ids.complaintRateUnknown], affectedNodeIds: [], selectedQuestion: { nodeId: "n-success-threshold", question: "What success threshold would justify building it?", reason: "One consequential unknown remains.", }, }; const result = applyValidatedProposal({ situationGraph: graph, proposal }); expect(result.success).toBe(true); expect(result.newActiveUnknownNodeId).toBe(result.selectedQuestion?.nodeId); }); it("replaces downstream pricing question with higher-value commercial-value question", () => { const { graph, ids } = makeApplicationFixture(); const proposal = { addedNodes: [ makeNode({ id: "n-commercial-value", label: "Commercial value definition", description: "Need commercial value definition because the decision depends on it.", kind: "unknown", status: "unknown", confidence: "high", }), makeNode({ id: "n-pricing", label: "Target price point", description: "Need a price point because revenue assumptions depend on it.", kind: "unknown", status: "unknown", confidence: "medium", dependsOn: ["n-commercial-value"], }), makeNode({ id: "n-build-decision", label: "Build Confidence Engine decision", description: "Decision introduced by the answer.", kind: "state", status: "supported", confidence: "medium", }), ], updatedNodes: [ { nodeId: ids.complaintRateUnknown, previousStatus: "unknown", newStatus: "resolved", previousValue: null, newValue: "Decision whether to build Confidence Engine", reason: "The answer resolves the original context unknown.", }, ], addedEdges: [ makeEdge({ id: "e-build-commercial-value", fromNodeId: "n-build-decision", toNodeId: "n-commercial-value", relationship: "depends_on", confidence: "medium", description: "The decision depends on defining commercial value.", }), makeEdge({ id: "e-commercial-value-pricing", fromNodeId: "n-commercial-value", toNodeId: "n-pricing", relationship: "depends_on", confidence: "medium", description: "Pricing depends on commercial value definition.", }), makeEdge({ id: "e-build-pricing", fromNodeId: "n-build-decision", toNodeId: "n-pricing", relationship: "depends_on", confidence: "low", description: "The decision also references pricing assumptions.", }), ], removedEdgeIds: [], resolvedUnknownNodeIds: [ids.complaintRateUnknown], affectedNodeIds: [], selectedQuestion: { nodeId: "n-pricing", question: "What is the target price point?", reason: "Model chose a downstream leaf.", }, }; const result = applyValidatedProposal({ situationGraph: graph, proposal }); expect(result.success).toBe(true); expect(result.selectedQuestion?.nodeId).toBe("n-commercial-value"); expect(result.selectedQuestion?.question.toLowerCase()).not.toContain( "price", ); }); it("resolves the existing comparability unknown and advances reasoning after the answer", () => { const { graph, proposal, comparabilityUnknownId } = makeComparabilityUpdateFixture(); const originalUnrelatedNode = JSON.stringify( graph.nodes.find((node) => node.id === "n-unrelated"), ); const result = applyValidatedProposal({ situationGraph: graph, proposal, previousQuestion: "Were these figures measured on the same basis and at the same scale?", answer: "Yes. Both figures cover the same accounting period and are taken from the same management accounts.", }); expect(result.success).toBe(true); expect(result.resolvedUnknownNodeIds).toContain(comparabilityUnknownId); expect(result.resolvedReasoningNodeIds).toEqual([ "reasoning:comparability", ]); expect(result.emergentReasoningNodeCreated).toBe(true); expect(result.emergentReasoningNodeId).toBeTruthy(); expect(result.emergentReasoningNodeReason).toContain("backed by the graph"); expect(result.previousReasoningState.comparabilityStatus).toBe("uncertain"); expect(result.reasoningState).toMatchObject({ comparabilityStatus: "confirmed", relationshipStatus: "potentially_related", relationshipAssessed: true, }); expect(result.reasoningState.comparabilityEvidence).toEqual([ comparabilityUnknownId, ]); expect(result.selectedQuestion?.nodeId).toBe(result.newActiveUnknownNodeId); expect(result.selectedQuestion).toMatchObject({ nodeId: result.newActiveUnknownNodeId, question: "What evidence would clarify how the two observations were measured?", }); expect(result.selectedQuestion?.question.toLowerCase()).not.toMatch( /dso|debtor days|receivables turnover|working capital|receivables/, ); expect(result.reasoningState.reasoningStages).toEqual([ { stage: "comparability", status: "confirmed", outcome: "Comparability was confirmed by the user answer covering the same period and source basis.", }, { stage: "relationship", status: "potentially_related", outcome: "The observations concern connected business signals but do not establish a direct contradiction or cause.", }, ]); const emergentNode = result.updatedSituationGraph.nodes.find( (node) => node.id === result.emergentReasoningNodeId, ); expect(emergentNode).toMatchObject({ kind: "unknown", status: "unknown", confidence: "medium", }); expect(emergentNode.description.toLowerCase()).toContain("because"); expect( result.updatedSituationGraph.edges.filter( (edge) => edge.toNodeId === result.emergentReasoningNodeId, ), ).not.toEqual([]); expect( result.updatedSituationGraph.edges.some( (edge) => edge.toNodeId === result.emergentReasoningNodeId && edge.relationship === "causes", ), ).toBe(false); expect( JSON.stringify( result.updatedSituationGraph.nodes.find( (node) => node.id === "n-unrelated", ), ), ).toBe(originalUnrelatedNode); }); it("reuses an equivalent existing unresolved reasoning unknown instead of creating a duplicate", () => { const { graph, proposal } = makeComparabilityUpdateFixture(); graph.nodes.push( makeNode({ id: "n-existing-explanation", label: "Explanation for why Revenue increased by 18%, but cash in the bank fell over the same period", description: "Need to understand what change or event could explain why these observations differ, because that is needed to investigate their relationship.", kind: "unknown", status: "unknown", confidence: "medium", }), ); const result = applyValidatedProposal({ situationGraph: graph, proposal, previousQuestion: "Were these figures measured on the same basis and at the same scale?", answer: "Yes. Both figures cover the same accounting period and are taken from the same management accounts.", }); expect(result.success).toBe(true); expect(result.emergentReasoningNodeCreated).toBe(false); expect(result.emergentReasoningNodeId).toBe("n-existing-explanation"); expect(result.newActiveUnknownNodeId).not.toBe("n-existing-explanation"); expect(result.selectedQuestion?.nodeId).not.toBe("n-existing-explanation"); expect(result.selectedQuestion?.question).toBe( "What evidence would clarify how the two observations were measured?", ); expect( result.updatedSituationGraph.nodes.filter( (node) => node.label === graph.nodes.at(-1).label, ), ).toHaveLength(1); }); it("decomposes a composite selected unknown before asking the next question", () => { const { graph, proposal } = makeComparabilityUpdateFixture(); const result = applyValidatedProposal({ situationGraph: graph, proposal, previousQuestion: "Were these figures measured on the same basis and at the same scale?", answer: "Yes. Both figures cover the same accounting period and are taken from the same management accounts.", }); expect(result.success).toBe(true); expect(result.atomicityAssessment).toBe("composite"); expect(result.decompositionPerformed).toBe(true); expect(result.childUnknownCount).toBe(5); expect(result.childNodeIds).toHaveLength(5); expect(result.atomicityReason).toBeTruthy(); expect(result.selectedQuestion?.nodeId).toBe(result.newActiveUnknownNodeId); expect(result.selectedQuestion?.nodeId).not.toBe( result.emergentReasoningNodeId, ); expect(result.selectedQuestion?.question.toLowerCase()).not.toMatch( /dso|working capital|receivables|capex/, ); const parentNode = result.updatedSituationGraph.nodes.find( (node) => node.id === result.emergentReasoningNodeId, ); expect(parentNode?.status).toBe("unknown"); const childNodes = result.updatedSituationGraph.nodes.filter((node) => result.childNodeIds.includes(node.id), ); expect(childNodes).toHaveLength(5); expect(childNodes.every((node) => node.parentId === parentNode.id)).toBe( true, ); expect( result.updatedSituationGraph.edges.filter( (edge) => result.childNodeIds.includes(edge.fromNodeId) && edge.toNodeId === parentNode.id && edge.relationship === "depends_on", ), ).toHaveLength(5); }); it("reuses existing decomposition children instead of duplicating them", () => { const { graph, proposal } = makeComparabilityUpdateFixture(); const firstResult = applyValidatedProposal({ situationGraph: graph, proposal, previousQuestion: "Were these figures measured on the same basis and at the same scale?", answer: "Yes. Both figures cover the same accounting period and are taken from the same management accounts.", }); expect(firstResult.success).toBe(true); const secondResult = applyValidatedProposal({ situationGraph: graph, proposal, previousQuestion: "Were these figures measured on the same basis and at the same scale?", answer: "Yes. Both figures cover the same accounting period and are taken from the same management accounts.", }); expect(secondResult.success).toBe(true); expect(secondResult.atomicityAssessment).toBe("composite"); const uniqueChildIds = new Set(firstResult.childNodeIds); expect(uniqueChildIds.size).toBe(firstResult.childNodeIds.length); expect( secondResult.updatedSituationGraph.nodes.filter((node) => firstResult.childNodeIds.includes(node.id), ), ).toHaveLength(firstResult.childNodeIds.length); }); it("reselects a remaining commercial sibling after resolving the first child", () => { const graph = makeCommercialUpdateFixture(); const firstResult = applyValidatedProposal({ situationGraph: graph, proposal: makeMeaningfulNoOpProposal(), }); expect(firstResult.success).toBe(true); expect(firstResult.selectedQuestion?.question).toBe( "Who experiences this problem?", ); const secondResult = applyValidatedProposal({ situationGraph: firstResult.updatedSituationGraph, proposal: { addedNodes: [], updatedNodes: [ { nodeId: firstResult.selectedQuestion.nodeId, previousStatus: "unknown", newStatus: "resolved", previousValue: null, newValue: "I experience it myself when I am trying to decide whether a project, idea or investment is justified, but I do not yet know how common that problem is for other people.", reason: "The answer confirms a self-observed instance.", }, ], addedEdges: [], removedEdgeIds: [], resolvedUnknownNodeIds: [firstResult.selectedQuestion.nodeId], affectedNodeIds: [], selectedQuestion: null, }, previousQuestion: firstResult.selectedQuestion.question, answer: "I experience it myself when I am trying to decide whether a project, idea or investment is justified, but I do not yet know how common that problem is for other people.", }); expect(secondResult.success).toBe(true); expect(secondResult.resolvedUnknownNodeIds).toContain( firstResult.selectedQuestion.nodeId, ); expect(secondResult.newActiveUnknownNodeId).toBe( secondResult.selectedQuestion?.nodeId, ); expect(secondResult.selectedQuestion?.question).toBe( "What makes you think other people experience this problem too?", ); expect(secondResult.selectedQuestion?.reasoningPattern).toBe("decision"); expect(secondResult.selectedQuestion?.questionFamily).toBe( "decision_foundation", ); expect(secondResult.selectedQuestion?.nodeId).not.toBe( firstResult.selectedQuestion.nodeId, ); expect(secondResult.unresolvedCandidateCount).toBeGreaterThan(0); expect(secondResult.eligibleCandidateCount).toBeGreaterThan(0); expect(secondResult.candidateNodeIds).toContain( secondResult.selectedQuestion?.nodeId, ); expect(secondResult.resolvedCurrentTurnNodeIds).toContain( firstResult.selectedQuestion.nodeId, ); expect(secondResult.noQuestionReason).toBeNull(); expect(secondResult.selectedQuestion?.question.toLowerCase()).not.toMatch( /price|budget|market size|pilot metrics|benchmark|technical differentiation/, ); expect(secondResult.reasoningPatternValidation).toMatchObject({ activePattern: "decision", valid: true, }); expect(secondResult.graphReasoningIntegrity).toBe("valid"); expect(secondResult.incompatibleNodeIds).toEqual([]); expect(secondResult.compatibilityFailures).toEqual([]); }); it("does not allow a decision-mode active unknown to remain a comparison child", () => { const graph = makeCommercialUpdateFixture(); graph.nodes.push( makeNode({ id: "n-commercial-comparison-child", label: "How the two observations were measured", description: "Need evidence about the measure used for each observation before comparing them.", kind: "unknown", status: "unknown", confidence: "medium", parentId: "n-commercial-parent", }), ); graph.activeUnknownNodeId = "n-commercial-comparison-child"; const result = applyValidatedProposal({ situationGraph: graph, proposal: makeMeaningfulNoOpProposal(), }); expect(result.success).toBe(true); expect(result.reasoningPatternValidation).toMatchObject({ activePattern: "decision", valid: true, }); expect(result.graphReasoningIntegrity).toBe("valid"); expect(result.incompatibleNodeIds).toContain( "n-commercial-comparison-child", ); expect(result.compatibilityFailures).toEqual( expect.arrayContaining([ expect.objectContaining({ nodeId: "n-commercial-comparison-child", activePattern: "decision", nodePattern: "comparison", }), ]), ); expect(result.replacementActions).toEqual( expect.arrayContaining([ expect.objectContaining({ rejectedNodeId: "n-commercial-comparison-child", replacementNodeId: result.selectedQuestion?.nodeId, }), ]), ); expect(result.selectedQuestion?.nodeId).not.toBe( "n-commercial-comparison-child", ); expect(result.selectedQuestion?.reasoningPattern).toBe("decision"); }); });