import { describe, expect, it } from "vitest"; import { applyValidatedProposal, evaluateBranchInteractions, } from "@/lib/graph/apply-proposal.js"; import { makeEdge, makeGraph, makeNode } from "@/lib/graph/schema.js"; function makeParentWithBranches(children) { const parent = makeNode({ id: "n-parent", label: "Explanation for why revenue increased while cash fell", 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", }); return makeGraph({ centralStatement: "Revenue increased while cash fell.", nodes: [ parent, ...children.map((child) => ({ ...child, parentId: parent.id })), ], edges: children.map((child, index) => makeEdge({ id: `e-${index + 1}`, fromNodeId: child.id, toNodeId: parent.id, relationship: "depends_on", description: `${child.label} feeds the parent.`, }), ), activeUnknownNodeId: children[0]?.id ?? null, resolvedNodeIds: [], currentSummary: "cross-branch corroboration fixture", }); } function makeResolvedChild(id, label, value, extra = {}) { return makeNode({ id, label, description: label, kind: "unknown", status: "resolved", confidence: "medium", value, evidenceIds: extra.evidenceIds ?? [], }); } function makeUnknownBranch(id, label, description, extra = {}) { return makeNode({ id, label, description, kind: "unknown", status: extra.status ?? "unknown", confidence: extra.confidence ?? "medium", evidenceIds: extra.evidenceIds ?? [], value: extra.value ?? null, }); } describe("evaluateBranchInteractions", () => { it("detects corroborating independent branches", () => { const graph = makeParentWithBranches([ makeResolvedChild("n-a", "Debtor balance increased", "bank-statement-a", { evidenceIds: ["bank-statement-a"], }), makeResolvedChild( "n-b", "Cash receipts were delayed", "receipts-ledger-b", { evidenceIds: ["receipts-ledger-b"] }, ), ]); const parentNode = graph.nodes.find((node) => node.id === "n-parent"); const result = evaluateBranchInteractions({ parentNode, graph }); expect(result.interactionSummary.corroboratingBranchCount).toBe(1); expect(result.interactionSummary.duplicateEvidenceCount).toBe(0); expect(result.interactionSummary.conflictingBranchCount).toBe(0); }); it("detects duplicate evidence instead of corroboration", () => { const graph = makeParentWithBranches([ makeResolvedChild( "n-a", "Bank statement shows increased debtor balance", "same-bank", { evidenceIds: ["same-bank"], }, ), makeResolvedChild( "n-b", "Delayed receipts also cite the bank statement", "same-bank", { evidenceIds: ["same-bank"], }, ), ]); const parentNode = graph.nodes.find((node) => node.id === "n-parent"); const result = evaluateBranchInteractions({ parentNode, graph }); expect(result.interactionSummary.duplicateEvidenceCount).toBe(1); expect(result.interactionSummary.corroboratingBranchCount).toBe(0); }); it("detects conflicting branches", () => { const graph = makeParentWithBranches([ makeResolvedChild("n-a", "Revenue recognised correctly", "correctly"), makeNode({ id: "n-b", label: "Revenue recognised incorrectly", description: "Revenue recognised incorrectly", kind: "unknown", status: "contradicted", confidence: "medium", value: "incorrectly", }), ]); const parentNode = graph.nodes.find((node) => node.id === "n-parent"); const result = evaluateBranchInteractions({ parentNode, graph }); expect(result.interactionSummary.conflictingBranchCount).toBe(1); }); }); describe("cross-branch corroboration effects", () => { function applyToGraph(children, resolvedIds, contradictedIds = []) { const graph = makeParentWithBranches(children); return applyValidatedProposal({ situationGraph: graph, proposal: { 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: [ ...resolvedIds.map((id) => ({ nodeId: id, previousStatus: "unknown", newStatus: "resolved", previousValue: null, newValue: `answer:${id}`, reason: "resolved child", })), ...contradictedIds.map((id) => ({ nodeId: id, previousStatus: "unknown", newStatus: "contradicted", previousValue: null, newValue: `contradiction:${id}`, reason: "contradicted child", })), ], addedEdges: [], removedEdgeIds: [], resolvedUnknownNodeIds: resolvedIds, affectedNodeIds: [], selectedQuestion: null, }, previousQuestion: "What evidence would clarify this branch?", answer: "deterministic branch update", }); } it("independent corroboration increases justified confidence without reaching high on incomplete parent", () => { const result = applyToGraph( [ makeUnknownBranch( "n-a", "Debtor balance increased", "Debtor balance increased", ), makeUnknownBranch( "n-b", "Cash receipts delayed", "Cash receipts delayed", ), makeUnknownBranch( "n-c", "Possible one-off event during the period", "Possible one-off event during the period", ), makeUnknownBranch( "n-d", "Whether the two observations reflect different timing", "Whether the two observations reflect different timing", ), ], ["n-a", "n-b"], ); expect(result.success).toBe(true); expect(result.interactionSummary?.corroboratingBranchCount).toBeGreaterThan( 0, ); expect(result.interactionSummary?.duplicateEvidenceCount).toBe(0); expect(result.parentConfidenceAfter).toBe("medium"); expect(result.confidenceCapReason).toBe( "independent_corroboration_with_incomplete_parent", ); }); it("duplicate evidence does not increase confidence", () => { const result = applyToGraph( [ makeUnknownBranch( "n-a", "Bank statement shows increased debtor balance", "Bank statement shows increased debtor balance", { evidenceIds: ["same-bank"] }, ), makeUnknownBranch( "n-b", "Delayed receipts also cite the bank statement", "Delayed receipts also cite the bank statement", { evidenceIds: ["same-bank"] }, ), makeUnknownBranch( "n-c", "Possible one-off event during the period", "Possible one-off event during the period", ), ], ["n-a", "n-b"], ); expect(result.success).toBe(true); expect(result.interactionSummary?.duplicateEvidenceCount).toBeGreaterThan( 0, ); expect(result.interactionSummary?.corroboratingBranchCount).toBe(0); expect(result.confidenceCapReason).toBe( "duplicate_evidence_no_extra_confidence", ); }); it("conflicting evidence caps confidence", () => { const result = applyToGraph( [ makeUnknownBranch( "n-a", "Revenue recognised correctly", "Revenue recognised correctly", ), makeUnknownBranch( "n-b", "Revenue recognised incorrectly", "Revenue recognised incorrectly", ), makeUnknownBranch( "n-c", "Possible one-off event during the period", "Possible one-off event during the period", ), ], ["n-a"], ["n-b"], ); expect(result.success).toBe(true); expect(result.interactionSummary?.conflictingBranchCount).toBeGreaterThan( 0, ); expect(result.conclusionConfidenceAfter).toBe("low"); }); it("independent branches stay interaction-neutral", () => { const result = applyToGraph( [ makeUnknownBranch( "n-a", "Marketing campaign changed traffic", "Marketing campaign changed traffic", ), makeUnknownBranch( "n-b", "Equipment maintenance occurred", "Equipment maintenance occurred", ), ], ["n-a"], ); expect(result.success).toBe(true); expect(result.interactionSummary?.independentBranchCount).toBeGreaterThan( 0, ); }); });