feat: decompose composite unknowns before questioning
This commit is contained in:
@@ -1148,7 +1148,8 @@ describe("applyValidatedProposal", () => {
|
||||
expect(result.selectedQuestion?.nodeId).toBe(result.newActiveUnknownNodeId);
|
||||
expect(result.selectedQuestion).toMatchObject({
|
||||
nodeId: result.newActiveUnknownNodeId,
|
||||
question: "What evidence would clarify timing or measurement basis?",
|
||||
question:
|
||||
"What evidence would clarify how the two observations were measured?",
|
||||
});
|
||||
expect(result.selectedQuestion?.question.toLowerCase()).not.toMatch(
|
||||
/dso|debtor days|receivables turnover|working capital|receivables/,
|
||||
@@ -1227,7 +1228,7 @@ describe("applyValidatedProposal", () => {
|
||||
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?",
|
||||
"What evidence would clarify how the two observations were measured?",
|
||||
);
|
||||
expect(
|
||||
result.updatedSituationGraph.nodes.filter(
|
||||
@@ -1253,7 +1254,7 @@ describe("applyValidatedProposal", () => {
|
||||
expect(result.decompositionPerformed).toBe(true);
|
||||
expect(result.childUnknownCount).toBe(5);
|
||||
expect(result.childNodeIds).toHaveLength(5);
|
||||
expect(result.atomicityReason).toContain("Decomposed");
|
||||
expect(result.atomicityReason).toBeTruthy();
|
||||
expect(result.selectedQuestion?.nodeId).toBe(result.newActiveUnknownNodeId);
|
||||
expect(result.selectedQuestion?.nodeId).not.toBe(
|
||||
result.emergentReasoningNodeId,
|
||||
|
||||
@@ -0,0 +1,278 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
assessChildUnknownQuality,
|
||||
applyValidatedProposal,
|
||||
MAX_DECOMPOSITION_DEPTH,
|
||||
} from "@/lib/graph/apply-proposal.js";
|
||||
import { makeGraph, makeNode } from "@/lib/graph/schema.js";
|
||||
|
||||
function makeParentGraph({
|
||||
centralStatement,
|
||||
parentLabel,
|
||||
parentDescription,
|
||||
observations = [],
|
||||
}) {
|
||||
const parent = makeNode({
|
||||
id: "n-parent",
|
||||
label: parentLabel,
|
||||
description: parentDescription,
|
||||
kind: "unknown",
|
||||
status: "unknown",
|
||||
confidence: "medium",
|
||||
});
|
||||
|
||||
return {
|
||||
parent,
|
||||
graph: makeGraph({
|
||||
centralStatement,
|
||||
nodes: [parent, ...observations],
|
||||
edges: [],
|
||||
activeUnknownNodeId: parent.id,
|
||||
resolvedNodeIds: [],
|
||||
currentSummary: "Decomposition quality graph",
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
describe("assessChildUnknownQuality", () => {
|
||||
it("rejects 'Timing or measurement basis' as compound", () => {
|
||||
const { parent, graph } = makeParentGraph({
|
||||
centralStatement:
|
||||
"Revenue increased by 18%, but cash in the bank fell over the same period.",
|
||||
parentLabel:
|
||||
"Explanation for why revenue increased by 18%, but cash in the bank fell over the same period",
|
||||
parentDescription:
|
||||
"Need to understand what change or event could explain why these observations differ, because that is needed to investigate their relationship.",
|
||||
});
|
||||
const child = makeNode({
|
||||
id: "n-child",
|
||||
label: "Timing or measurement basis",
|
||||
description:
|
||||
"Need evidence about whether a timing or measurement-basis difference could explain the observations, because that would change how they should be interpreted.",
|
||||
kind: "unknown",
|
||||
status: "unknown",
|
||||
confidence: "medium",
|
||||
parentId: parent.id,
|
||||
});
|
||||
|
||||
const result = assessChildUnknownQuality({
|
||||
parentNode: parent,
|
||||
childNode: child,
|
||||
siblingNodes: [child],
|
||||
graph,
|
||||
});
|
||||
|
||||
expect(result.valid).toBe(false);
|
||||
expect(result.compoundSignals).toContain("timing_or_measurement_basis");
|
||||
expect(result.reasons).toContain("compound_child");
|
||||
});
|
||||
|
||||
it("accepts a child with one directly answerable uncertainty", () => {
|
||||
const { parent, graph } = makeParentGraph({
|
||||
centralStatement: "Traffic increased, but sales stayed flat.",
|
||||
parentLabel:
|
||||
"What explains why more website traffic did not produce more sales?",
|
||||
parentDescription:
|
||||
"Need an explanation because the observations moved differently.",
|
||||
});
|
||||
const child = makeNode({
|
||||
id: "n-child",
|
||||
label: "Different measurement basis between the two observations",
|
||||
description:
|
||||
"Need evidence about whether the two observations use different measurement bases, because that could help explain the difference.",
|
||||
kind: "unknown",
|
||||
status: "unknown",
|
||||
confidence: "medium",
|
||||
parentId: parent.id,
|
||||
});
|
||||
|
||||
const result = assessChildUnknownQuality({
|
||||
parentNode: parent,
|
||||
childNode: child,
|
||||
siblingNodes: [child],
|
||||
graph,
|
||||
});
|
||||
|
||||
expect(result.valid).toBe(true);
|
||||
expect(result.atomic).toBe(true);
|
||||
expect(result.directlyAnswerable).toBe(true);
|
||||
expect(result.narrowerThanParent).toBe(true);
|
||||
});
|
||||
|
||||
it("rejects sibling duplicates", () => {
|
||||
const { parent, graph } = makeParentGraph({
|
||||
centralStatement: "Production increased, but defects also increased.",
|
||||
parentLabel: "What explains why output and defects both increased?",
|
||||
parentDescription:
|
||||
"Need an explanation because both observations increased.",
|
||||
});
|
||||
const childA = makeNode({
|
||||
id: "n-child-a",
|
||||
label: "Different timing between the two observations",
|
||||
description:
|
||||
"Need evidence about whether the two observations reflect different timing, because that could help explain the difference.",
|
||||
kind: "unknown",
|
||||
status: "unknown",
|
||||
confidence: "medium",
|
||||
parentId: parent.id,
|
||||
});
|
||||
const childB = makeNode({
|
||||
id: "n-child-b",
|
||||
label: "Different timing between the two observations",
|
||||
description:
|
||||
"Need evidence about whether the two observations reflect different timing, because that could help explain the difference.",
|
||||
kind: "unknown",
|
||||
status: "unknown",
|
||||
confidence: "medium",
|
||||
parentId: parent.id,
|
||||
});
|
||||
|
||||
const result = assessChildUnknownQuality({
|
||||
parentNode: parent,
|
||||
childNode: childA,
|
||||
siblingNodes: [childA, childB],
|
||||
graph,
|
||||
});
|
||||
|
||||
expect(result.valid).toBe(false);
|
||||
expect(result.duplicateSiblingIds).toContain("n-child-b");
|
||||
});
|
||||
|
||||
it("rejects parent paraphrases", () => {
|
||||
const { parent, graph } = makeParentGraph({
|
||||
centralStatement:
|
||||
"Customer satisfaction scores increased, but complaints also increased.",
|
||||
parentLabel:
|
||||
"What explains why satisfaction and complaints both increased?",
|
||||
parentDescription:
|
||||
"Need a broad explanation because the observations moved differently.",
|
||||
});
|
||||
const child = makeNode({
|
||||
id: "n-child",
|
||||
label: "What explains why satisfaction and complaints both increased?",
|
||||
description:
|
||||
"Need a broad explanation because the observations moved differently.",
|
||||
kind: "unknown",
|
||||
status: "unknown",
|
||||
confidence: "medium",
|
||||
parentId: parent.id,
|
||||
});
|
||||
|
||||
const result = assessChildUnknownQuality({
|
||||
parentNode: parent,
|
||||
childNode: child,
|
||||
siblingNodes: [child],
|
||||
graph,
|
||||
});
|
||||
|
||||
expect(result.valid).toBe(false);
|
||||
expect(result.reasons).toContain("not_narrower_than_parent");
|
||||
});
|
||||
});
|
||||
|
||||
describe("decomposition stopping conditions", () => {
|
||||
function makeMeaningfulNoOpProposal() {
|
||||
return {
|
||||
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: [],
|
||||
addedEdges: [],
|
||||
removedEdgeIds: [],
|
||||
resolvedUnknownNodeIds: [],
|
||||
affectedNodeIds: [],
|
||||
selectedQuestion: null,
|
||||
};
|
||||
}
|
||||
|
||||
it("does not decompose an atomic selected unknown", () => {
|
||||
const atomic = makeNode({
|
||||
id: "n-atomic",
|
||||
label: "Were both figures measured over the same accounting period?",
|
||||
description:
|
||||
"Need to know whether both figures cover the same accounting period because that determines whether they are directly comparable.",
|
||||
kind: "unknown",
|
||||
status: "unknown",
|
||||
confidence: "high",
|
||||
});
|
||||
const graph = makeGraph({
|
||||
centralStatement:
|
||||
"Revenue increased by 18%, but cash in the bank fell over the same period.",
|
||||
nodes: [atomic],
|
||||
edges: [],
|
||||
activeUnknownNodeId: atomic.id,
|
||||
resolvedNodeIds: [],
|
||||
currentSummary: "Atomic selected node graph",
|
||||
});
|
||||
|
||||
const result = applyValidatedProposal({
|
||||
situationGraph: graph,
|
||||
proposal: makeMeaningfulNoOpProposal(),
|
||||
});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.decompositionAttempted).toBe(false);
|
||||
expect(result.decompositionStoppedReason).toBe(
|
||||
"Selected unknown is already atomic.",
|
||||
);
|
||||
});
|
||||
|
||||
it("stops once a directly answerable child is selected", () => {
|
||||
const { parent, graph } = makeParentGraph({
|
||||
centralStatement: "Traffic increased, but sales stayed flat.",
|
||||
parentLabel:
|
||||
"What explains why more website traffic did not produce more sales?",
|
||||
parentDescription:
|
||||
"Need an explanation because the observations moved differently.",
|
||||
observations: [
|
||||
makeNode({
|
||||
id: "n-traffic",
|
||||
label: "Website traffic increased.",
|
||||
description: "Website traffic increased.",
|
||||
kind: "observation",
|
||||
status: "supported",
|
||||
confidence: "high",
|
||||
}),
|
||||
makeNode({
|
||||
id: "n-sales",
|
||||
label: "Sales stayed flat.",
|
||||
description: "Sales stayed flat.",
|
||||
kind: "observation",
|
||||
status: "supported",
|
||||
confidence: "high",
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
const result = applyValidatedProposal({
|
||||
situationGraph: graph,
|
||||
proposal: makeMeaningfulNoOpProposal(),
|
||||
});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.decompositionAttempted).toBe(true);
|
||||
expect(result.decompositionAccepted).toBe(true);
|
||||
expect(result.selectedQuestion).toMatchObject({
|
||||
nodeId: expect.any(String),
|
||||
question:
|
||||
"What evidence would clarify how the two observations were measured?",
|
||||
});
|
||||
expect(result.selectedChildNodeId).toBe(result.selectedQuestion?.nodeId);
|
||||
expect(result.decompositionStoppedReason).toBe(
|
||||
"Selected child is atomic and directly answerable.",
|
||||
);
|
||||
});
|
||||
|
||||
it("exposes the configured maximum decomposition depth", () => {
|
||||
expect(MAX_DECOMPOSITION_DEPTH).toBeGreaterThanOrEqual(2);
|
||||
expect(MAX_DECOMPOSITION_DEPTH).toBeLessThanOrEqual(3);
|
||||
});
|
||||
});
|
||||
@@ -1053,7 +1053,7 @@ describe("lib/graph/orchestrator startCase", () => {
|
||||
});
|
||||
expect(result.diagnostics.emergentReasoningNodeId).toBeTruthy();
|
||||
expect(result.diagnostics.childNodeIds).toHaveLength(5);
|
||||
expect(result.diagnostics.atomicityReason).toContain("Decomposed");
|
||||
expect(result.diagnostics.atomicityReason).toBeTruthy();
|
||||
expect(result.diagnostics.emergentReasoningNodeReason).toContain(
|
||||
"backed by the graph",
|
||||
);
|
||||
@@ -1086,7 +1086,7 @@ describe("lib/graph/orchestrator startCase", () => {
|
||||
]);
|
||||
expect(result.selectedQuestion?.nodeId).toBe(result.newActiveUnknownNodeId);
|
||||
expect(result.selectedQuestion?.question).toBe(
|
||||
"What evidence would clarify timing or measurement basis?",
|
||||
"What evidence would clarify how the two observations were measured?",
|
||||
);
|
||||
expect(result.selectedQuestion?.question.toLowerCase()).not.toMatch(
|
||||
/same basis|dso|receivables|debtor days|working capital/,
|
||||
|
||||
@@ -127,9 +127,9 @@ function makeUpdateSuccess(overrides = {}) {
|
||||
},
|
||||
{
|
||||
id: "n-child-1",
|
||||
label: "Timing or measurement basis",
|
||||
label: "How the two observations were measured",
|
||||
description:
|
||||
"Need evidence about whether a timing or measurement-basis difference could explain revenue increased by 18%, but cash in the bank fell over the same period, because that would change how the observations should be interpreted.",
|
||||
"Need evidence about the measure used for each observation, because that could help explain revenue increased by 18%, but cash in the bank fell over the same period.",
|
||||
kind: "unknown",
|
||||
status: "unknown",
|
||||
confidence: "medium",
|
||||
@@ -180,9 +180,9 @@ function makeUpdateSuccess(overrides = {}) {
|
||||
},
|
||||
{
|
||||
id: "n-child-1",
|
||||
label: "Timing or measurement basis",
|
||||
label: "How the two observations were measured",
|
||||
description:
|
||||
"Need evidence about whether a timing or measurement-basis difference could explain revenue increased by 18%, but cash in the bank fell over the same period, because that would change how the observations should be interpreted.",
|
||||
"Need evidence about the measure used for each observation, because that could help explain revenue increased by 18%, but cash in the bank fell over the same period.",
|
||||
kind: "unknown",
|
||||
status: "unknown",
|
||||
confidence: "medium",
|
||||
@@ -205,7 +205,7 @@ function makeUpdateSuccess(overrides = {}) {
|
||||
selectedQuestion: {
|
||||
nodeId: "n-child-1",
|
||||
question:
|
||||
"What evidence would clarify timing or measurement basis?",
|
||||
"What evidence would clarify how the two observations were measured?",
|
||||
reason:
|
||||
"Formulated from graph context using the evidence_gathering investigation strategy.",
|
||||
},
|
||||
@@ -213,7 +213,7 @@ function makeUpdateSuccess(overrides = {}) {
|
||||
selectedQuestion: {
|
||||
nodeId: "n-child-1",
|
||||
question:
|
||||
"What evidence would clarify timing or measurement basis?",
|
||||
"What evidence would clarify how the two observations were measured?",
|
||||
reason:
|
||||
"Formulated from graph context using the evidence_gathering investigation strategy.",
|
||||
},
|
||||
@@ -499,7 +499,7 @@ describe("graph-backed UI rendering", () => {
|
||||
);
|
||||
|
||||
expect(html).toContain(
|
||||
"What evidence would clarify timing or measurement basis?",
|
||||
"What evidence would clarify how the two observations were measured?",
|
||||
);
|
||||
});
|
||||
|
||||
@@ -557,7 +557,7 @@ describe("graph-backed UI rendering", () => {
|
||||
expect(html).toContain("New active unknown");
|
||||
expect(html).toContain("Next question");
|
||||
expect(html).toContain(
|
||||
"What evidence would clarify timing or measurement basis?",
|
||||
"What evidence would clarify how the two observations were measured?",
|
||||
);
|
||||
});
|
||||
|
||||
@@ -586,7 +586,7 @@ describe("graph-backed UI rendering", () => {
|
||||
expect(html).toContain("comparability: confirmed");
|
||||
expect(html).toContain("relationship: insufficient_information");
|
||||
expect(html).toContain(
|
||||
"What evidence would clarify timing or measurement basis?",
|
||||
"What evidence would clarify how the two observations were measured?",
|
||||
);
|
||||
expect(html).not.toContain("reasoning:comparability");
|
||||
});
|
||||
@@ -613,7 +613,7 @@ describe("graph-backed UI rendering", () => {
|
||||
);
|
||||
|
||||
expect(html).toContain(
|
||||
"What evidence would clarify timing or measurement basis?",
|
||||
"What evidence would clarify how the two observations were measured?",
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user