279 lines
8.8 KiB
JavaScript
279 lines
8.8 KiB
JavaScript
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);
|
|
});
|
|
});
|