Files
confidence-engine/tests/graph/cross-branch-corroboration.test.js

307 lines
8.9 KiB
JavaScript

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,
);
});
});