feat: decompose composite unknowns before questioning

This commit is contained in:
2026-08-02 19:24:38 +01:00
parent b1c633ba5c
commit 0723c2f49a
9 changed files with 768 additions and 25 deletions
+91 -6
View File
@@ -1146,10 +1146,10 @@ describe("applyValidatedProposal", () => {
comparabilityUnknownId,
]);
expect(result.selectedQuestion?.nodeId).toBe(result.newActiveUnknownNodeId);
expect(result.selectedQuestion?.question).toMatch(
/^What changed during that period that could help explain why /,
);
expect(result.selectedQuestion?.question).not.toContain("same basis");
expect(result.selectedQuestion).toMatchObject({
nodeId: result.newActiveUnknownNodeId,
question: "What evidence would clarify timing or measurement basis?",
});
expect(result.selectedQuestion?.question.toLowerCase()).not.toMatch(
/dso|debtor days|receivables turnover|working capital|receivables/,
);
@@ -1224,12 +1224,97 @@ describe("applyValidatedProposal", () => {
expect(result.success).toBe(true);
expect(result.emergentReasoningNodeCreated).toBe(false);
expect(result.emergentReasoningNodeId).toBe("n-existing-explanation");
expect(result.newActiveUnknownNodeId).toBe("n-existing-explanation");
expect(result.selectedQuestion?.nodeId).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 timing or measurement basis?",
);
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).toContain("Decomposed");
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);
});
});