feat: evaluate deterministic cross-branch corroboration
This commit is contained in:
@@ -127,7 +127,7 @@ describe("confidence propagation", () => {
|
||||
expect(parent.status).toBe("provisional");
|
||||
expect(parent.confidence).toBe("medium");
|
||||
expect(parent.confidenceAssessment).toEqual({
|
||||
evidenceConfidence: "high",
|
||||
evidenceConfidence: "medium",
|
||||
completenessStatus: "partial",
|
||||
conclusionConfidence: "medium",
|
||||
});
|
||||
|
||||
@@ -0,0 +1,306 @@
|
||||
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,
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -257,7 +257,7 @@ describe("upward propagation", () => {
|
||||
expect(result.parentConfidenceBefore).toBe("medium");
|
||||
expect(result.parentConfidenceAfter).toBe("medium");
|
||||
expect(result.evidenceConfidenceBefore).toBe("medium");
|
||||
expect(result.evidenceConfidenceAfter).toBe("high");
|
||||
expect(result.evidenceConfidenceAfter).toBe("medium");
|
||||
expect(result.completenessBefore).toBe("empty");
|
||||
expect(result.completenessAfter).toBe("partial");
|
||||
expect(result.conclusionConfidenceBefore).toBe("low");
|
||||
@@ -287,7 +287,7 @@ describe("upward propagation", () => {
|
||||
status: "provisional",
|
||||
confidence: "medium",
|
||||
confidenceAssessment: {
|
||||
evidenceConfidence: "high",
|
||||
evidenceConfidence: "medium",
|
||||
completenessStatus: "partial",
|
||||
conclusionConfidence: "medium",
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user