304 lines
9.9 KiB
JavaScript
304 lines
9.9 KiB
JavaScript
import { describe, expect, it } from "vitest";
|
|
import {
|
|
formulateQuestion,
|
|
formulateTieResolutionQuestion,
|
|
} from "@/lib/graph/question-formulator.js";
|
|
import { makeEdge, makeGraph, makeNode } from "@/lib/graph/schema.js";
|
|
import {
|
|
explainUnknownSelection,
|
|
selectActiveUnknownCandidate,
|
|
} from "@/lib/graph/utils.js";
|
|
|
|
function buildLiveShapedGraph() {
|
|
const summary = makeNode({
|
|
id: "nnvog0y",
|
|
label:
|
|
"Revenue grew by 18% while corporate cash reserves declined over an identical time frame.",
|
|
description: "Summary of the situation from the scenario text",
|
|
kind: "state",
|
|
status: "provisional",
|
|
confidence: "medium",
|
|
});
|
|
const revenueObservation = makeNode({
|
|
id: "nri36w9",
|
|
label: "Revenue increased by 18%.",
|
|
description: "Revenue increased by 18%.",
|
|
kind: "observation",
|
|
status: "supported",
|
|
confidence: "high",
|
|
evidenceIds: ["obs_rev"],
|
|
});
|
|
const cashObservation = makeNode({
|
|
id: "nnfc48j",
|
|
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",
|
|
evidenceIds: ["obs_cash"],
|
|
});
|
|
const revenueMetric = makeNode({
|
|
id: "nhsd6d5",
|
|
label: "Revenue metric (typically accrual-based income statement figure)",
|
|
description:
|
|
"Revenue metric (typically accrual-based income statement figure)",
|
|
kind: "metric",
|
|
status: "known",
|
|
confidence: "high",
|
|
});
|
|
const cashMetric = makeNode({
|
|
id: "neh5m6m",
|
|
label:
|
|
"Cash balance (liquidity measure on the balance sheet or cash flow statement)",
|
|
description:
|
|
"Cash balance (liquidity measure on the balance sheet or cash flow statement)",
|
|
kind: "metric",
|
|
status: "known",
|
|
confidence: "high",
|
|
});
|
|
const directionalRelationship = makeNode({
|
|
id: "nwo6070",
|
|
label:
|
|
"Divergent directional movement between top-line revenue growth and net cash position contraction.",
|
|
description:
|
|
"Divergent directional movement between top-line revenue growth and net cash position contraction.",
|
|
kind: "relationship",
|
|
status: "supported",
|
|
confidence: "high",
|
|
});
|
|
const contradictionRelationship = makeNode({
|
|
id: "nuiab02",
|
|
label:
|
|
"Apparent contradiction between profitability/revenue expansion and liquidity reduction.",
|
|
description:
|
|
"Apparent contradiction between profitability/revenue expansion and liquidity reduction.",
|
|
kind: "relationship",
|
|
status: "supported",
|
|
confidence: "medium",
|
|
});
|
|
const cashTiming = makeNode({
|
|
id: "niewza",
|
|
label:
|
|
"Whether revenue recognition timing differs from cash collection timing.",
|
|
description:
|
|
"Whether revenue recognition timing differs from cash collection timing.",
|
|
kind: "unknown",
|
|
status: "unknown",
|
|
confidence: "high",
|
|
});
|
|
const cashOutflows = makeNode({
|
|
id: "nqdzobz",
|
|
label:
|
|
"Magnitude and nature of cash outflows (operating expenses, debt repayments, capex, or working capital shifts).",
|
|
description:
|
|
"Magnitude and nature of cash outflows (operating expenses, debt repayments, capex, or working capital shifts).",
|
|
kind: "unknown",
|
|
status: "unknown",
|
|
confidence: "high",
|
|
});
|
|
|
|
const edges = [
|
|
makeEdge({
|
|
id: "e-revenue-summary",
|
|
fromNodeId: revenueObservation.id,
|
|
toNodeId: summary.id,
|
|
relationship: "supports",
|
|
description: "Revenue increase supports the scenario summary.",
|
|
}),
|
|
makeEdge({
|
|
id: "e-cash-summary",
|
|
fromNodeId: cashObservation.id,
|
|
toNodeId: summary.id,
|
|
relationship: "supports",
|
|
description: "Cash decline supports the scenario summary.",
|
|
}),
|
|
makeEdge({
|
|
id: "e-unk-niewza",
|
|
fromNodeId: cashTiming.id,
|
|
toNodeId: summary.id,
|
|
relationship: "depends_on",
|
|
description:
|
|
"Whether revenue recognition timing differs from cash collection timing. is an unresolved factor for this situation",
|
|
}),
|
|
makeEdge({
|
|
id: "e-unk-nqdzobz",
|
|
fromNodeId: cashOutflows.id,
|
|
toNodeId: summary.id,
|
|
relationship: "depends_on",
|
|
description:
|
|
"Magnitude and nature of cash outflows (operating expenses, debt repayments, capex, or working capital shifts). is an unresolved factor for this situation",
|
|
}),
|
|
];
|
|
|
|
return makeGraph({
|
|
centralStatement:
|
|
"Revenue increased by 18%, but cash in the bank fell over the same period.",
|
|
nodes: [
|
|
summary,
|
|
revenueObservation,
|
|
cashObservation,
|
|
revenueMetric,
|
|
cashMetric,
|
|
directionalRelationship,
|
|
contradictionRelationship,
|
|
cashTiming,
|
|
cashOutflows,
|
|
],
|
|
edges,
|
|
activeUnknownNodeId: null,
|
|
resolvedNodeIds: [],
|
|
currentSummary: "Diagnostic selection influence fixture",
|
|
});
|
|
}
|
|
|
|
function orderCandidates(explanation) {
|
|
return explanation.candidates.map((candidate) => ({
|
|
nodeId: candidate.nodeId,
|
|
label: candidate.label,
|
|
score: candidate.score,
|
|
downstreamCount: candidate.downstreamCount,
|
|
unresolvedParentUnknownCount: candidate.unresolvedParentUnknownCount,
|
|
}));
|
|
}
|
|
|
|
function removeDependencyLinks(graph) {
|
|
const nodes = graph.nodes.map((node) => ({
|
|
...node,
|
|
dependsOn: [],
|
|
affects: [],
|
|
parentId: null,
|
|
childIds: [],
|
|
}));
|
|
const edges = (graph.edges || []).filter(
|
|
(edge) => edge.relationship !== "depends_on",
|
|
);
|
|
return makeGraph({ ...graph, nodes, edges, activeUnknownNodeId: null });
|
|
}
|
|
|
|
function neutraliseUnknownWording(graph) {
|
|
let counter = 0;
|
|
const nodes = graph.nodes.map((node) => {
|
|
if (node.kind !== "unknown") return { ...node };
|
|
counter += 1;
|
|
return {
|
|
...node,
|
|
label: `Unknown ${String.fromCharCode(64 + counter)}`,
|
|
description: `Unknown factor ${counter} relevant to the scenario.`,
|
|
};
|
|
});
|
|
return makeGraph({ ...graph, nodes, activeUnknownNodeId: null });
|
|
}
|
|
|
|
describe("selection influence diagnostic", () => {
|
|
it("records ambiguous ordering changes for live-shaped, structure-only, and wording-neutralised fixtures", () => {
|
|
const liveGraph = buildLiveShapedGraph();
|
|
const liveExplanation = explainUnknownSelection(liveGraph, []);
|
|
const liveSelection = selectActiveUnknownCandidate(liveGraph, []);
|
|
const tieQuestion = formulateTieResolutionQuestion({ graph: liveGraph });
|
|
|
|
const noLinksExplanation = explainUnknownSelection(
|
|
removeDependencyLinks(liveGraph),
|
|
[],
|
|
);
|
|
const noLinksSelection = selectActiveUnknownCandidate(
|
|
removeDependencyLinks(liveGraph),
|
|
[],
|
|
);
|
|
const neutralWordingExplanation = explainUnknownSelection(
|
|
neutraliseUnknownWording(liveGraph),
|
|
[],
|
|
);
|
|
const neutralSelection = selectActiveUnknownCandidate(
|
|
neutraliseUnknownWording(liveGraph),
|
|
[],
|
|
);
|
|
|
|
const fallbackQuestion = formulateQuestion({
|
|
node: liveGraph.nodes.find((node) => node.id === "nqdzobz"),
|
|
graph: liveGraph,
|
|
});
|
|
|
|
const diagnosticRecord = {
|
|
liveStatus: liveExplanation.status,
|
|
liveShapedCandidateOrdering: orderCandidates(liveExplanation),
|
|
liveTiedCandidateIds: liveExplanation.tiedCandidateIds,
|
|
noLinksCandidateOrdering: orderCandidates(noLinksExplanation),
|
|
noLinksStatus: noLinksExplanation.status,
|
|
neutralWordingCandidateOrdering: orderCandidates(
|
|
neutralWordingExplanation,
|
|
),
|
|
neutralStatus: neutralWordingExplanation.status,
|
|
selectedExplanationContributions: liveExplanation.selected?.contributions,
|
|
tieQuestion: tieQuestion.question,
|
|
liveSelection,
|
|
noLinksSelection,
|
|
neutralSelection,
|
|
fallbackQuestion,
|
|
};
|
|
|
|
expect(diagnosticRecord.liveStatus).toBe("ambiguous");
|
|
expect(diagnosticRecord.liveTiedCandidateIds).toEqual([
|
|
"nqdzobz",
|
|
"niewza",
|
|
]);
|
|
expect(diagnosticRecord.liveShapedCandidateOrdering).toEqual([
|
|
{
|
|
nodeId: "nqdzobz",
|
|
label:
|
|
"Magnitude and nature of cash outflows (operating expenses, debt repayments, capex, or working capital shifts).",
|
|
score: 0,
|
|
downstreamCount: 0,
|
|
unresolvedParentUnknownCount: 0,
|
|
},
|
|
{
|
|
nodeId: "niewza",
|
|
label:
|
|
"Whether revenue recognition timing differs from cash collection timing.",
|
|
score: 0,
|
|
downstreamCount: 0,
|
|
unresolvedParentUnknownCount: 0,
|
|
},
|
|
]);
|
|
expect(diagnosticRecord.liveSelection).toMatchObject({
|
|
selectedNode: null,
|
|
status: "ambiguous",
|
|
tieType: "complete_unresolved_tie",
|
|
tiedCandidateIds: ["nqdzobz", "niewza"],
|
|
});
|
|
expect(diagnosticRecord.noLinksCandidateOrdering).toEqual(
|
|
diagnosticRecord.liveShapedCandidateOrdering,
|
|
);
|
|
expect(diagnosticRecord.noLinksStatus).toBe("ambiguous");
|
|
expect(diagnosticRecord.noLinksSelection.status).toBe("ambiguous");
|
|
expect(diagnosticRecord.neutralWordingCandidateOrdering).toEqual([
|
|
{
|
|
nodeId: "niewza",
|
|
label: "Unknown A",
|
|
score: 0,
|
|
downstreamCount: 0,
|
|
unresolvedParentUnknownCount: 0,
|
|
},
|
|
{
|
|
nodeId: "nqdzobz",
|
|
label: "Unknown B",
|
|
score: 0,
|
|
downstreamCount: 0,
|
|
unresolvedParentUnknownCount: 0,
|
|
},
|
|
]);
|
|
expect(diagnosticRecord.neutralStatus).toBe("ambiguous");
|
|
expect(diagnosticRecord.neutralSelection.status).toBe("ambiguous");
|
|
expect(diagnosticRecord.selectedExplanationContributions).toBeUndefined();
|
|
expect(diagnosticRecord.tieQuestion).toBe(
|
|
"Were these figures measured on the same basis and at the same scale?",
|
|
);
|
|
expect(diagnosticRecord.tieQuestion.toLowerCase()).not.toMatch(
|
|
/accounts receivable|capex|debt repayments|working capital/,
|
|
);
|
|
expect(diagnosticRecord.fallbackQuestion.strategy).toBeNull();
|
|
expect(diagnosticRecord.fallbackQuestion.question).toBe(
|
|
"What would clarify magnitude and nature of cash outflows (operating expenses, debt repayments, capex, or working capital shifts) in this situation?",
|
|
);
|
|
});
|
|
});
|