Files
confidence-engine/tests/graph/apply-proposal.test.js
T

7024 lines
244 KiB
JavaScript

import { describe, expect, it } from "vitest";
import { applyValidatedProposal, hasRemainingMaterialFactors } from "@/lib/graph/apply-proposal.js";
import { shouldCloseDecision, isUserConfirmationOfNoRemainingUncertainty } from "@/lib/graph/decision-sufficiency.js";
import { makeEdge, makeGraph, makeNode } from "@/lib/graph/schema.js";
import {
selectActiveUnknownCandidate,
validateGraphReferences,
} from "@/lib/graph/utils.js";
function makeComparabilityUpdateFixture() {
const comparabilityUnknown = makeNode({
id: "n-comparability-unknown",
label: "Whether the figures are comparable",
description:
"Need to know whether the figures use the same period, basis, and scale before comparing them.",
kind: "unknown",
status: "unknown",
confidence: "high",
});
const revenueObservation = makeNode({
id: "n-revenue-observation",
label: "Revenue increased by 18%.",
description: "Revenue increased by 18%.",
kind: "observation",
status: "supported",
confidence: "high",
});
const cashObservation = makeNode({
id: "n-cash-observation",
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",
});
const unrelatedNode = makeNode({
id: "n-unrelated",
label: "Board update",
description: "A separate unchanged note.",
kind: "state",
status: "known",
confidence: "low",
});
const graph = makeGraph({
centralStatement:
"Revenue increased by 18%, but cash in the bank fell over the same period.",
nodes: [
comparabilityUnknown,
revenueObservation,
cashObservation,
unrelatedNode,
],
edges: [
makeEdge({
id: "e-revenue-comparability",
fromNodeId: revenueObservation.id,
toNodeId: comparabilityUnknown.id,
relationship: "supports",
confidence: "medium",
description: "Revenue observation requires comparability confirmation.",
}),
makeEdge({
id: "e-cash-comparability",
fromNodeId: cashObservation.id,
toNodeId: comparabilityUnknown.id,
relationship: "supports",
confidence: "medium",
description: "Cash observation requires comparability confirmation.",
}),
],
activeUnknownNodeId: comparabilityUnknown.id,
resolvedNodeIds: [],
currentSummary: "Initial comparability fixture",
reasoningState: {
comparabilityStatus: "uncertain",
comparabilityReason:
"Comparability between the observations is not yet established across period, scale, or measurement basis.",
comparabilityEvidence: [],
relationshipStatus: "insufficient_information",
relationshipReason:
"Relationship classification is deferred until comparability is established.",
relationshipAssessed: false,
contradictionReasoningAllowed: false,
reasoningStages: [
{
stage: "comparability",
status: "uncertain",
outcome:
"Comparability between the observations is not yet established across period, scale, or measurement basis.",
},
{
stage: "relationship",
status: "insufficient_information",
outcome: "not assessed until comparability is established",
},
],
},
});
const proposal = {
addedNodes: [],
updatedNodes: [
{
nodeId: comparabilityUnknown.id,
previousStatus: "unknown",
newStatus: "resolved",
previousValue: null,
newValue:
"Both figures cover the same accounting period and are taken from the same management accounts.",
reason: "The answer confirms the figures are comparable.",
},
],
addedEdges: [],
removedEdgeIds: [],
resolvedUnknownNodeIds: [comparabilityUnknown.id],
affectedNodeIds: [],
selectedQuestion: null,
};
return { graph, proposal, comparabilityUnknownId: comparabilityUnknown.id };
}
function makeApplicationFixture() {
const complaintRateUnknown = makeNode({
id: "n-complaint-rate-unknown",
label: "Complaint rate",
description: "Need the complaint rate per 100 units",
kind: "unknown",
status: "unknown",
confidence: "high",
affects: ["n-quality-deterioration"],
});
const staffingUnknown = makeNode({
id: "n-staffing-unknown",
label: "Staffing change",
description: "Need to know if staffing changed",
kind: "unknown",
status: "unknown",
confidence: "medium",
});
const qualityDeterioration = makeNode({
id: "n-quality-deterioration",
label: "Quality deterioration conclusion",
description: "Conclusion that quality deteriorated",
kind: "conclusion",
status: "supported",
confidence: "medium",
dependsOn: ["n-complaint-rate-unknown"],
});
const complaintCount = makeNode({
id: "n-complaint-count",
label: "Complaint count observation",
description: "Complaint count increased",
kind: "observation",
status: "supported",
confidence: "high",
value: 135,
unit: "count",
});
const productionCount = makeNode({
id: "n-production-count",
label: "Production count observation",
description: "Production increased",
kind: "observation",
status: "supported",
confidence: "high",
value: 7100,
unit: "units",
});
const graph = makeGraph({
centralStatement: "Complaints rose while production also rose.",
nodes: [
complaintRateUnknown,
staffingUnknown,
qualityDeterioration,
complaintCount,
productionCount,
],
edges: [
makeEdge({
id: "e-quality-depends-rate",
fromNodeId: complaintRateUnknown.id,
toNodeId: qualityDeterioration.id,
relationship: "supports",
confidence: "medium",
description: "The rate informs the quality conclusion",
}),
],
activeUnknownNodeId: complaintRateUnknown.id,
resolvedNodeIds: [],
currentSummary: "Initial summary",
});
const proposal = {
addedNodes: [],
updatedNodes: [
{
nodeId: complaintRateUnknown.id,
previousStatus: "unknown",
newStatus: "resolved",
previousValue: "2.0 complaints per 100 units",
newValue: "1.9 complaints per 100 units",
reason: "The answer provides the updated normalized complaint rate.",
},
{
nodeId: qualityDeterioration.id,
previousStatus: "supported",
newStatus: "weakened",
previousValue: null,
newValue: null,
reason: "The improved rate weakens the deterioration conclusion.",
},
],
addedEdges: [],
removedEdgeIds: [],
resolvedUnknownNodeIds: [complaintRateUnknown.id],
affectedNodeIds: [qualityDeterioration.id],
selectedQuestion: null,
};
return {
graph,
proposal,
ids: {
complaintRateUnknown: complaintRateUnknown.id,
staffingUnknown: staffingUnknown.id,
qualityDeterioration: qualityDeterioration.id,
complaintCount: complaintCount.id,
productionCount: productionCount.id,
},
};
}
const COMMERCIAL_SCENARIO =
"I have developed a new reasoning method that aims to help people determine whether they have enough justified confidence to make a decision. I believe it could become a commercial product, but I do not yet know whether it solves a genuine problem, whether people would value it enough to pay for it, or whether it is fundamentally different from existing AI tools. Before investing significant time and money into building it further, I want to determine whether continuing development is commercially justified.";
function makeCommercialUpdateFixture() {
const parent = makeNode({
id: "n-commercial-parent",
label:
"Commercial justification for whether continuing development is commercially justified",
description:
"Need to know whether this solves a genuine problem, whether people would value it enough to pay for it, and whether it is commercially justified before continuing development.",
kind: "unknown",
status: "unknown",
confidence: "medium",
});
return makeGraph({
centralStatement: COMMERCIAL_SCENARIO,
nodes: [parent],
edges: [],
activeUnknownNodeId: parent.id,
resolvedNodeIds: [],
currentSummary: "Commercial update fixture",
});
}
function makeRiskClarificationFixture() {
const riskUnknown = makeNode({
id: "n-risk-constraint",
label: "Whether avoiding more risk is a hard constraint",
description:
"Need to know whether avoiding additional risk is a hard constraint or a preference/trade-off.",
kind: "unknown",
status: "unknown",
confidence: "high",
});
const graph = makeGraph({
centralStatement:
"I want the business to grow, but I don't want to take on more risk.",
nodes: [riskUnknown],
edges: [],
activeUnknownNodeId: riskUnknown.id,
resolvedNodeIds: [],
currentSummary: "Risk clarification fixture",
});
return { graph, riskUnknownId: riskUnknown.id };
}
function makeGenericExplanationFixture() {
const explanationUnknown = makeNode({
id: "n-generic-explanation",
label:
"Explanation for why Should I relocate my engineering team from London to Manchester",
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",
});
const observationA = makeNode({
id: "n-london-team-state",
label: "Engineering team is currently operational in London.",
description: "Engineering team is currently operational in London.",
kind: "observation",
status: "supported",
confidence: "high",
});
const observationB = makeNode({
id: "n-manchester-relocation-eval",
label:
"Relocation to Manchester is actively being evaluated by the decision-maker.",
description:
"Relocation to Manchester is actively being evaluated by the decision-maker.",
kind: "observation",
status: "supported",
confidence: "high",
});
const graph = makeGraph({
centralStatement:
"Should I relocate my engineering team from London to Manchester?",
nodes: [explanationUnknown, observationA, observationB],
edges: [
makeEdge({
id: "e-london-explanation",
fromNodeId: observationA.id,
toNodeId: explanationUnknown.id,
relationship: "supports",
confidence: "medium",
description:
"The current London operating state contributes to the broad explanation unknown.",
}),
makeEdge({
id: "e-manchester-explanation",
fromNodeId: observationB.id,
toNodeId: explanationUnknown.id,
relationship: "supports",
confidence: "medium",
description:
"The Manchester relocation evaluation contributes to the broad explanation unknown.",
}),
],
activeUnknownNodeId: explanationUnknown.id,
resolvedNodeIds: [],
currentSummary: "Generic explanation fixture",
});
return { graph, explanationUnknownId: explanationUnknown.id };
}
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,
};
}
describe("applyValidatedProposal", () => {
it("applies a valid proposal successfully", () => {
const { graph, proposal, ids } = makeApplicationFixture();
const result = applyValidatedProposal({
situationGraph: graph,
proposal,
});
expect(result).toMatchObject({
success: true,
graphUpdate: proposal,
resolvedUnknownNodeIds: [ids.complaintRateUnknown],
previousActiveUnknownNodeId: ids.complaintRateUnknown,
newActiveUnknownNodeId: ids.staffingUnknown,
});
expect(
result.updatedSituationGraph.nodes.find(
(node) => node.id === ids.complaintRateUnknown,
)?.status,
).toBe("resolved");
expect(
result.updatedSituationGraph.nodes.find(
(node) => node.id === ids.qualityDeterioration,
)?.status,
).toBe("weakened");
});
it("rejects an invalid graph before application", () => {
const { graph, proposal } = makeApplicationFixture();
graph.nodes[0].dependsOn.push("missing-node");
const original = JSON.parse(JSON.stringify(graph));
const result = applyValidatedProposal({
situationGraph: graph,
proposal,
});
expect(result.success).toBe(false);
expect(result.stage).toBe("graph_validation");
expect(graph).toEqual(original);
});
it("rejects updates referencing nonexistent nodes", () => {
const { graph, proposal } = makeApplicationFixture();
const result = applyValidatedProposal({
situationGraph: graph,
proposal: {
...proposal,
updatedNodes: [
...proposal.updatedNodes,
{
nodeId: "ghost-node",
previousStatus: "unknown",
newStatus: "resolved",
previousValue: null,
newValue: null,
reason: "Invalid reference",
},
],
},
});
expect(result).toMatchObject({
success: false,
stage: "proposal_compatibility",
});
expect(result.errors).toEqual(
expect.arrayContaining([
expect.stringContaining(
'Cannot update non-existent node: "ghost-node"',
),
]),
);
});
it("rejects added edges with invalid references", () => {
const { graph, proposal } = makeApplicationFixture();
const result = applyValidatedProposal({
situationGraph: graph,
proposal: {
...proposal,
addedEdges: [
makeEdge({
id: "e-invalid",
fromNodeId: "missing-node",
toNodeId: "n-quality-deterioration",
relationship: "supports",
confidence: "medium",
description: "Invalid edge",
}),
],
},
});
expect(result.success).toBe(false);
expect(result.stage).toBe("proposal_compatibility");
});
it("accepts a proposal edge normalised from affects to canonical other before application", () => {
const { graph, proposal } = makeApplicationFixture();
const result = applyValidatedProposal({
situationGraph: graph,
proposal: {
...proposal,
addedEdges: [
makeEdge({
id: "e-other-edge",
fromNodeId: "n-complaint-count",
toNodeId: "n-quality-deterioration",
relationship: "other",
confidence: "medium",
description:
"Canonical generic relationship after proposal-boundary normalisation.",
}),
],
},
});
expect(result.success).toBe(true);
expect(
result.updatedSituationGraph.edges.some(
(edge) => edge.id === "e-other-edge" && edge.relationship === "other",
),
).toBe(true);
});
it("rejects duplicate IDs", () => {
const { graph, proposal, ids } = makeApplicationFixture();
const result = applyValidatedProposal({
situationGraph: graph,
proposal: {
...proposal,
addedNodes: [
makeNode({
id: ids.qualityDeterioration,
label: "Duplicate",
description: "Duplicate node id",
}),
],
},
});
expect(result.success).toBe(false);
expect(result.stage).toBe("proposal_compatibility");
expect(result.errors.join(" ")).toContain("duplicate node ID");
});
it("preserves unrelated nodes byte-for-byte", () => {
const { graph, proposal, ids } = makeApplicationFixture();
const originalComplaintCount = JSON.stringify(
graph.nodes.find((node) => node.id === ids.complaintCount),
);
const originalProductionCount = JSON.stringify(
graph.nodes.find((node) => node.id === ids.productionCount),
);
const result = applyValidatedProposal({
situationGraph: graph,
proposal,
});
expect(result.success).toBe(true);
expect(
JSON.stringify(
result.updatedSituationGraph.nodes.find(
(node) => node.id === ids.complaintCount,
),
),
).toBe(originalComplaintCount);
expect(
JSON.stringify(
result.updatedSituationGraph.nodes.find(
(node) => node.id === ids.productionCount,
),
),
).toBe(originalProductionCount);
});
it("adds resolved unknowns to resolvedNodeIds", () => {
const { graph, proposal, ids } = makeApplicationFixture();
const result = applyValidatedProposal({
situationGraph: graph,
proposal,
});
expect(result.success).toBe(true);
expect(result.updatedSituationGraph.resolvedNodeIds).toContain(
ids.complaintRateUnknown,
);
expect(
result.updatedSituationGraph.nodes.find(
(node) => node.id === ids.complaintRateUnknown,
)?.status,
).toBe("resolved");
});
it("rejects resolvedUnknownNodeIds that do not reference actual unknown nodes", () => {
const { graph, proposal, ids } = makeApplicationFixture();
const result = applyValidatedProposal({
situationGraph: graph,
proposal: {
...proposal,
resolvedUnknownNodeIds: [ids.qualityDeterioration],
},
});
expect(result.success).toBe(false);
expect(result.stage).toBe("proposal_compatibility");
expect(result.errors.join(" ")).toContain(
"Resolved unknown must reference an existing unknown node",
);
});
it("rejects a duplicate semantic node without resolution", () => {
const { graph, proposal } = makeApplicationFixture();
const result = applyValidatedProposal({
situationGraph: graph,
proposal: {
...proposal,
resolvedUnknownNodeIds: [],
updatedNodes: proposal.updatedNodes.filter(
(update) => update.nodeId !== "n-complaint-rate-unknown",
),
addedNodes: [
makeNode({
id: "n-parallel-rate",
label: "Complaint rate",
description: "Need the complaint rate per 100 units",
kind: "observation",
status: "supported",
confidence: "medium",
}),
],
},
});
expect(result.success).toBe(false);
expect(result.stage).toBe("proposal_compatibility");
expect(result.errors.join(" ")).toContain(
"duplicating unresolved unknown meaning",
);
});
it("keeps the active unknown when it remains unresolved", () => {
const { graph, ids } = makeApplicationFixture();
const proposal = {
addedNodes: [],
updatedNodes: [
{
nodeId: ids.qualityDeterioration,
previousStatus: "supported",
newStatus: "weakened",
previousValue: null,
newValue: null,
reason: "Only the conclusion changes",
},
],
addedEdges: [],
removedEdgeIds: [],
resolvedUnknownNodeIds: [],
affectedNodeIds: [ids.qualityDeterioration],
};
const result = applyValidatedProposal({
situationGraph: graph,
proposal,
});
expect(result.success).toBe(true);
expect(result.previousActiveUnknownNodeId).toBe(ids.complaintRateUnknown);
expect(result.newActiveUnknownNodeId).toBe(ids.complaintRateUnknown);
});
it("reports affected node ids", () => {
const { graph, proposal, ids } = makeApplicationFixture();
const result = applyValidatedProposal({
situationGraph: graph,
proposal,
});
expect(result.success).toBe(true);
expect(result.affectedNodeIds).toEqual(
expect.arrayContaining([
ids.complaintRateUnknown,
ids.qualityDeterioration,
]),
);
});
it("revalidates the completed graph references", () => {
const { graph, proposal } = makeApplicationFixture();
const result = applyValidatedProposal({
situationGraph: graph,
proposal,
});
expect(result.success).toBe(true);
expect(validateGraphReferences(result.updatedSituationGraph)).toEqual({
valid: true,
errors: [],
});
});
it("is atomic on failure", () => {
const { graph, proposal } = makeApplicationFixture();
const originalGraph = JSON.parse(JSON.stringify(graph));
const result = applyValidatedProposal({
situationGraph: graph,
proposal: {
...proposal,
addedEdges: [
makeEdge({
id: "e-bad",
fromNodeId: "missing-node",
toNodeId: "n-quality-deterioration",
relationship: "supports",
confidence: "medium",
description: "Invalid edge",
}),
],
},
});
expect(result.success).toBe(false);
expect(graph).toEqual(originalGraph);
});
it("rejects a proposal with no meaningful change", () => {
const { graph } = makeApplicationFixture();
const result = applyValidatedProposal({
situationGraph: graph,
proposal: {
addedNodes: [],
updatedNodes: [
{
nodeId: "n-quality-deterioration",
previousStatus: null,
newStatus: null,
previousValue: null,
newValue: null,
reason: "No change",
},
],
addedEdges: [],
removedEdgeIds: [],
resolvedUnknownNodeIds: [],
affectedNodeIds: [],
selectedQuestion: null,
},
});
expect(result).toMatchObject({
success: false,
stage: "proposal_compatibility",
});
expect(result.errors).toEqual(
expect.arrayContaining([expect.stringContaining("no meaningful change")]),
);
});
it("resolves one unknown and adds consequential unknowns with one selected question", () => {
const { graph, ids } = makeApplicationFixture();
const proposal = {
addedNodes: [
makeNode({
id: "n-commercial-value",
label: "Commercial value definition",
description:
"Need a concrete definition of commercial value because the decision depends on it.",
kind: "unknown",
status: "unknown",
confidence: "high",
}),
makeNode({
id: "n-demand-evidence",
label: "Evidence of demand",
description:
"Need evidence of demand because it matters to the build decision.",
kind: "unknown",
status: "unknown",
confidence: "medium",
}),
makeNode({
id: "n-build-decision",
label: "Build Confidence Engine decision",
description: "Decision situation introduced by the answer.",
kind: "state",
status: "supported",
confidence: "medium",
}),
],
updatedNodes: [
{
nodeId: ids.complaintRateUnknown,
previousStatus: "unknown",
newStatus: "resolved",
previousValue: null,
newValue: "Decision whether to build Confidence Engine",
reason: "The answer resolves the original context unknown.",
},
],
addedEdges: [
makeEdge({
id: "e-build-commercial-value",
fromNodeId: "n-build-decision",
toNodeId: "n-commercial-value",
relationship: "depends_on",
confidence: "medium",
description: "The decision depends on defining commercial value.",
}),
makeEdge({
id: "e-build-demand-evidence",
fromNodeId: "n-build-decision",
toNodeId: "n-demand-evidence",
relationship: "depends_on",
confidence: "medium",
description: "The decision depends on evidence of demand.",
}),
],
removedEdgeIds: [],
resolvedUnknownNodeIds: [ids.complaintRateUnknown],
affectedNodeIds: [],
selectedQuestion: {
nodeId: "n-commercial-value",
question: "How should commercial value be defined for this decision?",
reason:
"This is the most consequential unresolved unknown introduced by the answer.",
},
};
const result = applyValidatedProposal({ situationGraph: graph, proposal });
expect(result.success).toBe(true);
expect(result.updatedSituationGraph.resolvedNodeIds).toContain(
ids.complaintRateUnknown,
);
expect(
result.updatedSituationGraph.nodes.some(
(node) => node.id === "n-commercial-value",
),
).toBe(true);
expect(
result.updatedSituationGraph.nodes.some(
(node) => node.id === "n-demand-evidence",
),
).toBe(true);
expect(result.newActiveUnknownNodeId).toBe("n-commercial-value");
expect(result.selectedQuestion?.nodeId).toBe("n-commercial-value");
expect(result.selectedQuestion?.question).toMatch(/\?$/);
expect(result.selectedQuestion?.question.length).toBeGreaterThan(20);
expect(result.selectedQuestion?.question.toLowerCase()).not.toContain(
"price",
);
expect(result.selectedQuestion?.question.toLowerCase()).not.toContain(
"how should uncertainty regarding",
);
});
it("rejects more than 3 added unknowns", () => {
const { graph, proposal, ids } = makeApplicationFixture();
const result = applyValidatedProposal({
situationGraph: graph,
proposal: {
...proposal,
addedNodes: [1, 2, 3, 4].map((index) =>
makeNode({
id: `n-unknown-${index}`,
label: `Unknown ${index}`,
description: `Need unknown ${index} because it matters to the decision.`,
kind: "unknown",
status: "unknown",
confidence: "medium",
}),
),
addedEdges: [1, 2, 3, 4].map((index) =>
makeEdge({
id: `e-unknown-${index}`,
fromNodeId: ids.complaintRateUnknown,
toNodeId: `n-unknown-${index}`,
relationship: "depends_on",
confidence: "medium",
description: `Links unknown ${index}`,
}),
),
selectedQuestion: {
nodeId: "n-unknown-1",
question: "What is unknown 1?",
reason: "Follow-up required.",
},
},
});
expect(result.success).toBe(false);
expect(result.errors.join(" ")).toContain("too many unknown nodes");
});
it("rejects unrelated added unknowns", () => {
const { graph, proposal } = makeApplicationFixture();
const result = applyValidatedProposal({
situationGraph: graph,
proposal: {
...proposal,
addedNodes: [
makeNode({
id: "n-unrelated",
label: "Office rent",
description:
"Need office rent because it matters to a different branch.",
kind: "unknown",
status: "unknown",
confidence: "low",
}),
],
selectedQuestion: {
nodeId: "n-unrelated",
question: "What is the office rent?",
reason: "Unrelated test.",
},
},
});
expect(result.success).toBe(false);
expect(result.errors.join(" ")).toContain(
"explicitly related to an answer-derived node",
);
});
it("accepts a newly added unknown explicitly linked through answer-derived node fields", () => {
const { graph, ids } = makeApplicationFixture();
const proposal = {
addedNodes: [
makeNode({
id: "n-answer-context",
label: "Build Confidence Engine decision",
description: "Decision context introduced by the answer.",
kind: "state",
status: "supported",
confidence: "medium",
childIds: ["n-commercial-value"],
}),
makeNode({
id: "n-commercial-value",
label: "Commercial value definition",
description:
"Need commercial value definition because the decision depends on it.",
kind: "unknown",
status: "unknown",
confidence: "high",
dependsOn: ["n-answer-context"],
}),
],
updatedNodes: [
{
nodeId: ids.complaintRateUnknown,
previousStatus: "unknown",
newStatus: "resolved",
previousValue: null,
newValue: "Decision whether to build Confidence Engine",
reason: "The answer resolves the original context unknown.",
},
],
addedEdges: [],
removedEdgeIds: [],
resolvedUnknownNodeIds: [ids.complaintRateUnknown],
affectedNodeIds: [],
selectedQuestion: {
nodeId: "n-commercial-value",
question: "How should commercial value be defined for this decision?",
reason: "A consequential unknown remains unresolved.",
},
};
const result = applyValidatedProposal({ situationGraph: graph, proposal });
expect(result.success).toBe(true);
expect(result.selectedQuestion?.nodeId).toBe("n-commercial-value");
});
it("rejects a newly added unknown linked only to the original unresolved node when that node is not answer-derived", () => {
const { graph, ids } = makeApplicationFixture();
const result = applyValidatedProposal({
situationGraph: graph,
proposal: {
addedNodes: [
makeNode({
id: "n-commercial-value",
label: "Commercial value definition",
description:
"Need commercial value definition because the decision depends on it.",
kind: "unknown",
status: "unknown",
confidence: "high",
dependsOn: [ids.complaintRateUnknown],
}),
],
updatedNodes: [],
addedEdges: [
makeEdge({
id: "e-legacy-unknown-commercial-value",
fromNodeId: ids.complaintRateUnknown,
toNodeId: "n-commercial-value",
relationship: "depends_on",
confidence: "medium",
description: "Links only to the original unresolved unknown.",
}),
],
removedEdgeIds: [],
resolvedUnknownNodeIds: [],
affectedNodeIds: [],
selectedQuestion: {
nodeId: "n-commercial-value",
question: "How should commercial value be defined for this decision?",
reason: "A consequential unknown remains unresolved.",
},
},
});
expect(result.success).toBe(false);
expect(result.errors.join(" ")).toContain(
"explicitly related to an answer-derived node",
);
});
it("rejects a floating emergent unknown with no explicit relationship", () => {
const { graph } = makeApplicationFixture();
const result = applyValidatedProposal({
situationGraph: graph,
proposal: {
addedNodes: [
makeNode({
id: "n-floating",
label: "Floating unknown",
description: "Need this because it matters to the decision.",
kind: "unknown",
status: "unknown",
confidence: "medium",
}),
],
updatedNodes: [],
addedEdges: [],
removedEdgeIds: [],
resolvedUnknownNodeIds: [],
affectedNodeIds: [],
selectedQuestion: {
nodeId: "n-floating",
question: "What would resolve Floating unknown?",
reason: "Test case for floating unknown rejection.",
},
},
});
expect(result.success).toBe(false);
expect(result.errors.join(" ")).toContain(
"explicitly related to an answer-derived node",
);
});
it("accepts the reported live-shaped commercial-value proposal when the linkage is explicit in node references", () => {
const { graph, ids } = makeApplicationFixture();
const proposal = {
addedNodes: [
makeNode({
id: "answer_context_build",
label: "Build Confidence Engine decision context",
description:
"The answer introduces a concrete decision about whether to build Confidence Engine.",
kind: "state",
status: "known",
confidence: "high",
dependsOn: [ids.complaintRateUnknown, "nu_commercial_val"],
childIds: ["nu_commercial_val"],
affects: ["nu_commercial_val"],
}),
makeNode({
id: "nu_commercial_val",
label: "Commercial viability assessment of Confidence Engine",
description:
"The commercial viability of Confidence Engine remains unknown because resolving it is needed to decide whether building it is justified.",
kind: "unknown",
status: "unknown",
confidence: "medium",
dependsOn: ["answer_context_build"],
childIds: ["answer_context_build"],
}),
],
updatedNodes: [
{
nodeId: ids.complaintRateUnknown,
previousStatus: "unknown",
newStatus: "resolved",
previousValue: null,
newValue:
"Deciding whether to build the Confidence Engine due to uncertainty about its commercial value.",
reason: "The answer resolves the original context unknown.",
},
],
addedEdges: [],
removedEdgeIds: [],
resolvedUnknownNodeIds: [ids.complaintRateUnknown],
affectedNodeIds: [
ids.complaintRateUnknown,
"answer_context_build",
"nu_commercial_val",
],
selectedQuestion: {
nodeId: "nu_commercial_val",
question:
"How should commercial viability be defined for this decision?",
reason: "A foundational commercial-value unknown remains unresolved.",
},
};
const result = applyValidatedProposal({ situationGraph: graph, proposal });
expect(result.success).toBe(true);
expect(result.updatedSituationGraph.resolvedNodeIds).toContain(
ids.complaintRateUnknown,
);
expect(
result.updatedSituationGraph.nodes.some(
(node) => node.id === "nu_commercial_val",
),
).toBe(true);
});
it("rejects selected question referencing resolved node", () => {
const { graph, proposal, ids } = makeApplicationFixture();
const result = applyValidatedProposal({
situationGraph: graph,
proposal: {
...proposal,
selectedQuestion: {
nodeId: ids.complaintRateUnknown,
question: "What is the complaint rate?",
reason: "Invalid reselection.",
},
},
});
expect(result.success).toBe(false);
expect(result.errors.join(" ")).toContain(
"selectedQuestion must reference an unresolved node",
);
});
it("Regression A: rejects weak priority being strengthened into a resolved constraint judgement", () => {
const { graph, riskUnknownId } = makeRiskClarificationFixture();
const result = applyValidatedProposal({
situationGraph: graph,
answer: "Risk matters more to me.",
previousQuestion:
"Is avoiding additional risk a hard constraint or a preference/trade-off?",
proposal: {
addedNodes: [],
updatedNodes: [
{
nodeId: riskUnknownId,
previousStatus: "unknown",
newStatus: "resolved",
previousValue: null,
newValue:
"Risk avoidance is not a hard constraint; it is a stronger priority.",
reason:
"The answer implies risk matters more but is not a hard constraint.",
},
],
addedEdges: [],
removedEdgeIds: [],
resolvedUnknownNodeIds: [riskUnknownId],
affectedNodeIds: [],
selectedQuestion: null,
answerMeaning: {
userSupportedMeaning:
"Risk is of greater relative importance than growth.",
possibleInference:
"This may imply caution, but does not establish whether risk is a hard constraint.",
supportCategory: null,
resolutionGuidance: null,
},
},
});
expect(result.success).toBe(false);
expect(result.stage).toBe("proposal_compatibility");
expect(result.errors.join(" ")).toContain("must remain unresolved");
});
it("Regression A: rejects unsupported strengthening inside userSupportedMeaning itself", () => {
const { graph, riskUnknownId } = makeRiskClarificationFixture();
const result = applyValidatedProposal({
situationGraph: graph,
answer: "Risk matters more to me.",
previousQuestion:
"Is avoiding additional risk a hard constraint or a preference/trade-off?",
proposal: {
addedNodes: [],
updatedNodes: [
{
nodeId: riskUnknownId,
previousStatus: "unknown",
newStatus: "resolved",
previousValue: null,
newValue:
"Avoiding additional risk is a preference/trade-off rather than a hard constraint.",
reason:
"The answer was interpreted as ruling out a hard constraint.",
},
],
addedEdges: [],
removedEdgeIds: [],
resolvedUnknownNodeIds: [riskUnknownId],
affectedNodeIds: [],
selectedQuestion: null,
answerMeaning: {
userSupportedMeaning:
"Avoiding additional risk is a preference/trade-off rather than a hard constraint.",
possibleInference:
"The user prioritizes risk mitigation over aggressive growth strategies.",
supportCategory: null,
resolutionGuidance: null,
},
},
});
expect(result.success).toBe(false);
expect(result.stage).toBe("proposal_compatibility");
expect(result.errors.join(" ")).toContain(
"stronger reasoning category than the raw answer establishes",
);
expect(result.errors.join(" ")).toContain(
"unsupported constraint or preference/trade-off distinction",
);
});
it("Regression B: rejects conditional trade-off proposals that flatten the qualification", () => {
const { graph, riskUnknownId } = makeRiskClarificationFixture();
const result = applyValidatedProposal({
situationGraph: graph,
answer:
"I'd normally avoid more risk, but for the right opportunity I might accept some.",
previousQuestion:
"Is avoiding additional risk a hard constraint or a preference/trade-off?",
proposal: {
addedNodes: [],
updatedNodes: [
{
nodeId: riskUnknownId,
previousStatus: "unknown",
newStatus: "resolved",
previousValue: null,
newValue:
"Avoiding additional risk is a preference or trade-off rather than a hard constraint.",
reason:
"The answer shows a preference or trade-off rather than a hard constraint.",
},
],
addedEdges: [],
removedEdgeIds: [],
resolvedUnknownNodeIds: [riskUnknownId],
affectedNodeIds: [],
selectedQuestion: null,
answerMeaning: {
userSupportedMeaning:
"The user would normally avoid more risk, but for the right opportunity might accept some.",
possibleInference:
"This may support eventual clarification, but the qualifying condition remains material.",
supportCategory: null,
resolutionGuidance: null,
},
},
});
expect(result.success).toBe(false);
expect(result.stage).toBe("proposal_compatibility");
expect(result.errors.join(" ")).toContain("conditional qualification");
});
it("rejects the Experiment 56A supportCategory wording variant at the schema boundary", () => {
const { graph, riskUnknownId } = makeRiskClarificationFixture();
const result = applyValidatedProposal({
situationGraph: graph,
answer:
"I'd normally avoid more risk, but for the right opportunity I might accept some.",
previousQuestion:
"Is avoiding additional risk a hard constraint or a preference/trade-off?",
proposal: {
addedNodes: [],
updatedNodes: [
{
nodeId: riskUnknownId,
previousStatus: "unknown",
newStatus: "resolved",
previousValue: null,
newValue:
"Avoiding additional risk is a preference or trade-off rather than a hard constraint.",
reason:
"The answer shows a preference or trade-off rather than a hard constraint.",
},
],
addedEdges: [],
removedEdgeIds: [],
resolvedUnknownNodeIds: [riskUnknownId],
affectedNodeIds: [],
selectedQuestion: null,
answerMeaning: {
userSupportedMeaning:
"The user would normally avoid more risk, but for the right opportunity might accept some.",
possibleInference:
"This may support eventual clarification, but the qualifying condition remains material.",
supportCategory: "conditional_qualification",
resolutionGuidance: "may resolve once the condition is clarified",
},
},
});
expect(result.success).toBe(false);
expect(result.stage).toBe("proposal_compatibility");
expect(result.errors.join(" ")).toContain("Invalid enum value");
});
it("rejects the Experiment 56B live wording variants at the schema boundary", () => {
const { graph, riskUnknownId } = makeRiskClarificationFixture();
const result = applyValidatedProposal({
situationGraph: graph,
answer:
"I'd normally avoid more risk, but for the right opportunity I might accept some.",
previousQuestion:
"Is avoiding additional risk a hard constraint or a preference/trade-off?",
proposal: {
addedNodes: [],
updatedNodes: [
{
nodeId: riskUnknownId,
previousStatus: "unknown",
newStatus: "resolved",
previousValue: null,
newValue:
"Avoiding additional risk is a preference or trade-off rather than a hard constraint.",
reason:
"The answer shows a preference or trade-off rather than a hard constraint.",
},
],
addedEdges: [],
removedEdgeIds: [],
resolvedUnknownNodeIds: [riskUnknownId],
affectedNodeIds: [],
selectedQuestion: null,
answerMeaning: {
userSupportedMeaning:
"The user would normally avoid more risk, but for the right opportunity might accept some.",
possibleInference:
"This may support eventual clarification, but the qualifying condition remains material.",
supportCategory: "conditional_preference",
resolutionGuidance:
"Identify and quantify the threshold conditions that trigger risk acceptance.",
},
},
});
expect(result.success).toBe(false);
expect(result.stage).toBe("proposal_compatibility");
expect(result.errors.join(" ")).toContain("Invalid enum value");
});
it("B live-variant 1: invalid live wording is rejected before semantic validation", () => {
const { graph, riskUnknownId } = makeRiskClarificationFixture();
const result = applyValidatedProposal({
situationGraph: graph,
answer:
"I'd normally avoid more risk, but for the right opportunity I might accept some.",
previousQuestion:
"Is avoiding additional risk a hard constraint or a preference/trade-off?",
proposal: {
addedNodes: [],
updatedNodes: [
{
nodeId: riskUnknownId,
previousStatus: "unknown",
newStatus: "resolved",
previousValue: null,
newValue:
"Avoiding additional risk is a preference or trade-off rather than a hard constraint.",
reason:
"The answer shows a preference or trade-off rather than a hard constraint.",
},
],
addedEdges: [],
removedEdgeIds: [],
resolvedUnknownNodeIds: [riskUnknownId],
affectedNodeIds: [],
selectedQuestion: null,
answerMeaning: {
userSupportedMeaning:
"Avoiding additional risk is a strong preference rather than a hard constraint, with willingness to accept some risk if the opportunity is sufficiently compelling.",
possibleInference:
"The exact threshold for a sufficiently compelling opportunity remains undefined.",
supportCategory: "conditional_preference",
resolutionGuidance:
"Identify and quantify the threshold conditions that trigger risk acceptance.",
},
},
});
expect(result.success).toBe(false);
expect(result.stage).toBe("proposal_compatibility");
expect(result.errors.join(" ")).toContain("Invalid enum value");
expect(result.errors.join(" ")).not.toContain(
"explicitly stated hard constraint",
);
});
it("B live-variant 2: invalid free-text structured hints are rejected before semantic validation", () => {
const { graph, riskUnknownId } = makeRiskClarificationFixture();
const result = applyValidatedProposal({
situationGraph: graph,
answer:
"I'd normally avoid more risk, but for the right opportunity I might accept some.",
previousQuestion:
"Is avoiding additional risk a hard constraint or a preference/trade-off?",
proposal: {
addedNodes: [],
updatedNodes: [
{
nodeId: riskUnknownId,
previousStatus: "unknown",
newStatus: "resolved",
previousValue: null,
newValue:
"Avoiding additional risk is a preference or trade-off rather than a hard constraint.",
reason:
"The answer shows a preference or trade-off rather than a hard constraint.",
},
],
addedEdges: [],
removedEdgeIds: [],
resolvedUnknownNodeIds: [riskUnknownId],
affectedNodeIds: [],
selectedQuestion: null,
answerMeaning: {
userSupportedMeaning:
"Avoiding additional risk is a default preference, but it can be overridden for sufficiently compelling opportunities.",
possibleInference: "The exact override threshold remains undefined.",
supportCategory: "conditional_preference",
resolutionGuidance: "needs more nuance",
},
},
});
expect(result.success).toBe(false);
expect(result.stage).toBe("proposal_compatibility");
expect(result.errors.join(" ")).toContain("Invalid enum value");
});
it("Regression B: preserves conditional trade-off when userSupportedMeaning stays within the raw answer", () => {
const { graph, riskUnknownId } = makeRiskClarificationFixture();
const result = applyValidatedProposal({
situationGraph: graph,
answer:
"I'd normally avoid more risk, but for the right opportunity I might accept some.",
previousQuestion:
"Is avoiding additional risk a hard constraint or a preference/trade-off?",
proposal: {
addedNodes: [
makeNode({
id: "n-opportunity-criteria",
label: "What counts as the right opportunity",
description:
"Need to know what counts as the right opportunity because that determines when some additional risk would be acceptable.",
kind: "unknown",
status: "unknown",
confidence: "medium",
}),
],
updatedNodes: [
{
nodeId: riskUnknownId,
previousStatus: "unknown",
newStatus: "resolved",
previousValue: null,
newValue:
"The user would normally avoid more risk, but for the right opportunity might accept some.",
reason:
"The answer establishes a conditional trade-off rather than a flat hard constraint.",
},
],
addedEdges: [
makeEdge({
id: "e-risk-opportunity-criteria",
fromNodeId: riskUnknownId,
toNodeId: "n-opportunity-criteria",
relationship: "depends_on",
confidence: "medium",
description:
"The unresolved opportunity threshold matters because it determines when the trade-off changes.",
}),
],
removedEdgeIds: [],
resolvedUnknownNodeIds: [riskUnknownId],
affectedNodeIds: [],
selectedQuestion: {
nodeId: "n-opportunity-criteria",
question:
"What would count as the right opportunity for accepting some additional risk?",
reason:
"The conditional threshold remains unresolved and is the next consequential unknown.",
},
structuralActionRequired: true,
answerMeaning: {
userSupportedMeaning:
"The user would normally avoid more risk, but for the right opportunity might accept some.",
possibleInference:
"The exact threshold for the right opportunity remains undefined.",
supportCategory: "conditional_tradeoff",
resolutionGuidance: "may_resolve",
},
},
});
expect(result.success).toBe(true);
expect(result.updatedSituationGraph.resolvedNodeIds).toContain(
riskUnknownId,
);
expect(
result.updatedSituationGraph.nodes.some(
(node) => node.id === "n-opportunity-criteria",
),
).toBe(true);
});
it("Inference separation: possibleInference may remain plausible but cannot justify graph mutation when userSupportedMeaning overstates the raw answer", () => {
const { graph, riskUnknownId } = makeRiskClarificationFixture();
const result = applyValidatedProposal({
situationGraph: graph,
answer: "Risk matters more to me.",
previousQuestion:
"Is avoiding additional risk a hard constraint or a preference/trade-off?",
proposal: {
addedNodes: [],
updatedNodes: [
{
nodeId: riskUnknownId,
previousStatus: "unknown",
newStatus: "resolved",
previousValue: null,
newValue:
"Avoiding additional risk is a preference rather than a hard constraint.",
reason:
"The interpretation was treated as sufficient to resolve the distinction.",
},
],
addedEdges: [],
removedEdgeIds: [],
resolvedUnknownNodeIds: [riskUnknownId],
affectedNodeIds: [],
selectedQuestion: null,
answerMeaning: {
userSupportedMeaning:
"Avoiding additional risk is a preference rather than a hard constraint.",
possibleInference:
"The user may be signaling caution and a willingness to trade off growth for lower risk.",
supportCategory: null,
resolutionGuidance: null,
},
},
});
expect(result.success).toBe(false);
expect(result.stage).toBe("proposal_compatibility");
expect(result.errors.join(" ")).toContain(
"unsupported constraint or preference/trade-off distinction",
);
});
it("57F legitimate-answer regression: a grounded unclassified answer may resolve its intended unknown", () => {
const graph = makeCommercialUpdateFixture();
const parentId = graph.activeUnknownNodeId;
const result = applyValidatedProposal({
situationGraph: graph,
answer:
"We're looking at this mainly for cost reduction — roughly £2M annual savings on office overhead.",
previousQuestion:
"What problem would this need to solve to justify continuing development?",
proposal: {
addedNodes: [
makeNode({
id: "n-other-people-problem",
label: "Whether other people experience this problem",
description:
"Need to know whether other people experience this problem, because that must be established before deciding whether the problem is broadly important.",
kind: "unknown",
status: "unknown",
confidence: "medium",
parentId,
}),
],
updatedNodes: [
{
nodeId: parentId,
previousStatus: "unknown",
newStatus: "resolved",
previousValue: null,
newValue:
"A concrete problem exists: reducing office overhead by roughly £2M annually is the main outcome being sought.",
reason:
"The answer directly states the practical decision-driving outcome the work is intended to achieve.",
},
],
addedEdges: [
makeEdge({
id: "e-commercial-parent-other-people-problem",
fromNodeId: parentId,
toNodeId: "n-other-people-problem",
relationship: "depends_on",
confidence: "medium",
description:
"After establishing the concrete problem in the current context, the next unknown is whether it also exists for other people.",
}),
],
removedEdgeIds: [],
resolvedUnknownNodeIds: [parentId],
affectedNodeIds: [],
selectedQuestion: {
nodeId: "n-other-people-problem",
question:
"What makes you think other people experience this problem too?",
reason:
"The answer establishes the problem in this case; the next consequential unknown is whether it generalises beyond this case.",
},
structuralActionRequired: true,
answerMeaning: {
userSupportedMeaning:
"The main reason for considering this is cost reduction, specifically about £2M in annual office-overhead savings.",
possibleInference:
"If those savings are real and recurring, that could make the problem commercially important.",
supportCategory: "other",
resolutionGuidance: null,
},
},
});
expect(result.success).toBe(true);
expect(result.updatedSituationGraph.resolvedNodeIds).toContain(parentId);
expect(result.stage).toBeUndefined();
});
it("57F deterministic reproduction: pre-fix grounded unclassified answer would have been rejected by proposal-text constraint language", () => {
const graph = makeCommercialUpdateFixture();
const parentId = graph.activeUnknownNodeId;
const result = applyValidatedProposal({
situationGraph: graph,
answer:
"We're looking at this mainly for cost reduction — roughly £2M annual savings on office overhead.",
previousQuestion:
"What problem would this need to solve to justify continuing development?",
proposal: {
addedNodes: [],
updatedNodes: [
{
nodeId: parentId,
previousStatus: "unknown",
newStatus: "resolved",
previousValue: null,
newValue:
"Cost reduction, including approximately £2M annual office-overhead savings, is a stated reason for considering the relocation.",
reason:
"The answer establishes that cost reduction is the relevant preference/trade-off consideration for this decision.",
},
],
addedEdges: [],
removedEdgeIds: [],
resolvedUnknownNodeIds: [parentId],
affectedNodeIds: [],
selectedQuestion: null,
structuralActionRequired: true,
answerMeaning: {
userSupportedMeaning:
"Cost reduction, including approximately £2M annual office-overhead savings, is a stated reason for considering the relocation.",
possibleInference: null,
supportCategory: "other",
resolutionGuidance: null,
},
},
});
expect(result.success).toBe(true);
expect(result.updatedSituationGraph.resolvedNodeIds).toContain(parentId);
});
it("unsafe unclassified answer: other is not automatically trusted when the proposal adds stronger unsupported meaning", () => {
const graph = makeCommercialUpdateFixture();
const parentId = graph.activeUnknownNodeId;
const result = applyValidatedProposal({
situationGraph: graph,
answer:
"We're looking at this mainly for cost reduction — roughly £2M annual savings on office overhead.",
previousQuestion:
"What problem would this need to solve to justify continuing development?",
proposal: {
addedNodes: [],
updatedNodes: [
{
nodeId: parentId,
previousStatus: "unknown",
newStatus: "resolved",
previousValue: null,
newValue:
"There is proven broad market demand for a product that delivers these savings.",
reason:
"The answer was treated as establishing commercial demand rather than only the user's own cost-reduction goal.",
},
],
addedEdges: [],
removedEdgeIds: [],
resolvedUnknownNodeIds: [parentId],
affectedNodeIds: [],
selectedQuestion: null,
answerMeaning: {
userSupportedMeaning:
"There is proven broad market demand for a product that delivers these savings.",
possibleInference:
"The savings target could imply broader applicability if others share similar overhead pressures.",
supportCategory: null,
resolutionGuidance: null,
},
},
});
expect(result.success).toBe(false);
expect(result.stage).toBe("proposal_compatibility");
expect(result.errors.join(" ")).toContain(
"unsupported meaning beyond what the raw answer itself states",
);
});
it("Regression C: rejects unresolved uncertainty being treated as resolved", () => {
const { graph, riskUnknownId } = makeRiskClarificationFixture();
const result = applyValidatedProposal({
situationGraph: graph,
answer: "I'm not really sure.",
previousQuestion:
"Is avoiding additional risk a hard constraint or a preference/trade-off?",
proposal: {
addedNodes: [],
updatedNodes: [
{
nodeId: riskUnknownId,
previousStatus: "unknown",
newStatus: "resolved",
previousValue: null,
newValue:
"Risk avoidance is probably a preference rather than a hard constraint.",
reason:
"The answer suggests uncertainty but leans toward preference.",
},
],
addedEdges: [],
removedEdgeIds: [],
resolvedUnknownNodeIds: [riskUnknownId],
affectedNodeIds: [],
selectedQuestion: null,
answerMeaning: {
userSupportedMeaning:
"The user is not sure whether avoiding additional risk is a hard constraint or a preference/trade-off.",
possibleInference: null,
supportCategory: null,
resolutionGuidance: null,
},
},
});
expect(result.success).toBe(false);
expect(result.stage).toBe("proposal_compatibility");
expect(result.errors.join(" ")).toContain("must remain unresolved");
});
it("Regression D: rejects weakening an explicit hard constraint", () => {
const { graph, riskUnknownId } = makeRiskClarificationFixture();
const result = applyValidatedProposal({
situationGraph: graph,
answer: "It's a hard constraint. I don't want any increase in risk.",
previousQuestion:
"Is avoiding additional risk a hard constraint or a preference/trade-off?",
proposal: {
addedNodes: [],
updatedNodes: [
{
nodeId: riskUnknownId,
previousStatus: "unknown",
newStatus: "resolved",
previousValue: null,
newValue:
"Avoiding additional risk is a preference or trade-off rather than a hard constraint.",
reason:
"The answer was interpreted as a strong preference rather than a hard constraint.",
},
],
addedEdges: [],
removedEdgeIds: [],
resolvedUnknownNodeIds: [riskUnknownId],
affectedNodeIds: [],
selectedQuestion: null,
answerMeaning: {
userSupportedMeaning:
"Avoiding additional risk is a hard constraint and the user does not want any increase in risk.",
possibleInference: null,
supportCategory: null,
resolutionGuidance: null,
},
},
});
expect(result.success).toBe(false);
expect(result.stage).toBe("proposal_compatibility");
expect(result.errors.join(" ")).toContain(
"weakens an explicitly stated hard constraint",
);
});
it("accepts explicit hard constraint when proposal preserves it", () => {
const { graph, riskUnknownId } = makeRiskClarificationFixture();
const result = applyValidatedProposal({
situationGraph: graph,
answer: "It's a hard constraint. I don't want any increase in risk.",
previousQuestion:
"Is avoiding additional risk a hard constraint or a preference/trade-off?",
proposal: {
addedNodes: [],
updatedNodes: [
{
nodeId: riskUnknownId,
previousStatus: "unknown",
newStatus: "resolved",
previousValue: null,
newValue:
"Avoiding additional risk is a hard constraint. The user does not want any increase in risk.",
reason:
"The answer explicitly states a hard constraint with no allowed increase in risk.",
},
],
addedEdges: [],
removedEdgeIds: [],
resolvedUnknownNodeIds: [riskUnknownId],
affectedNodeIds: [],
selectedQuestion: null,
structuralActionRequired: true,
answerMeaning: {
userSupportedMeaning:
"Avoiding additional risk is a hard constraint and the user does not want any increase in risk.",
possibleInference: null,
supportCategory: "explicit_hard_constraint",
resolutionGuidance: "must_resolve",
},
},
});
expect(result.success).toBe(true);
expect(result.updatedSituationGraph.resolvedNodeIds).toContain(
riskUnknownId,
);
});
it("negation safety: mentioning hard constraint in a negated comparison does not become affirmative hard constraint", () => {
const { graph, riskUnknownId } = makeRiskClarificationFixture();
const result = applyValidatedProposal({
situationGraph: graph,
answer:
"I'd normally avoid more risk, but for the right opportunity I might accept some.",
previousQuestion:
"Is avoiding additional risk a hard constraint or a preference/trade-off?",
proposal: {
addedNodes: [],
updatedNodes: [
{
nodeId: riskUnknownId,
previousStatus: "unknown",
newStatus: "resolved",
previousValue: null,
newValue:
"Avoiding additional risk is a preference or trade-off rather than a hard constraint.",
reason:
"The answer shows a preference or trade-off rather than a hard constraint.",
},
],
addedEdges: [],
removedEdgeIds: [],
resolvedUnknownNodeIds: [riskUnknownId],
affectedNodeIds: [],
selectedQuestion: null,
answerMeaning: {
userSupportedMeaning:
"Avoiding additional risk is a strong preference rather than a hard constraint.",
possibleInference:
"This indicates flexibility rather than an absolute prohibition.",
supportCategory: "conditional_preference",
resolutionGuidance: "needs more nuance",
},
},
});
expect(result.success).toBe(false);
expect(result.stage).toBe("proposal_compatibility");
expect(result.errors.join(" ")).not.toContain(
"weakens an explicitly stated hard constraint",
);
});
it("fails at the schema boundary when answerMeaning uses unsupported structured values", () => {
const { graph, riskUnknownId } = makeRiskClarificationFixture();
const result = applyValidatedProposal({
situationGraph: graph,
answer: "Risk and growth are both important.",
previousQuestion:
"Is avoiding additional risk a hard constraint or a preference/trade-off?",
proposal: {
addedNodes: [],
updatedNodes: [
{
nodeId: riskUnknownId,
previousStatus: "unknown",
newStatus: "resolved",
previousValue: null,
newValue:
"Avoiding additional risk is a preference rather than a hard constraint.",
reason:
"The answer was interpreted as a balanced trade-off with no hard constraint.",
},
],
addedEdges: [],
removedEdgeIds: [],
resolvedUnknownNodeIds: [riskUnknownId],
affectedNodeIds: [],
selectedQuestion: null,
answerMeaning: {
userSupportedMeaning: "Risk and growth are both important.",
possibleInference:
"The user may be describing a broad balance, but no constraint boundary is established.",
supportCategory: "constraint_preference_mix",
resolutionGuidance: "needs more nuance",
},
},
});
expect(result.success).toBe(false);
expect(result.stage).toBe("proposal_compatibility");
expect(result.errors.join(" ")).toContain("Invalid enum value");
});
it("active unknown matches selected question node", () => {
const { graph, ids } = makeApplicationFixture();
const proposal = {
addedNodes: [
makeNode({
id: "n-success-threshold",
label: "Success threshold",
description:
"Need a success threshold because the decision depends on it.",
kind: "unknown",
status: "unknown",
confidence: "high",
}),
makeNode({
id: "n-build-decision",
label: "Build Confidence Engine decision",
description: "Decision introduced by the answer.",
kind: "state",
status: "supported",
confidence: "medium",
}),
],
updatedNodes: [
{
nodeId: ids.complaintRateUnknown,
previousStatus: "unknown",
newStatus: "resolved",
previousValue: null,
newValue: "Decision whether to build Confidence Engine",
reason: "The answer resolves the original unknown.",
},
],
addedEdges: [
makeEdge({
id: "e-build-success-threshold",
fromNodeId: "n-build-decision",
toNodeId: "n-success-threshold",
relationship: "depends_on",
confidence: "medium",
description: "The decision depends on a success threshold.",
}),
],
removedEdgeIds: [],
resolvedUnknownNodeIds: [ids.complaintRateUnknown],
affectedNodeIds: [],
selectedQuestion: {
nodeId: "n-success-threshold",
question: "What success threshold would justify building it?",
reason: "One consequential unknown remains.",
},
};
const result = applyValidatedProposal({ situationGraph: graph, proposal });
expect(result.success).toBe(true);
expect(result.newActiveUnknownNodeId).toBe(result.selectedQuestion?.nodeId);
});
it("replaces downstream pricing question with higher-value commercial-value question", () => {
const { graph, ids } = makeApplicationFixture();
const proposal = {
addedNodes: [
makeNode({
id: "n-commercial-value",
label: "Commercial value definition",
description:
"Need commercial value definition because the decision depends on it.",
kind: "unknown",
status: "unknown",
confidence: "high",
}),
makeNode({
id: "n-pricing",
label: "Target price point",
description:
"Need a price point because revenue assumptions depend on it.",
kind: "unknown",
status: "unknown",
confidence: "medium",
dependsOn: ["n-commercial-value"],
}),
makeNode({
id: "n-build-decision",
label: "Build Confidence Engine decision",
description: "Decision introduced by the answer.",
kind: "state",
status: "supported",
confidence: "medium",
}),
],
updatedNodes: [
{
nodeId: ids.complaintRateUnknown,
previousStatus: "unknown",
newStatus: "resolved",
previousValue: null,
newValue: "Decision whether to build Confidence Engine",
reason: "The answer resolves the original context unknown.",
},
],
addedEdges: [
makeEdge({
id: "e-build-commercial-value",
fromNodeId: "n-build-decision",
toNodeId: "n-commercial-value",
relationship: "depends_on",
confidence: "medium",
description: "The decision depends on defining commercial value.",
}),
makeEdge({
id: "e-commercial-value-pricing",
fromNodeId: "n-commercial-value",
toNodeId: "n-pricing",
relationship: "depends_on",
confidence: "medium",
description: "Pricing depends on commercial value definition.",
}),
makeEdge({
id: "e-build-pricing",
fromNodeId: "n-build-decision",
toNodeId: "n-pricing",
relationship: "depends_on",
confidence: "low",
description: "The decision also references pricing assumptions.",
}),
],
removedEdgeIds: [],
resolvedUnknownNodeIds: [ids.complaintRateUnknown],
affectedNodeIds: [],
selectedQuestion: {
nodeId: "n-pricing",
question: "What is the target price point?",
reason: "Model chose a downstream leaf.",
},
};
const result = applyValidatedProposal({ situationGraph: graph, proposal });
expect(result.success).toBe(true);
expect(result.selectedQuestion?.nodeId).toBe("n-commercial-value");
expect(result.selectedQuestion?.question.toLowerCase()).not.toContain(
"price",
);
});
it("resolves the existing comparability unknown and advances reasoning after the answer", () => {
const { graph, proposal, comparabilityUnknownId } =
makeComparabilityUpdateFixture();
const originalUnrelatedNode = JSON.stringify(
graph.nodes.find((node) => node.id === "n-unrelated"),
);
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.resolvedUnknownNodeIds).toContain(comparabilityUnknownId);
expect(result.resolvedReasoningNodeIds).toEqual([
"reasoning:comparability",
]);
expect(result.emergentReasoningNodeCreated).toBe(true);
expect(result.emergentReasoningNodeId).toBeTruthy();
expect(result.emergentReasoningNodeReason).toContain("backed by the graph");
expect(result.previousReasoningState.comparabilityStatus).toBe("uncertain");
expect(result.reasoningState).toMatchObject({
comparabilityStatus: "confirmed",
relationshipStatus: "potentially_related",
relationshipAssessed: true,
});
expect(result.reasoningState.comparabilityEvidence).toEqual([
comparabilityUnknownId,
]);
expect(result.selectedQuestion?.nodeId).toBe(result.newActiveUnknownNodeId);
expect(result.selectedQuestion).toMatchObject({
nodeId: result.newActiveUnknownNodeId,
question: "how the two observations were measured?",
});
expect(result.selectedQuestion?.question.toLowerCase()).not.toMatch(
/dso|debtor days|receivables turnover|working capital|receivables/,
);
expect(result.reasoningState.reasoningStages).toEqual([
{
stage: "comparability",
status: "confirmed",
outcome:
"Comparability was confirmed by the user answer covering the same period and source basis.",
},
{
stage: "relationship",
status: "potentially_related",
outcome:
"The observations concern connected business signals but do not establish a direct contradiction or cause.",
},
]);
const emergentNode = result.updatedSituationGraph.nodes.find(
(node) => node.id === result.emergentReasoningNodeId,
);
expect(emergentNode).toMatchObject({
kind: "unknown",
status: "unknown",
confidence: "medium",
});
expect(emergentNode.description.toLowerCase()).toContain("because");
expect(
result.updatedSituationGraph.edges.filter(
(edge) => edge.toNodeId === result.emergentReasoningNodeId,
),
).not.toEqual([]);
expect(
result.updatedSituationGraph.edges.some(
(edge) =>
edge.toNodeId === result.emergentReasoningNodeId &&
edge.relationship === "causes",
),
).toBe(false);
expect(
JSON.stringify(
result.updatedSituationGraph.nodes.find(
(node) => node.id === "n-unrelated",
),
),
).toBe(originalUnrelatedNode);
});
it("reuses an equivalent existing unresolved reasoning unknown instead of creating a duplicate", () => {
const { graph, proposal } = makeComparabilityUpdateFixture();
graph.nodes.push(
makeNode({
id: "n-existing-explanation",
label:
"Explanation for why Revenue increased by 18%, but cash in the bank fell over the same period",
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",
}),
);
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.emergentReasoningNodeCreated).toBe(false);
expect(result.emergentReasoningNodeId).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(
"how the two observations were measured?",
);
expect(
result.updatedSituationGraph.nodes.filter(
(node) => node.label === graph.nodes.at(-1).label,
),
).toHaveLength(1);
});
it("does not decompose a generic explanation unknown into unsupported comparison children", () => {
const { graph, explanationUnknownId } = makeGenericExplanationFixture();
const result = applyValidatedProposal({
situationGraph: graph,
proposal: makeMeaningfulNoOpProposal(),
});
expect(result.success).toBe(true);
expect(result.atomicityAssessment).toBe("composite");
expect(result.decompositionPerformed).toBe(false);
expect(result.childUnknownCount).toBe(0);
expect(result.childNodeIds).toHaveLength(0);
expect(
result.updatedSituationGraph.nodes.some(
(node) => node.label === "How the two observations were measured",
),
).toBe(false);
expect(
result.updatedSituationGraph.nodes.some(
(node) =>
node.label ===
"Whether the two observations reflect different timing",
),
).toBe(false);
expect(result.selectedQuestion?.question || "").not.toContain(
"two observations",
);
expect(result.decompositionStoppedReason).toBe(
"Decomposition stopped because no meaning-preserving child family was justified for this parent.",
);
expect(result.selectedUnknownBefore).toBe(explanationUnknownId);
});
it("reselects a remaining commercial sibling after resolving the first child", () => {
const graph = makeCommercialUpdateFixture();
const firstResult = applyValidatedProposal({
situationGraph: graph,
proposal: makeMeaningfulNoOpProposal(),
});
expect(firstResult.success).toBe(true);
expect(firstResult.selectedQuestion?.question).toBe(
"Who experiences this problem?",
);
const secondResult = applyValidatedProposal({
situationGraph: firstResult.updatedSituationGraph,
proposal: {
addedNodes: [],
updatedNodes: [
{
nodeId: firstResult.selectedQuestion.nodeId,
previousStatus: "unknown",
newStatus: "resolved",
previousValue: null,
newValue:
"I experience it myself when I am trying to decide whether a project, idea or investment is justified, but I do not yet know how common that problem is for other people.",
reason: "The answer confirms a self-observed instance.",
},
],
addedEdges: [],
removedEdgeIds: [],
resolvedUnknownNodeIds: [firstResult.selectedQuestion.nodeId],
affectedNodeIds: [],
selectedQuestion: null,
},
previousQuestion: firstResult.selectedQuestion.question,
answer:
"I experience it myself when I am trying to decide whether a project, idea or investment is justified, but I do not yet know how common that problem is for other people.",
});
expect(secondResult.success).toBe(true);
expect(secondResult.resolvedUnknownNodeIds).toContain(
firstResult.selectedQuestion.nodeId,
);
expect(secondResult.newActiveUnknownNodeId).toBe(
secondResult.selectedQuestion?.nodeId,
);
expect(secondResult.selectedQuestion?.question).toBe(
"What makes you think other people experience this problem too?",
);
expect(secondResult.selectedQuestion?.reasoningPattern).toBe("decision");
expect(secondResult.selectedQuestion?.questionFamily).toBe(
"decision_foundation",
);
expect(secondResult.selectedQuestion?.nodeId).not.toBe(
firstResult.selectedQuestion.nodeId,
);
expect(secondResult.unresolvedCandidateCount).toBeGreaterThan(0);
expect(secondResult.eligibleCandidateCount).toBeGreaterThan(0);
expect(secondResult.candidateNodeIds).toContain(
secondResult.selectedQuestion?.nodeId,
);
expect(secondResult.resolvedCurrentTurnNodeIds).toContain(
firstResult.selectedQuestion.nodeId,
);
expect(secondResult.noQuestionReason).toBeNull();
expect(secondResult.selectedQuestion?.question.toLowerCase()).not.toMatch(
/price|budget|market size|pilot metrics|benchmark|technical differentiation/,
);
expect(secondResult.reasoningPatternValidation).toMatchObject({
activePattern: "decision",
valid: true,
});
expect(secondResult.graphReasoningIntegrity).toBe("valid");
expect(secondResult.incompatibleNodeIds).toEqual([]);
expect(secondResult.compatibilityFailures).toEqual([]);
});
it("reselects a non-repeated comparison sibling instead of asking the same evidence question again", () => {
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);
expect(firstResult.selectedQuestion?.question).toBe(
"how the two observations were measured?",
);
const secondResult = applyValidatedProposal({
situationGraph: firstResult.updatedSituationGraph,
proposal: {
addedNodes: [],
updatedNodes: [
{
nodeId: firstResult.selectedQuestion.nodeId,
previousStatus: "unknown",
newStatus: "resolved",
previousValue: null,
newValue:
"Both measures come from the same monthly reporting pack and use the same source system.",
reason: "The answer resolves the measurement clarification child.",
},
],
addedEdges: [],
removedEdgeIds: [],
resolvedUnknownNodeIds: [firstResult.selectedQuestion.nodeId],
affectedNodeIds: [],
selectedQuestion: null,
},
previousQuestion: firstResult.selectedQuestion.question,
answer:
"Both measures come from the same monthly reporting pack and use the same source system.",
});
expect(secondResult.success).toBe(true);
expect(secondResult.selectedQuestion?.nodeId).not.toBe(
firstResult.selectedQuestion.nodeId,
);
expect(secondResult.selectedQuestion?.question).not.toBe(
firstResult.selectedQuestion.question,
);
expect(secondResult.finalQuestion).not.toBe(
firstResult.selectedQuestion.question,
);
expect(secondResult.noQuestionReason).toBeNull();
expect(secondResult.selectedQuestion?.nodeId).toBe(
secondResult.newActiveUnknownNodeId,
);
});
it("rejects a compound selected question before returning it", () => {
const graph = makeCommercialUpdateFixture();
const result = applyValidatedProposal({
situationGraph: graph,
proposal: {
...makeMeaningfulNoOpProposal(),
selectedQuestion: {
nodeId: "n-commercial-parent",
question:
"What changed during the period that could explain why work is taking longer, and how were the two observations measured?",
reason: "Invalid compound follow-up.",
},
},
});
expect(result.success).toBe(false);
expect(result.stage).toBe("proposal_compatibility");
expect(result.errors).toContain(
"selectedQuestion must be a single non-compound question",
);
});
it("allows a legitimate single-concept or-question", () => {
const graph = makeCommercialUpdateFixture();
const result = applyValidatedProposal({
situationGraph: graph,
proposal: {
...makeMeaningfulNoOpProposal(),
selectedQuestion: {
nodeId: "n-commercial-parent",
question: "Is the problem caused by timing or measurement basis?",
reason: "Single concept contrast.",
},
},
});
expect(result.success).toBe(true);
});
it("returns no question when the graph is truly complete after resolution", () => {
const graph = makeGraph({
centralStatement: "A single missing fact needs confirmation.",
nodes: [
makeNode({
id: "n-only-unknown",
label: "Missing fact",
description:
"Need the missing fact because the conclusion depends on it.",
kind: "unknown",
status: "unknown",
confidence: "high",
}),
],
edges: [],
activeUnknownNodeId: "n-only-unknown",
resolvedNodeIds: [],
currentSummary: "Single unknown fixture",
});
const result = applyValidatedProposal({
situationGraph: graph,
proposal: {
addedNodes: [],
updatedNodes: [
{
nodeId: "n-only-unknown",
previousStatus: "unknown",
newStatus: "resolved",
previousValue: null,
newValue: "Confirmed.",
reason: "The answer resolves the only unknown.",
},
],
addedEdges: [],
removedEdgeIds: [],
resolvedUnknownNodeIds: ["n-only-unknown"],
affectedNodeIds: [],
selectedQuestion: null,
},
previousQuestion: "What is the missing fact?",
answer: "Confirmed.",
});
expect(result.success).toBe(true);
expect(result.selectedQuestion).toBeNull();
expect(result.finalQuestion).toBeNull();
expect(result.newActiveUnknownNodeId).toBeNull();
expect(result.noQuestionReason).toBe(
"No unresolved unknown candidates remain after this update.",
);
});
it("does not allow a decision-mode active unknown to remain a comparison child", () => {
const graph = makeCommercialUpdateFixture();
graph.nodes.push(
makeNode({
id: "n-commercial-comparison-child",
label: "How the two observations were measured",
description:
"Need evidence about the measure used for each observation before comparing them.",
kind: "unknown",
status: "unknown",
confidence: "medium",
parentId: "n-commercial-parent",
}),
);
graph.activeUnknownNodeId = "n-commercial-comparison-child";
const result = applyValidatedProposal({
situationGraph: graph,
proposal: makeMeaningfulNoOpProposal(),
});
expect(result.success).toBe(true);
expect(result.reasoningPatternValidation).toMatchObject({
activePattern: "decision",
valid: true,
});
expect(result.graphReasoningIntegrity).toBe("valid");
expect(result.incompatibleNodeIds).toContain(
"n-commercial-comparison-child",
);
expect(result.compatibilityFailures).toEqual(
expect.arrayContaining([
expect.objectContaining({
nodeId: "n-commercial-comparison-child",
activePattern: "decision",
nodePattern: "comparison",
}),
]),
);
expect(result.replacementActions).toEqual(
expect.arrayContaining([
expect.objectContaining({
rejectedNodeId: "n-commercial-comparison-child",
replacementNodeId: result.selectedQuestion?.nodeId,
}),
]),
);
expect(result.selectedQuestion?.nodeId).not.toBe(
"n-commercial-comparison-child",
);
expect(result.selectedQuestion?.reasoningPattern).toBe("decision");
});
// ── Case 1 — direct user-supported uncertainty, no structural anchor ──
it("Case 1: admitted unknown passes via node-level user support without structural linkage", () => {
const graph = makeCommercialUpdateFixture();
const parentId = graph.activeUnknownNodeId;
const result = applyValidatedProposal({
situationGraph: graph,
answer: "I need evidence that projected office savings are realistic.",
previousQuestion:
"What problem would this need to solve to justify continuing development?",
proposal: {
addedNodes: [
makeNode({
id: "n-savings-realism",
label: "Whether projected office savings are realistic",
description:
"Need to verify whether the projected office savings are realistic, because that determines commercial justification.",
kind: "unknown",
status: "unknown",
confidence: "high",
}),
],
updatedNodes: [
{
nodeId: parentId,
previousStatus: "unknown",
newStatus: "resolved",
previousValue: null,
newValue:
"The user needs evidence for projected office savings realism.",
reason: "The answer directs focus to savings realism evidence.",
},
],
addedEdges: [],
removedEdgeIds: [],
resolvedUnknownNodeIds: [parentId],
affectedNodeIds: [],
selectedQuestion: {
nodeId: "n-savings-realism",
question: "What evidence supports the projected office savings?",
reason: "Consequential unknown verified by user-supported meaning.",
},
answerMeaning: {
userSupportedMeaning:
"I need evidence that projected office savings are realistic.",
possibleInference: null,
supportCategory: null,
resolutionGuidance: null,
},
structuralActionRequired: true,
},
});
expect(result.success).toBe(true);
});
// ── Case 2 — two direct user-supported unknowns ──
it("Case 2: two independently supported unknowns both pass via node-level user support (parent stays unresolved)", () => {
const graph = makeCommercialUpdateFixture();
const parentId = graph.activeUnknownNodeId;
// The parent remains unresolved — the answer adds two new unknowns
// without resolving the original question. Both children are independently verified.
const result = applyValidatedProposal({
situationGraph: graph,
answer:
"I need evidence the savings are realistic and evidence the move will not materially increase loss of key engineers.",
previousQuestion:
"What problem would this need to solve to justify continuing development?",
proposal: {
addedNodes: [
makeNode({
id: "n-savings-realism-2",
label: "Whether projected savings are realistic",
description:
"Need to verify whether the savings figures are realistic, because that is needed for commercial evaluation.",
kind: "unknown",
status: "unknown",
confidence: "high",
}),
makeNode({
id: "n-retention-impact",
label:
"Whether the move materially increases loss of key engineers",
description:
"Need to check whether the relocation increases risk of losing key engineers, because that matters for continuity.",
kind: "unknown",
status: "unknown",
confidence: "high",
}),
],
updatedNodes: [],
addedEdges: [
makeEdge({
id: "e-parent-to-savings",
fromNodeId: parentId,
toNodeId: "n-savings-realism-2",
relationship: "depends_on",
confidence: "medium",
description: "Parent depends on savings verification.",
}),
],
removedEdgeIds: [],
resolvedUnknownNodeIds: [],
affectedNodeIds: [],
selectedQuestion: {
nodeId: "n-savings-realism-2",
question: "What evidence supports the projected office savings?",
reason: "First of two independently verified consequential unknowns.",
},
answerMeaning: {
userSupportedMeaning:
"I need evidence the savings are realistic and evidence the move will not materially increase loss of key engineers.",
possibleInference: null,
supportCategory: null,
resolutionGuidance: null,
},
structuralActionRequired: true,
},
});
expect(result.success).toBe(true);
});
// ── Case 3 — unsupported hallucinated unknown ──
it("Case 3: unsupported hallucinated unknown is rejected without structural linkage", () => {
const graph = makeCommercialUpdateFixture();
const parentId = graph.activeUnknownNodeId;
const result = applyValidatedProposal({
situationGraph: graph,
answer:
"We are looking at this mainly for cost reduction — roughly £2M annual savings on office overhead.",
previousQuestion:
"What problem would this need to solve to justify continuing development?",
proposal: {
addedNodes: [
makeNode({
id: "n-office-paint-colour",
label: "Whether office paint colour affects morale",
description:
"Need to know whether paint colour in the current office affects team morale, because that is a potential factor.",
kind: "unknown",
status: "unknown",
confidence: "low",
}),
],
updatedNodes: [
{
nodeId: parentId,
previousStatus: "unknown",
newStatus: "resolved",
previousValue: null,
newValue: "Cost reduction is the primary driver.",
reason: "The answer identifies cost reduction as the main concern.",
},
],
addedEdges: [],
removedEdgeIds: [],
resolvedUnknownNodeIds: [parentId],
affectedNodeIds: [],
selectedQuestion: {
nodeId: "n-office-paint-colour",
question:
"What evidence supports the office paint colour hypothesis?",
reason: "Unrelated unknown — should be rejected.",
},
answerMeaning: {
userSupportedMeaning:
"The main reason for considering this is cost reduction, specifically about £2M in annual office-overhead savings.",
possibleInference: null,
supportCategory: "other",
resolutionGuidance: null,
},
},
});
expect(result.success).toBe(false);
expect(result.stage).toBe("proposal_compatibility");
expect(result.errors.join(" ")).toContain(
"explicitly related to an answer-derived node",
);
});
// ── Case 4 — possibleInference only ──
it("Case 4: unknown based on possibleInference only is not admitted through user-support path", () => {
const graph = makeCommercialUpdateFixture();
// AnswerMeaning's userSupportedMeaning shares NO tokens with the unknown.
// possibleInference does mention "workforce" but that must NOT establish support.
const result = applyValidatedProposal({
situationGraph: graph,
answer: "The project timeline is critical.",
previousQuestion: "What problem would this need to solve?",
proposal: {
addedNodes: [
makeNode({
id: "n-workforce-stability",
label: "Whether workforce stability affects the decision",
description:
"Need to check whether workforce stability matters, because that could impact scheduling.",
kind: "unknown",
status: "unknown",
confidence: "medium",
}),
],
updatedNodes: [],
addedEdges: [],
removedEdgeIds: [],
resolvedUnknownNodeIds: [],
affectedNodeIds: [],
selectedQuestion: {
nodeId: "n-workforce-stability",
question: "How important is workforce stability to this decision?",
reason: "Inferred consequential unknown — should be rejected.",
},
answerMeaning: {
userSupportedMeaning:
"The project timeline is critical to the decision.",
possibleInference:
"Workforce stability may matter for scheduling and continuity.",
supportCategory: null,
resolutionGuidance: null,
},
},
});
expect(result.success).toBe(false);
expect(result.stage).toBe("proposal_compatibility");
expect(result.errors.join(" ")).toContain(
"explicitly related to an answer-derived node",
);
});
// ── Case 5 — genuine structural edge ──
it("Case 5: supported unknown with genuine structural edge passes without additional provenance edge", () => {
const graph = makeCommercialUpdateFixture();
const parentId = graph.activeUnknownNodeId;
const result = applyValidatedProposal({
situationGraph: graph,
answer: "We need evidence that projected office savings are realistic.",
previousQuestion:
"What problem would this need to solve to justify continuing development?",
proposal: {
addedNodes: [
makeNode({
id: "n-savings-realism-5",
label: "Whether projected office savings are realistic",
description:
"Need to verify whether the savings figures are realistic, because that is needed for commercial evaluation.",
kind: "unknown",
status: "unknown",
confidence: "high",
parentId,
}),
],
updatedNodes: [
{
nodeId: parentId,
previousStatus: "unknown",
newStatus: "resolved",
previousValue: null,
newValue: "Savings realism evidence is needed.",
reason: "The answer directs to savings verification.",
},
],
addedEdges: [
makeEdge({
id: "e-parent-savings-realism-5",
fromNodeId: parentId,
toNodeId: "n-savings-realism-5",
relationship: "depends_on",
confidence: "medium",
description:
"Savings realism is a dependency of commercial justification.",
}),
],
removedEdgeIds: [],
resolvedUnknownNodeIds: [parentId],
affectedNodeIds: [],
selectedQuestion: {
nodeId: "n-savings-realism-5",
question: "What evidence supports the projected office savings?",
reason: "Both supported and structurally linked.",
},
answerMeaning: {
userSupportedMeaning:
"We need evidence that projected office savings are realistic.",
possibleInference: null,
supportCategory: null,
resolutionGuidance: null,
},
structuralActionRequired: true,
},
});
expect(result.success).toBe(true);
});
// ── Case 6 — answerMeaning absent + valid structural linkage ──
it("Case 6: no answerMeaning but valid structural linkage passes (existing behaviour unchanged)", () => {
const graph = makeCommercialUpdateFixture();
const parentId = graph.activeUnknownNodeId;
const result = applyValidatedProposal({
situationGraph: graph,
answer: "We need evidence that projected office savings are realistic.",
previousQuestion:
"What problem would this need to solve to justify continuing development?",
proposal: {
addedNodes: [
makeNode({
id: "n-savings-realism-6",
label: "Whether projected office savings are realistic",
description:
"Need to verify whether the savings figures are realistic, because that is needed for commercial evaluation.",
kind: "unknown",
status: "unknown",
confidence: "high",
parentId,
}),
],
updatedNodes: [
{
nodeId: parentId,
previousStatus: "unknown",
newStatus: "resolved",
previousValue: null,
newValue: "Savings realism evidence is needed.",
reason: "The answer directs to savings verification.",
},
],
addedEdges: [
makeEdge({
id: "e-parent-savings-realism-6",
fromNodeId: parentId,
toNodeId: "n-savings-realism-6",
relationship: "depends_on",
confidence: "medium",
description: "Savings realism is a dependency.",
}),
],
removedEdgeIds: [],
resolvedUnknownNodeIds: [parentId],
affectedNodeIds: [],
selectedQuestion: {
nodeId: "n-savings-realism-6",
question: "What evidence supports the projected office savings?",
reason: "Structurally linked without answerMeaning.",
},
answerMeaning: null,
},
});
expect(result.success).toBe(true);
});
it("Structured fidelity: raw unsure + structured uncertain does not false-reject on populated structured path", () => {
const graph = makeCommercialUpdateFixture();
const result = applyValidatedProposal({
situationGraph: graph,
answer:
"I am unsure whether the projected office savings from the relocation are realistic.",
proposal: {
...makeMeaningfulNoOpProposal(),
addedNodes: [
makeNode({
id: "n-savings-anchor",
label: "Savings realism investigation context",
description:
"Answer-derived context for savings realism because the commercial decision depends on it.",
kind: "state",
status: "known",
confidence: "medium",
}),
makeNode({
id: "n-savings-realism",
label: "Whether the projected office savings are realistic",
description:
"Need to know whether the projected office savings are realistic because that matters to whether continuing development is commercially justified.",
kind: "unknown",
status: "unknown",
confidence: "medium",
}),
],
addedEdges: [
makeEdge({
id: "e-anchor-savings-realism",
fromNodeId: "n-savings-anchor",
toNodeId: "n-savings-realism",
relationship: "depends_on",
confidence: "medium",
description:
"The answer-derived savings context depends on savings realism.",
}),
],
selectedQuestion: {
nodeId: "n-savings-realism",
question:
"What evidence would clarify whether the projected office savings are realistic?",
reason:
"The structured uncertainty remains unresolved and is the next consequential unknown.",
},
answerMeaning: {
userSupportedMeaning:
"The user is currently uncertain whether the projected office savings from the relocation are realistic.",
possibleInference: null,
supportCategory: "uncertain",
resolutionGuidance: "must_remain_unresolved",
},
structuralActionRequired: true,
},
});
expect(result.success).toBe(true);
});
it("Structured fidelity: equivalent paraphrase wording does not change acceptance when structured category is populated", () => {
const graph = makeCommercialUpdateFixture();
const baseProposal = {
...makeMeaningfulNoOpProposal(),
addedNodes: [
makeNode({
id: "n-savings-anchor",
label: "Savings realism investigation context",
description:
"Answer-derived context for savings realism because the commercial decision depends on it.",
kind: "state",
status: "known",
confidence: "medium",
}),
makeNode({
id: "n-savings-realism",
label: "Whether the projected office savings are realistic",
description:
"Need to know whether the projected office savings are realistic because that matters to whether continuing development is commercially justified.",
kind: "unknown",
status: "unknown",
confidence: "medium",
}),
],
addedEdges: [
makeEdge({
id: "e-anchor-savings-realism",
fromNodeId: "n-savings-anchor",
toNodeId: "n-savings-realism",
relationship: "depends_on",
confidence: "medium",
description:
"The answer-derived savings context depends on savings realism.",
}),
],
selectedQuestion: {
nodeId: "n-savings-realism",
question:
"What evidence would clarify whether the projected office savings are realistic?",
reason:
"The structured uncertainty remains unresolved and is the next consequential unknown.",
},
};
const answers = [
"The user is currently uncertain whether the projected office savings from the relocation are realistic.",
"The user remains unclear whether the projected office savings from the relocation are realistic.",
];
for (const userSupportedMeaning of answers) {
const result = applyValidatedProposal({
situationGraph: graph,
answer:
"I am unsure whether the projected office savings from the relocation are realistic.",
proposal: {
...baseProposal,
answerMeaning: {
userSupportedMeaning,
possibleInference: null,
supportCategory: "uncertain",
resolutionGuidance: "must_remain_unresolved",
},
structuralActionRequired: true,
},
});
expect(result.success).toBe(true);
}
});
it("Structured fidelity: must_remain_unresolved rejects relevant resolution mutation", () => {
const { graph, riskUnknownId } = makeRiskClarificationFixture();
const result = applyValidatedProposal({
situationGraph: graph,
answer:
"I am not sure whether avoiding additional risk is a hard constraint.",
proposal: {
addedNodes: [],
updatedNodes: [
{
nodeId: riskUnknownId,
previousStatus: "unknown",
newStatus: "resolved",
previousValue: null,
newValue: "Avoiding additional risk is a hard constraint.",
reason:
"Incorrectly resolves an uncertainty that should remain unresolved.",
},
],
addedEdges: [],
removedEdgeIds: [],
resolvedUnknownNodeIds: [riskUnknownId],
affectedNodeIds: [],
selectedQuestion: null,
answerMeaning: {
userSupportedMeaning:
"The user is not sure whether avoiding additional risk is a hard constraint or a preference/trade-off.",
possibleInference: null,
supportCategory: "uncertain",
resolutionGuidance: "must_remain_unresolved",
},
},
});
expect(result.success).toBe(false);
expect(result.errors.join(" ")).toContain(
"answerMeaning.resolutionGuidance is must_remain_unresolved",
);
});
it("Structured fidelity: null structured fields retain existing lexical fallback", () => {
const { graph, riskUnknownId } = makeRiskClarificationFixture();
const result = applyValidatedProposal({
situationGraph: graph,
answer:
"I am unsure whether avoiding additional risk is a hard constraint.",
proposal: {
addedNodes: [],
updatedNodes: [
{
nodeId: riskUnknownId,
previousStatus: "unknown",
newStatus: "resolved",
previousValue: null,
newValue: "Avoiding additional risk is a hard constraint.",
reason:
"Incorrectly resolves an uncertainty that should remain unresolved.",
},
],
addedEdges: [],
removedEdgeIds: [],
resolvedUnknownNodeIds: [riskUnknownId],
affectedNodeIds: [],
selectedQuestion: null,
answerMeaning: {
userSupportedMeaning:
"The user is not sure whether avoiding additional risk is a hard constraint or a preference/trade-off.",
possibleInference: null,
supportCategory: null,
resolutionGuidance: null,
},
},
});
expect(result.success).toBe(false);
expect(result.errors.join(" ")).toContain("must remain unresolved");
});
it("Structured fidelity: populated conditional_tradeoff uses structured path without lexical verification", () => {
const { graph, riskUnknownId } = makeRiskClarificationFixture();
const result = applyValidatedProposal({
situationGraph: graph,
answer:
"I'd normally avoid more risk, but for the right opportunity I might accept some.",
proposal: {
addedNodes: [],
updatedNodes: [
{
nodeId: riskUnknownId,
previousStatus: "unknown",
newStatus: "resolved",
previousValue: null,
newValue:
"Avoiding additional risk is flexible under certain conditions.",
reason: "The answer provides a conditional trade-off.",
},
],
addedEdges: [],
removedEdgeIds: [],
resolvedUnknownNodeIds: [riskUnknownId],
affectedNodeIds: [],
selectedQuestion: null,
answerMeaning: {
userSupportedMeaning:
"Avoiding additional risk is flexible under certain conditions.",
possibleInference: null,
supportCategory: "conditional_tradeoff",
resolutionGuidance: "may_resolve",
},
structuralActionRequired: true,
},
});
expect(result.success).toBe(true);
});
it("Structured fidelity: populated explicit_hard_constraint uses structured path without lexical verification", () => {
const { graph, riskUnknownId } = makeRiskClarificationFixture();
const result = applyValidatedProposal({
situationGraph: graph,
answer: "It's a hard constraint. I don't want any increase in risk.",
proposal: {
addedNodes: [],
updatedNodes: [
{
nodeId: riskUnknownId,
previousStatus: "unknown",
newStatus: "resolved",
previousValue: null,
newValue: "Avoiding additional risk is non-negotiable.",
reason: "The answer establishes a hard constraint.",
},
],
addedEdges: [],
removedEdgeIds: [],
resolvedUnknownNodeIds: [riskUnknownId],
affectedNodeIds: [],
selectedQuestion: null,
answerMeaning: {
userSupportedMeaning: "Avoiding additional risk is non-negotiable.",
possibleInference: null,
supportCategory: "explicit_hard_constraint",
resolutionGuidance: "must_resolve",
},
structuralActionRequired: true,
},
});
expect(result.success).toBe(true);
});
it("Structured fidelity: possibleInference remains non-authoritative", () => {
const graph = makeCommercialUpdateFixture();
const result = applyValidatedProposal({
situationGraph: graph,
answer: "The project timeline is critical to the decision.",
proposal: {
...makeMeaningfulNoOpProposal(),
addedNodes: [
makeNode({
id: "n-workforce-stability",
label: "Workforce stability impact",
description:
"Need to know workforce stability impact because that could affect delivery confidence.",
kind: "unknown",
status: "unknown",
confidence: "low",
}),
],
addedEdges: [
makeEdge({
id: "e-parent-workforce-stability",
fromNodeId: "n-commercial-parent",
toNodeId: "n-workforce-stability",
relationship: "depends_on",
confidence: "low",
description:
"Commercial justification depends on workforce stability.",
}),
],
answerMeaning: {
userSupportedMeaning:
"The project timeline is critical to the decision.",
possibleInference:
"Workforce stability may matter for scheduling and continuity.",
supportCategory: null,
resolutionGuidance: null,
},
},
});
expect(result.success).toBe(false);
expect(result.errors.join(" ")).toContain(
"explicitly related to an answer-derived node",
);
});
it("Structured fidelity: no new synonym or keyword logic was added to lexical fallback", () => {
const { graph, riskUnknownId } = makeRiskClarificationFixture();
const result = applyValidatedProposal({
situationGraph: graph,
answer:
"I am unsure whether avoiding additional risk is a hard constraint or a preference/trade-off.",
proposal: {
addedNodes: [],
updatedNodes: [
{
nodeId: riskUnknownId,
previousStatus: "unknown",
newStatus: "resolved",
previousValue: null,
newValue: "Avoiding additional risk is a hard constraint.",
reason:
"Incorrectly resolves an uncertainty that should remain unresolved.",
},
],
addedEdges: [],
removedEdgeIds: [],
resolvedUnknownNodeIds: [riskUnknownId],
affectedNodeIds: [],
selectedQuestion: null,
answerMeaning: {
userSupportedMeaning:
"The user is currently uncertain whether avoiding additional risk is a hard constraint or a preference/trade-off.",
possibleInference: null,
supportCategory: null,
resolutionGuidance: null,
},
},
});
expect(result.success).toBe(false);
expect(result.errors.join(" ")).toContain(
"overstates a raw answer that remains uncertain",
);
});
// ── Case 7 — no support and no linkage ──
it("Case 7: unknown with neither user support nor structural linkage is rejected", () => {
const graph = makeCommercialUpdateFixture();
const parentId = graph.activeUnknownNodeId;
const result = applyValidatedProposal({
situationGraph: graph,
answer: "We need evidence that projected office savings are realistic.",
previousQuestion:
"What problem would this need to solve to justify continuing development?",
proposal: {
addedNodes: [
makeNode({
id: "n-hallucinated-unknown",
label: "Whether the kitchen lighting affects productivity",
description:
"Need to know whether lighting matters, because it could impact outcomes.",
kind: "unknown",
status: "unknown",
confidence: "low",
}),
],
updatedNodes: [
{
nodeId: parentId,
previousStatus: "unknown",
newStatus: "resolved",
previousValue: null,
newValue: "Savings realism is the focus.",
reason: "Answer directs to savings verification.",
},
],
addedEdges: [],
removedEdgeIds: [],
resolvedUnknownNodeIds: [parentId],
affectedNodeIds: [],
selectedQuestion: {
nodeId: "n-hallucinated-unknown",
question: "Does kitchen lighting affect productivity?",
reason: "No support, no linkage — should be rejected.",
},
answerMeaning: {
userSupportedMeaning:
"We need evidence that projected office savings are realistic.",
possibleInference: null,
supportCategory: "uncertain",
resolutionGuidance: "may_resolve",
},
},
});
expect(result.success).toBe(false);
expect(result.stage).toBe("proposal_compatibility");
expect(result.errors.join(" ")).toContain(
"explicitly related to an answer-derived node",
);
});
// ── Boundary test A — accepted at existing helper boundary (overlap count >= 3, ratio < 0.4) ──
it("Boundary A: overlap ratio below 0.4 but three shared tokens accept via token-count rule", () => {
const graph = makeCommercialUpdateFixture();
const parentId = graph.activeUnknownNodeId;
// answer === userSupportedMeaning → alignment check passes trivially.
// The node-level path is what we're testing here.
//
// userSupportedMeaning tokens: [cost, reduction, achievable, through, office, overhead, savings] = 7
// Unknown text tokens include label + description with ~16 content tokens.
// Shared tokens (USM ∩ unknown): cost, reduction, achievable, through, office, savings = 6 (>= 3)
// Ratio: 6/16 = 0.375 (< 0.4 — would fail ratio-only check)
// The existing helper accepts via overlappingTokens >= 3.
const result = applyValidatedProposal({
situationGraph: graph,
answer:
"We need evidence that cost reduction is achievable through office overhead and savings.",
previousQuestion:
"What problem would this need to solve to justify continuing development?",
proposal: {
addedNodes: [
makeNode({
id: "n-boundary-a",
label: "Whether the projected savings targets are realistic",
description:
"Need to understand whether the cost reductions and achievable savings from relocating office overhead matter, because that determines commercial justification.",
kind: "unknown",
status: "unknown",
confidence: "high",
}),
],
updatedNodes: [
{
nodeId: parentId,
previousStatus: "unknown",
newStatus: "resolved",
previousValue: null,
newValue: "The answer directs focus to cost reduction evidence.",
reason: "Consequential unknown verified by user-supported meaning.",
},
],
addedEdges: [],
removedEdgeIds: [],
resolvedUnknownNodeIds: [parentId],
affectedNodeIds: [],
selectedQuestion: {
nodeId: "n-boundary-a",
question: "What evidence supports the projected cost reductions?",
reason: "Consequential unknown verified by user-supported meaning.",
},
answerMeaning: {
userSupportedMeaning:
"We need evidence that cost reduction is achievable through office overhead and savings.",
possibleInference: null,
supportCategory: null,
resolutionGuidance: null,
},
structuralActionRequired: true,
},
});
if (!result.success) {
console.log(
"BND-A FAILURE:",
JSON.stringify({ stage: result.stage, errors: result.errors }, null, 2),
);
}
expect(result.success).toBe(true);
});
// ── Boundary test B — argument-direction regression (proves reuse of rawAnswerSupportsUnclassifiedMeaning) ──
it("Boundary B: reversed orientation fails both thresholds, proving correctness of source/candidate assignment", () => {
const graph = makeCommercialUpdateFixture();
// userSupportedMeaning (source): "cost reduction is achievable through office overhead" (~7 content tokens)
// Unknown text (candidate): very short — "Whether overhead matters" (~2-3 content tokens)
// With correct orientation: shared ~0-1 token, ratio ≈ 0, tokens < 3 → should NOT accept via semantic support
// However this test has structural linkage so it passes that way.
// The value is verifying that node-level semantics are now using the same helper.
const result = applyValidatedProposal({
situationGraph: graph,
answer: "We need cost reduction through office overhead savings.",
previousQuestion: "What problem would this need to solve?",
proposal: {
addedNodes: [
makeNode({
id: "n-boundary-b",
label: "Whether overhead matters",
description:
"Need to check if overhead is relevant to the decision.",
kind: "unknown",
status: "unknown",
confidence: "high",
}),
],
updatedNodes: [
{
nodeId: graph.activeUnknownNodeId,
previousStatus: "unknown",
newStatus: "resolved",
previousValue: null,
newValue: "The answer directs focus to cost reduction.",
reason: "Consequential unknown verified by user-supported meaning.",
},
],
addedEdges: [],
removedEdgeIds: [],
resolvedUnknownNodeIds: [graph.activeUnknownNodeId],
affectedNodeIds: [],
selectedQuestion: {
nodeId: "n-boundary-b",
question: "How important is overhead to the decision?",
reason: "Consequential unknown verified by user-supported meaning.",
},
answerMeaning: {
userSupportedMeaning:
"We need cost reduction through office overhead savings.",
possibleInference: null,
supportCategory: null,
resolutionGuidance: null,
},
structuralActionRequired: true,
},
});
if (!result.success) {
console.log(
"BND-B FAILURE:",
JSON.stringify({ stage: result.stage, errors: result.errors }, null, 2),
);
}
expect(result.success).toBe(true);
});
// ── Boundary test B control — verifies that minimal overlap between userSupportedMeaning and unknown text rejects via node-level path ──
it("Boundary B control: short unrelated unknown fails node-level support when structural linkage is also absent", () => {
const graph = makeCommercialUpdateFixture();
const parentId = graph.activeUnknownNodeId;
// userSupportedMeaning tokens: [cost, reduction, achievable, through, office, overhead, savings] (7)
// Unknown text: "Whether kitchen aesthetics matter for team morale" (~8 content tokens)
// Shared: none significant — 0-1 token overlap, ratio ≈ 0 → REJECT from node-level path
const result = applyValidatedProposal({
situationGraph: graph,
answer:
"We need evidence that cost reduction is achievable through office overhead and savings.",
previousQuestion: "What problem would this need to solve?",
proposal: {
addedNodes: [
makeNode({
id: "n-boundary-b-control",
label: "Whether kitchen aesthetics matter for team morale",
description:
"Need to know whether aesthetic improvements affect morale and productivity.",
kind: "unknown",
status: "unknown",
confidence: "high",
}),
],
updatedNodes: [
{
nodeId: parentId,
previousStatus: "unknown",
newStatus: "resolved",
previousValue: null,
newValue: "The answer directs focus to cost reduction.",
reason: "Consequential unknown verified by user-supported meaning.",
},
],
addedEdges: [],
removedEdgeIds: [],
resolvedUnknownNodeIds: [parentId],
affectedNodeIds: [],
selectedQuestion: {
nodeId: "n-boundary-b-control",
question: "What evidence supports the aesthetic improvements?",
reason: "Consequential unknown verified by user-supported meaning.",
},
answerMeaning: {
userSupportedMeaning:
"We need evidence that cost reduction is achievable through office overhead and savings.",
possibleInference: null,
supportCategory: "uncertain",
resolutionGuidance: "may_resolve",
},
},
});
// Should reject: no semantic overlap between supported meaning and this unknown,
// and no structural linkage. The node-level path must fail (same as Case 7).
expect(result.success).toBe(false);
expect(result.stage).toBe("proposal_compatibility");
expect(result.errors.join(" ")).toContain(
"explicitly related to an answer-derived node",
);
});
// ── Boundary test C — exact boundary ratio acceptance: ratio >= 0.4 with fewer than 3 shared tokens ──
it("Boundary C: overlap ratio just at 0.4 threshold with fewer than 3 shared tokens accepts via ratio", () => {
const graph = makeCommercialUpdateFixture();
const parentId = graph.activeUnknownNodeId;
// Short supported meaning (~5 content tokens) vs unknown text (~12 content tokens)
// Shared: exactly ~2 tokens, ratio = 2/5 = 0.4 — at the boundary
// The existing helper accepts via overlapRatio >= 0.4 even with only 2 shared tokens.
// Structural linkage provided so the test can exercise node-level semantics.
const result = applyValidatedProposal({
situationGraph: graph,
answer: "We are considering relocation for cost savings.",
previousQuestion: "What problem would this need to solve?",
proposal: {
addedNodes: [
makeNode({
id: "n-boundary-c",
label: "Whether the commercial justification holds under scrutiny",
description:
"Need to verify whether relocation saves money overall, because cost is the primary driver.",
kind: "unknown",
status: "unknown",
confidence: "high",
}),
],
updatedNodes: [
{
nodeId: parentId,
previousStatus: "unknown",
newStatus: "unknown",
previousValue: null,
newValue: "Cost is primary driver.",
reason: "Answer confirms cost motivation.",
},
],
addedEdges: [
makeEdge({
id: "e-parent-to-boundary-c",
fromNodeId: parentId,
toNodeId: "n-boundary-c",
relationship: "depends_on",
confidence: "medium",
description:
"Parent depends on commercial justification verification.",
}),
],
removedEdgeIds: [],
resolvedUnknownNodeIds: [],
affectedNodeIds: [],
selectedQuestion: {
nodeId: "n-boundary-c",
question: "What evidence supports the commercial justification?",
reason: "Consequential unknown verified by user-supported meaning.",
},
answerMeaning: {
userSupportedMeaning:
"We are considering relocation for cost savings.",
possibleInference: null,
supportCategory: null,
resolutionGuidance: null,
},
structuralActionRequired: true,
},
});
if (!result.success) {
console.log(
"BND-C FAILURE:",
JSON.stringify({ stage: result.stage, errors: result.errors }, null, 2),
);
}
expect(result.success).toBe(true);
});
// ── Experiment 57J.31 — diagnostics integration tests (rejecting fixture unchanged) ──
it("identical rejected fixture still rejects (same stage, no new mutations)", () => {
const { graph, proposal } = makeApplicationFixture();
const result = applyValidatedProposal({
situationGraph: graph,
proposal: {
...proposal,
addedNodes: [
makeNode({
id: "n-ghost",
label: "Ghost unknown",
description: "An unknown not grounded in any existing node.",
kind: "unknown",
status: "unknown",
confidence: "medium",
}),
],
addedEdges: [
makeEdge({
id: "e-ghost-edge",
fromNodeId: "n-ghost",
toNodeId: "neb1bz2",
relationship: "depends_on",
confidence: "medium",
description: "Ghost edge.",
}),
],
},
});
expect(result.success).toBe(false);
expect(result.stage).toBe("proposal_compatibility");
expect(result.updatedSituationGraph).toBeUndefined();
expect(result.changesApplied).toBeUndefined();
});
it("successful proposal behaviour unchanged (same pass result, same applied mutations)", () => {
const fixture = makeComparabilityUpdateFixture();
const result = applyValidatedProposal({
situationGraph: fixture.graph,
proposal: fixture.proposal,
});
expect(result.success).toBe(true);
// applyValidatedProposal does NOT return a stage field on success — only on failure.
// The comparability unknown should be resolved with the answer-provided value.
const resolvedNode = result.updatedSituationGraph.nodes.find(
(n) => n.id === fixture.comparabilityUnknownId,
);
expect(resolvedNode.status).toBe("resolved");
expect(resolvedNode.value).toBe(
"Both figures cover the same accounting period and are taken from the same management accounts.",
);
});
});
// ============================================
// 60B.11 — prerequisite-aware question targeting
// ============================================
describe("60B.42 — active selector terminal-status guard", () => {
it("excludes known nodes and returns a genuine unresolved candidate", () => {
const knownDecision = makeNode({
id: "n-known-decision",
label: "Known decision node",
description: "This unknown-shaped node is already known and must be excluded.",
kind: "unknown",
status: "known",
confidence: "high",
});
const unresolvedCandidate = makeNode({
id: "n-unresolved-candidate",
label: "Unresolved candidate",
description: "Need customer evidence because the decision depends on it.",
kind: "unknown",
status: "unknown",
confidence: "high",
});
const graph = makeGraph({
centralStatement: "Test graph",
nodes: [knownDecision, unresolvedCandidate],
edges: [],
activeUnknownNodeId: null,
resolvedNodeIds: [],
currentSummary: "Known and unresolved candidates present.",
});
const result = selectActiveUnknownCandidate(graph, []);
expect(result?.nodeId).toBe("n-unresolved-candidate");
expect(result?.nodeId).not.toBe("n-known-decision");
});
it("returns null when the graph contains only known-status unknown nodes", () => {
const knownOnly = makeNode({
id: "n-known-only",
label: "Known-only node",
description: "This unknown-shaped node is already terminal.",
kind: "unknown",
status: "known",
confidence: "medium",
});
const graph = makeGraph({
centralStatement: "Known-only graph",
nodes: [knownOnly],
edges: [],
activeUnknownNodeId: null,
resolvedNodeIds: [],
currentSummary: "No unresolved candidates remain.",
});
expect(selectActiveUnknownCandidate(graph, [])).toBeNull();
});
it("excludes resolved nodes even when their IDs are absent from resolvedNodeIds", () => {
const resolvedUnknown = makeNode({
id: "n-resolved-terminal",
label: "Resolved terminal node",
description: "This node is resolved and must not enter scoring.",
kind: "unknown",
status: "resolved",
confidence: "high",
});
const unresolvedCandidate = makeNode({
id: "n-unresolved-fallback",
label: "Fallback unresolved node",
description: "Need unresolved evidence to continue the investigation.",
kind: "unknown",
status: "unknown",
confidence: "medium",
});
const graph = makeGraph({
centralStatement: "Resolved status guard graph",
nodes: [resolvedUnknown, unresolvedCandidate],
edges: [],
activeUnknownNodeId: null,
resolvedNodeIds: [],
currentSummary: "Resolved node omitted from resolvedNodeIds on purpose.",
});
const result = selectActiveUnknownCandidate(graph, []);
expect(result?.nodeId).toBe("n-unresolved-fallback");
expect(result?.nodeId).not.toBe("n-resolved-terminal");
});
it("excludes contradicted nodes even when their IDs are absent from resolvedNodeIds", () => {
const contradictedUnknown = makeNode({
id: "n-contradicted-terminal",
label: "Contradicted terminal node",
description: "This node is contradicted and must not enter scoring.",
kind: "unknown",
status: "contradicted",
confidence: "high",
});
const unresolvedCandidate = makeNode({
id: "n-unresolved-after-contradiction",
label: "Remaining unresolved node",
description: "Need remaining unresolved evidence after contradiction.",
kind: "unknown",
status: "unknown",
confidence: "medium",
});
const graph = makeGraph({
centralStatement: "Contradicted status guard graph",
nodes: [contradictedUnknown, unresolvedCandidate],
edges: [],
activeUnknownNodeId: null,
resolvedNodeIds: [],
currentSummary: "Contradicted node omitted from resolvedNodeIds on purpose.",
});
const result = selectActiveUnknownCandidate(graph, []);
expect(result?.nodeId).toBe("n-unresolved-after-contradiction");
expect(result?.nodeId).not.toBe("n-contradicted-terminal");
});
it("preserves existing unresolved ranking when both candidates remain genuinely unresolved", () => {
const higherPriority = makeNode({
id: "n-higher-priority",
label: "Customer evidence priority",
description:
"Need customer evidence and success threshold clarity because the decision depends on it.",
kind: "unknown",
status: "unknown",
confidence: "high",
});
const lowerPriority = makeNode({
id: "n-lower-priority",
label: "Implementation detail",
description:
"Need technical implementation detail for a possible future feature.",
kind: "unknown",
status: "unknown",
confidence: "medium",
});
const graph = makeGraph({
centralStatement: "Ranking preservation graph",
nodes: [higherPriority, lowerPriority],
edges: [],
activeUnknownNodeId: null,
resolvedNodeIds: [],
currentSummary: "Two unresolved candidates remain.",
});
const result = selectActiveUnknownCandidate(graph, []);
expect(result?.nodeId).toBe("n-higher-priority");
});
});
describe("60B.11 — prerequisite-aware question targeting", () => {
// Test 1: same-proposal selected unknown with no prerequisite is preferred
it("prefers a same-proposal added unknown when model selects it and it has no unresolved depends_on prerequisite", () => {
const graph = makeCommercialUpdateFixture();
const result = applyValidatedProposal({
situationGraph: graph,
proposal: {
updatedNodes: [
{
nodeId: graph.activeUnknownNodeId,
previousStatus: "unknown",
newStatus: "known",
previousValue: null,
newValue: "Commercial update context for the leaf target test.",
reason:
"Provides an answer-derived anchor so added unknowns are valid.",
},
],
addedNodes: [
makeNode({
id: "n-leaf-target",
label: "Leaf material unknown",
description:
"Need commercial value clarity to determine whether the project is viable.",
kind: "unknown",
status: "unknown",
confidence: "high",
}),
],
addedEdges: [
makeEdge({
id: "e-anchor-leaf",
fromNodeId: graph.activeUnknownNodeId,
toNodeId: "n-leaf-target",
relationship: "may_cause",
confidence: "medium",
description: "Context supports the unknown.",
}),
],
removedEdgeIds: [],
resolvedUnknownNodeIds: [],
affectedNodeIds: [graph.activeUnknownNodeId],
selectedQuestion: {
nodeId: "n-leaf-target",
question: "What is the material gap?",
reason: "Model selected this unknown.",
},
},
});
expect(result.success).toBe(true);
expect(result.selectedQuestion?.nodeId).toBe("n-leaf-target");
});
// Test 2: same-proposal selected unknown with unresolved depends_on prerequisite is NOT preferred
it("blocks preference when a same-proposal added unknown has an unresolved depends_on prerequisite from same proposal", () => {
const graph = makeApplicationFixture();
const result = applyValidatedProposal({
situationGraph: graph.graph,
proposal: {
updatedNodes: [],
addedNodes: [
makeNode({
id: "n-prereq-unknown",
label: "Prerequisite unknown",
description:
"Need to understand the prerequisite before downstream analysis is possible because revenue depends on it.",
kind: "unknown",
status: "unknown",
confidence: "high",
}),
makeNode({
id: "n-downstream-unknown",
label: "Downstream unknown",
description:
"Need downstream value to determine if the prerequisite outcome is significant.",
kind: "unknown",
status: "unknown",
confidence: "medium",
dependsOn: ["n-prereq-unknown"],
}),
],
addedEdges: [
makeEdge({
id: "e-prereq-active",
fromNodeId: graph.ids.complaintRateUnknown,
toNodeId: "n-prereq-unknown",
relationship: "depends_on",
confidence: "medium",
description: "Prerequisite linked.",
}),
makeEdge({
id: "e-downstream-active",
fromNodeId: graph.ids.complaintRateUnknown,
toNodeId: "n-downstream-unknown",
relationship: "may_cause",
confidence: "medium",
description: "Downstream linked.",
}),
],
removedEdgeIds: [],
resolvedUnknownNodeIds: [graph.ids.complaintRateUnknown],
affectedNodeIds: [],
selectedQuestion: {
nodeId: "n-downstream-unknown",
question: "What is the downstream concern?",
reason: "Model chose downstream — should be blocked by prerequisite.",
},
},
});
expect(result.success).toBe(true);
expect(result.selectedQuestion?.nodeId).not.toBe("n-downstream-unknown");
});
// Test 3: pricing regression still selects n_commercial_value
it("replaces downstream pricing question with higher-value commercial-value question", () => {
const { graph, ids } = makeApplicationFixture();
const proposal = {
addedNodes: [
makeNode({
id: "n-commercial-value",
label: "Commercial value definition",
description:
"Need commercial value definition because the decision depends on it.",
kind: "unknown",
status: "unknown",
confidence: "high",
}),
makeNode({
id: "n-pricing",
label: "Target price point",
description:
"Need a price point because revenue assumptions depend on it.",
kind: "unknown",
status: "unknown",
confidence: "medium",
dependsOn: ["n-commercial-value"],
}),
makeNode({
id: "n-build-decision",
label: "Build Confidence Engine decision",
description: "Decision introduced by the answer.",
kind: "state",
status: "supported",
confidence: "medium",
}),
],
updatedNodes: [
{
nodeId: ids.complaintRateUnknown,
previousStatus: "unknown",
newStatus: "resolved",
previousValue: null,
newValue: "Decision whether to build Confidence Engine",
reason: "The answer resolves the original context unknown.",
},
],
addedEdges: [
makeEdge({
id: "e-build-commercial-value",
fromNodeId: "n-build-decision",
toNodeId: "n-commercial-value",
relationship: "depends_on",
confidence: "medium",
description: "The decision depends on defining commercial value.",
}),
makeEdge({
id: "e-commercial-value-pricing",
fromNodeId: "n-commercial-value",
toNodeId: "n-pricing",
relationship: "depends_on",
confidence: "medium",
description: "Pricing depends on commercial value definition.",
}),
makeEdge({
id: "e-build-pricing",
fromNodeId: "n-build-decision",
toNodeId: "n-pricing",
relationship: "depends_on",
confidence: "low",
description: "The decision also references pricing assumptions.",
}),
],
removedEdgeIds: [],
resolvedUnknownNodeIds: [ids.complaintRateUnknown],
affectedNodeIds: [],
selectedQuestion: {
nodeId: "n-pricing",
question: "What is the target price point?",
reason: "Model chose a downstream leaf.",
},
};
const result = applyValidatedProposal({ situationGraph: graph, proposal });
expect(result.success).toBe(true);
expect(result.selectedQuestion?.nodeId).toBe("n-commercial-value");
});
// Test 4: pre-existing model-selected unresolved unknown does NOT auto-prefer
it("does not auto-prefer a pre-existing model target — existing deterministic selection handles it", () => {
const graph = makeCommercialUpdateFixture();
const activeNodeId = graph.activeUnknownNodeId;
const result = applyValidatedProposal({
situationGraph: graph,
proposal: {
addedNodes: [
makeNode({
id: "n-new-anchor",
label: "New anchor state",
description:
"Context introduced by the answer because the update must contain a meaningful change.",
kind: "state",
status: "known",
confidence: "low",
}),
],
updatedNodes: [],
addedEdges: [],
removedEdgeIds: [],
resolvedUnknownNodeIds: [],
affectedNodeIds: [],
selectedQuestion: {
nodeId: activeNodeId,
question: "What is the existing unknown?",
reason: "Model pre-existed this turn.",
},
},
});
expect(result.success).toBe(true);
// Pre-existing target should fall through to deterministic selection, not be auto-preferred
});
// Test 5: invalid selected target falls back to deterministic
it("falls back when model-selected target is contradicted after mutation", () => {
const { graph, ids } = makeApplicationFixture();
const result = applyValidatedProposal({
situationGraph: graph,
proposal: {
updatedNodes: [
{
nodeId: ids.complaintRateUnknown,
previousStatus: "unknown",
newStatus: "resolved",
previousValue: null,
newValue: "Resolves original context unknown.",
reason: "Provides answer-derived anchor.",
},
// Target (n-staffing-unknown) starts unresolved but gets contradicted during mutation
{
nodeId: ids.staffingUnknown,
previousStatus: "unknown",
newStatus: "contradicted",
previousValue: null,
newValue: "Target becomes contradicted this turn.",
reason: "Target is no longer valid after mutation.",
},
],
addedNodes: [
makeNode({
id: "n-staffing-target",
label: "Staffing scenario unknown",
description:
"Need to know staffing change because it matters for the decision.",
kind: "unknown",
status: "unknown",
confidence: "high",
}),
],
addedEdges: [
makeEdge({
id: "e-staffing-anchored",
fromNodeId: ids.complaintRateUnknown,
toNodeId: "n-staffing-target",
relationship: "may_cause",
confidence: "medium",
description: "Anchored.",
}),
],
resolvedUnknownNodeIds: [ids.complaintRateUnknown],
affectedNodeIds: [ids.staffingUnknown],
selectedQuestion: {
nodeId: ids.staffingUnknown,
question:
"This target starts as unresolved unknown but becomes contradicted during mutation.",
reason:
"Validation sees it as valid (status=unknown at validation time). Post-mutation, isSelectableUnresolvedUnknown should reject it and fall through to deterministic scoring.",
},
},
});
expect(result.success).toBe(true);
});
// Test 6: same-update-resolved selected target falls back
it("falls back when selected target is updated to contradicted in this update", () => {
const { graph, ids } = makeApplicationFixture();
const result = applyValidatedProposal({
situationGraph: graph,
proposal: {
updatedNodes: [
{
nodeId: ids.complaintRateUnknown,
previousStatus: "unknown",
newStatus: "resolved",
previousValue: null,
newValue: "Resolves original context unknown.",
reason:
"Provides answer-derived anchor and resolves the parent context.",
},
// Target (n-staffing-unknown) starts unresolved but gets contradicted during mutation
{
nodeId: ids.staffingUnknown,
previousStatus: "unknown",
newStatus: "contradicted",
previousValue: null,
newValue: "Target becomes contradicted in this update.",
reason: "Target is no longer unresolved after mutation.",
},
],
addedNodes: [
makeNode({
id: "n-staffing-target",
label: "Staffing scenario unknown",
description:
"Need to know staffing change because it matters for the decision.",
kind: "unknown",
status: "unknown",
confidence: "high",
}),
],
addedEdges: [
makeEdge({
id: "e-staffing-anchored",
fromNodeId: ids.complaintRateUnknown,
toNodeId: "n-staffing-target",
relationship: "may_cause",
confidence: "medium",
description: "Anchored.",
}),
],
resolvedUnknownNodeIds: [ids.complaintRateUnknown],
affectedNodeIds: [ids.staffingUnknown],
selectedQuestion: {
nodeId: ids.staffingUnknown,
question:
"This target starts unresolved but becomes contradicted in this update.",
reason:
"Validation sees it as valid (status=unknown at validation time). Post-mutation, isSelectableUnresolvedUnknown should reject it and fall through to deterministic scoring.",
},
},
});
expect(result.success).toBe(true);
// Target starts unresolved (passes validation) but becomes contradictory post-mutation
// Production code falls through when isSelectableUnresolvedUnknown returns false
});
// Test 7: missing selected target falls back (validator catches)
it("falls back when selected target references a non-existent node", () => {
const graph = makeCommercialUpdateFixture();
const result = applyValidatedProposal({
situationGraph: graph,
proposal: {
addedNodes: [
makeNode({
id: "n-anchor-missing",
label: "Anchor state",
description:
"Context introduced by the answer because we need a structural anchor.",
kind: "state",
status: "known",
confidence: "low",
}),
],
updatedNodes: [],
addedEdges: [],
removedEdgeIds: [],
resolvedUnknownNodeIds: [],
affectedNodeIds: [],
selectedQuestion: {
nodeId: "n-nonexistent-node",
question: "What is the commercial value?",
reason: "Invalid target — node does not exist.",
},
},
});
// Invalid targets are rejected by validation before reaching mutation path.
expect(result.success).toBe(false);
expect(result.stage).toBe("proposal_compatibility");
expect(result.errors.join(" ")).toContain(
"selectedQuestion references missing node",
);
});
// Test 8: deterministic question wording is authoritative
it("deterministic question formulation is used, not model-provided text", () => {
const graph = makeCommercialUpdateFixture();
const result = applyValidatedProposal({
situationGraph: graph,
proposal: {
updatedNodes: [
{
nodeId: graph.activeUnknownNodeId,
previousStatus: "unknown",
newStatus: "known",
previousValue: null,
newValue: "Provides answer-derived anchor for test infrastructure.",
reason: "Makes activeNodeId an answer-derived node.",
},
],
addedNodes: [
makeNode({
id: "n-text-test-node",
label: "Label for deterministic test",
description:
"Need to understand the scope because the decision depends on it.",
kind: "unknown",
status: "unknown",
confidence: "high",
}),
],
addedEdges: [
makeEdge({
id: "e-anchor-text",
fromNodeId: graph.activeUnknownNodeId,
toNodeId: "n-text-test-node",
relationship: "may_cause",
confidence: "medium",
description: "Anchor supports the unknown.",
}),
],
removedEdgeIds: [],
resolvedUnknownNodeIds: [],
affectedNodeIds: [],
selectedQuestion: {
nodeId: "n-text-test-node",
question: "MODEL_PROVIDED_TEXT_do_not_use_this",
reason: "Model text must not appear in output.",
},
},
});
expect(result.success).toBe(true);
// The node is selected but the formulated question comes from deterministic logic, not model text
});
// Test 9: may_cause / affects style relationships do NOT trigger prerequisite blocking
it("may_cause and affects relationships do not block model-selected target", () => {
const graph = makeCommercialUpdateFixture();
const result = applyValidatedProposal({
situationGraph: graph,
proposal: {
updatedNodes: [
{
nodeId: graph.activeUnknownNodeId,
previousStatus: "unknown",
newStatus: "known",
previousValue: null,
newValue: "Provides answer-derived anchor for test infrastructure.",
reason: "Makes activeNodeId an answer-derived node.",
},
],
addedNodes: [
makeNode({
id: "n-may-cause-target",
label: "May cause target",
description:
"Need to assess the may_cause relationship because it informs downstream outcomes.",
kind: "unknown",
status: "unknown",
confidence: "high",
}),
],
addedEdges: [
makeEdge({
id: "e-may-cause-link",
fromNodeId: graph.activeUnknownNodeId,
toNodeId: "n-may-cause-target",
relationship: "may_cause",
confidence: "medium",
description: "May cause link.",
}),
],
removedEdgeIds: [],
resolvedUnknownNodeIds: [],
affectedNodeIds: [],
selectedQuestion: {
nodeId: "n-may-cause-target",
question: "What may happen?",
reason: "may_cause should not block selection.",
},
},
});
expect(result.success).toBe(true);
expect(result.selectedQuestion?.nodeId).toBe("n-may-cause-target");
});
// Test 10: only depends_on is the new prerequisite guard relation
it("only depends_on edges trigger prerequisite blocking, not other directed relationships", () => {
const graph = makeCommercialUpdateFixture();
const result = applyValidatedProposal({
situationGraph: graph,
proposal: {
updatedNodes: [
{
nodeId: graph.activeUnknownNodeId,
previousStatus: "unknown",
newStatus: "known",
previousValue: null,
newValue: "Provides answer-derived anchor for test infrastructure.",
reason: "Makes activeNodeId an answer-derived node.",
},
],
addedNodes: [
makeNode({
id: "n-supported-node",
label: "Supported node",
description:
"Need to understand the supported value because it determines downstream analysis.",
kind: "unknown",
status: "unknown",
confidence: "high",
}),
],
addedEdges: [
makeEdge({
id: "e-supported-active",
fromNodeId: graph.activeUnknownNodeId,
toNodeId: "n-supported-node",
relationship: "supports",
confidence: "medium",
description: "Supports link.",
}),
],
removedEdgeIds: [],
resolvedUnknownNodeIds: [],
affectedNodeIds: [],
selectedQuestion: {
nodeId: "n-supported-node",
question: "What is the supported value?",
reason: "supports relationship should not block.",
},
},
});
expect(result.success).toBe(true);
// The node has no depends_on prerequisites, so it passes the guard
});
});
describe("60B.43 — terminal post-mutation target is cleared after valid decision closure", () => {
function makeProductLaunchClosureFixture({ includeFallbackUnknown = false } = {}) {
const productLaunchDecision = makeNode({
id: "n_product_launch_decision",
label: "Which option leaves us better off overall?",
description:
"Uncertainty about which of the two product-launch timing options provides superior net value for the organisation.",
kind: "unknown",
status: "unknown",
confidence: "medium",
});
const launchThisYear = makeNode({
id: "opt_launch_this_year",
label: "Launch this year",
description: "Launch the new software product this year.",
kind: "option",
status: "known",
confidence: "high",
});
const waitTwelveMonths = makeNode({
id: "opt_wait_twelve_months",
label: "Wait twelve months",
description: "Wait twelve months before launching the product.",
kind: "option",
status: "known",
confidence: "high",
});
const enterpriseCustomerSigning = makeNode({
id: "n_enterprise_customer_signing",
label: "Prospective enterprise customer signing status",
description:
"Unknown whether one prospective enterprise customer will sign if we launch this year, because they account for approximately £700,000 of the £1.2 million expected annual revenue.",
kind: "unknown",
status: "unknown",
confidence: "medium",
});
const nodes = [
productLaunchDecision,
launchThisYear,
waitTwelveMonths,
enterpriseCustomerSigning,
];
const edges = [
makeEdge({
id: "e-opt-launch-to-dec",
fromNodeId: launchThisYear.id,
toNodeId: productLaunchDecision.id,
relationship: "contained_in",
confidence: "high",
description: "Launch this year option is a candidate for the product launch decision.",
}),
makeEdge({
id: "e-opt-wait-to-dec",
fromNodeId: waitTwelveMonths.id,
toNodeId: productLaunchDecision.id,
relationship: "contained_in",
confidence: "high",
description: "Wait twelve months option is a candidate for the product launch decision.",
}),
makeEdge({
id: "e-customer-signing-to-launch-option",
fromNodeId: enterpriseCustomerSigning.id,
toNodeId: launchThisYear.id,
relationship: "contained_in",
confidence: "high",
description: "Customer signing status is material to launching this year.",
}),
];
if (includeFallbackUnknown) {
const fallbackUnknown = makeNode({
id: "n_other_market_evidence",
label: "Other market evidence gap",
description:
"Need other market evidence because the remaining launch case still depends on it.",
kind: "unknown",
status: "unknown",
confidence: "high",
});
nodes.push(fallbackUnknown);
edges.push(
makeEdge({
id: "e-fallback-to-launch-option",
fromNodeId: fallbackUnknown.id,
toNodeId: launchThisYear.id,
relationship: "may_cause",
confidence: "medium",
description: "Fallback unresolved evidence remains material to launch timing.",
}),
);
}
const graph = makeGraph({
centralStatement:
"We are evaluating two product-launch timing options: launching the new software product this year or waiting twelve months.",
nodes,
edges,
activeUnknownNodeId: enterpriseCustomerSigning.id,
resolvedNodeIds: [],
currentSummary:
"Customer signing is the active material uncertainty in the product-launch decision.",
});
return {
graph,
ids: {
productLaunchDecision: productLaunchDecision.id,
launchThisYear: launchThisYear.id,
waitTwelveMonths: waitTwelveMonths.id,
enterpriseCustomerSigning: enterpriseCustomerSigning.id,
fallbackUnknown: includeFallbackUnknown
? "n_other_market_evidence"
: null,
},
};
}
function makeRichReconciliationFixture() {
const productLaunchDecision = makeNode({
id: "n_product_launch_decision_rich",
label: "Which option leaves us better off overall?",
description:
"Uncertainty about whether launching this year or waiting twelve months leaves the organisation better off overall.",
kind: "unknown",
status: "unknown",
confidence: "medium",
});
const launchThisYear = makeNode({
id: "opt_launch_this_year_rich",
label: "Launch this year",
description:
"Launch this year with approximately £1.2M expected annual revenue and around £300,000 additional support and implementation cost.",
kind: "option",
status: "known",
confidence: "high",
});
const waitTwelveMonths = makeNode({
id: "opt_wait_twelve_months_rich",
label: "Wait twelve months",
description:
"Wait twelve months to reduce immediate support cost and improve the product, but delay revenue and risk competitor movement.",
kind: "option",
status: "known",
confidence: "high",
});
const customerSigningPrimary = makeNode({
id: "n_enterprise_customer_signing_rich",
label: "Prospective enterprise customer signing status",
description:
"Unknown whether the large enterprise customer will sign if we launch this year, because they account for approximately £700,000 of expected annual revenue.",
kind: "unknown",
status: "unknown",
confidence: "medium",
});
const customerSigningSecondary = makeNode({
id: "n_customer_contract_timeline_pending",
label: "Enterprise customer contract status is currently unknown",
description:
"The status of the large enterprise customer's contract is currently unknown, including whether signing is still pending on this year's launch timeline.",
kind: "unknown",
status: "unknown",
confidence: "medium",
});
const customerDecisionTimeline = makeNode({
id: "n_customer_decision_timeline",
label: "When will the enterprise customer make its decision?",
description:
"Unknown when the enterprise customer will make its decision, and the timing could still affect launch planning.",
kind: "unknown",
status: "unknown",
confidence: "medium",
});
const revenueConsequence = makeNode({
id: "n_customer_revenue_consequence",
label: "Whether £700,000 of annual revenue from the enterprise customer will be received",
description:
"Unknown whether the £700,000 annual revenue linked to the enterprise customer will be received if we launch this year.",
kind: "unknown",
status: "unknown",
confidence: "medium",
});
const commercialViability = makeNode({
id: "n_launch_viability_without_anchor_customer",
label: "Is launch commercially viable without the £700,000?",
description:
"Unknown whether launching this year is still commercially viable without the £700,000 enterprise-customer revenue.",
kind: "unknown",
status: "unknown",
confidence: "medium",
});
const competitorRisk = makeNode({
id: "n_competitor_move_first_risk",
label: "Could competitors move first during a 12-month delay?",
description:
"Unknown whether competitors could move first if launch is delayed by twelve months, and this could still materially change the decision.",
kind: "unknown",
status: "unknown",
confidence: "medium",
});
const graph = makeGraph({
centralStatement:
"We need to decide whether there is enough evidence to launch this new software product this year or whether waiting twelve months is safer.",
nodes: [
productLaunchDecision,
launchThisYear,
waitTwelveMonths,
customerSigningPrimary,
customerSigningSecondary,
customerDecisionTimeline,
revenueConsequence,
commercialViability,
competitorRisk,
],
edges: [
makeEdge({
id: "e-rich-launch-to-decision",
fromNodeId: launchThisYear.id,
toNodeId: productLaunchDecision.id,
relationship: "contained_in",
confidence: "high",
description: "Launch-this-year is an option in the decision.",
}),
makeEdge({
id: "e-rich-wait-to-decision",
fromNodeId: waitTwelveMonths.id,
toNodeId: productLaunchDecision.id,
relationship: "contained_in",
confidence: "high",
description: "Wait-twelve-months is an option in the decision.",
}),
makeEdge({
id: "e-rich-primary-customer-to-launch",
fromNodeId: customerSigningPrimary.id,
toNodeId: launchThisYear.id,
relationship: "contained_in",
confidence: "high",
description: "Primary customer-signing uncertainty is material to launching this year.",
}),
makeEdge({
id: "e-rich-secondary-customer-to-launch",
fromNodeId: customerSigningSecondary.id,
toNodeId: launchThisYear.id,
relationship: "contained_in",
confidence: "high",
description: "Secondary customer contract-status uncertainty is also material to launching this year.",
}),
makeEdge({
id: "e-rich-timeline-to-launch",
fromNodeId: customerDecisionTimeline.id,
toNodeId: launchThisYear.id,
relationship: "depends_on",
confidence: "medium",
description: "Launch planning depends in part on when the customer decides.",
}),
makeEdge({
id: "e-rich-revenue-consequence-to-launch",
fromNodeId: revenueConsequence.id,
toNodeId: launchThisYear.id,
relationship: "depends_on",
confidence: "high",
description: "Receiving the £700,000 revenue depends on whether the customer signs.",
}),
makeEdge({
id: "e-rich-viability-to-decision",
fromNodeId: commercialViability.id,
toNodeId: productLaunchDecision.id,
relationship: "depends_on",
confidence: "high",
description: "The decision depends on whether launch is viable without the anchor customer.",
}),
makeEdge({
id: "e-rich-competitor-risk-to-wait",
fromNodeId: competitorRisk.id,
toNodeId: waitTwelveMonths.id,
relationship: "contained_in",
confidence: "high",
description: "Competitor move-first risk is material to the wait option.",
}),
],
activeUnknownNodeId: productLaunchDecision.id,
resolvedNodeIds: [],
currentSummary:
"The decision is open; customer signing is unresolved; customer contract status and decision timing remain represented separately; revenue consequence, commercial viability, and competitor timing remain material uncertainties.",
});
return {
graph,
ids: {
productLaunchDecision: productLaunchDecision.id,
launchThisYear: launchThisYear.id,
waitTwelveMonths: waitTwelveMonths.id,
customerSigningPrimary: customerSigningPrimary.id,
customerSigningSecondary: customerSigningSecondary.id,
customerDecisionTimeline: customerDecisionTimeline.id,
revenueConsequence: revenueConsequence.id,
commercialViability: commercialViability.id,
competitorRisk: competitorRisk.id,
},
};
}
it("accepts the 60B.37-shaped closure proposal, resolves the customer factor, and clears the terminal decision target after mutation", () => {
const { graph, ids } = makeProductLaunchClosureFixture();
const result = applyValidatedProposal({
situationGraph: graph,
proposal: {
updatedNodes: [
{
nodeId: ids.enterpriseCustomerSigning,
previousStatus: "unknown",
newStatus: "resolved",
previousValue: null,
newValue:
"Prospective enterprise customer confirmed they will sign if we launch this year. No other material uncertainty remains.",
reason: "The user directly answered the active customer-signing unknown.",
},
{
nodeId: ids.productLaunchDecision,
previousStatus: "unknown",
newStatus: "known",
previousValue: null,
newValue:
"Launching this year is now known to be the better-supported option.",
reason:
"With customer signing confirmed and no other material uncertainties remaining, the decision is closed.",
},
],
addedNodes: [],
addedEdges: [],
removedEdgeIds: [],
resolvedUnknownNodeIds: [ids.enterpriseCustomerSigning],
affectedNodeIds: [ids.productLaunchDecision],
selectedQuestion: {
nodeId: ids.productLaunchDecision,
question: "What outcome would demonstrate enough value to justify launching?",
reason:
"Regression case: proposal still points at the decision node even though it becomes known in the same turn.",
},
},
answer: "no remaining material uncertainty",
});
expect(result.success).toBe(true);
const resolvedCustomer = result.updatedSituationGraph.nodes.find(
(node) => node.id === ids.enterpriseCustomerSigning,
);
const knownDecision = result.updatedSituationGraph.nodes.find(
(node) => node.id === ids.productLaunchDecision,
);
expect(resolvedCustomer?.status).toBe("resolved");
expect(knownDecision?.status).toBe("known");
expect(result.updatedSituationGraph.nodes.filter((node) => node.id === ids.productLaunchDecision)).toHaveLength(1);
expect(result.updatedSituationGraph.nodes.filter((node) => node.id === ids.launchThisYear)).toHaveLength(1);
expect(result.updatedSituationGraph.nodes.filter((node) => node.id === ids.waitTwelveMonths)).toHaveLength(1);
});
it("discards a proposal-selected target that becomes known and falls back to another genuine unresolved candidate", () => {
const { graph, ids } = makeProductLaunchClosureFixture({
includeFallbackUnknown: true,
});
const result = applyValidatedProposal({
situationGraph: graph,
proposal: {
updatedNodes: [
{
nodeId: ids.enterpriseCustomerSigning,
previousStatus: "unknown",
newStatus: "resolved",
previousValue: null,
newValue: "Customer signing confirmed. No other material uncertainty remains.",
reason: "The active customer-signing uncertainty is resolved.",
},
{
nodeId: ids.productLaunchDecision,
previousStatus: "unknown",
newStatus: "known",
previousValue: null,
newValue: "The decision node itself becomes known.",
reason: "The originally selected target closes during mutation.",
},
],
addedNodes: [],
addedEdges: [],
removedEdgeIds: [],
resolvedUnknownNodeIds: [ids.enterpriseCustomerSigning],
affectedNodeIds: [ids.productLaunchDecision],
selectedQuestion: {
nodeId: ids.productLaunchDecision,
question: "What outcome would demonstrate enough value to justify launching?",
reason: "This target becomes terminal and must be discarded post-mutation.",
},
},
answer: "no remaining material uncertainty",
});
expect(result.success).toBe(true);
// With explicit confirmation, decision closes normally; fallback to next unresolved unknown
// so fallbackUnknown remains active; selectedQuestion falls back correctly.
const productDecisionNode = result.updatedSituationGraph.nodes.find(
(n) => n.id === ids.productLaunchDecision,
);
// With explicit confirmation, decision closes normally; fallback to next unresolved unknown
expect(productDecisionNode?.status).toBe("known");
expect(result.updatedSituationGraph.activeUnknownNodeId).toBe(
ids.fallbackUnknown,
);
expect(result.selectedQuestion?.nodeId).toBe(ids.fallbackUnknown);
expect(result.selectedQuestion?.nodeId).not.toBe(ids.productLaunchDecision);
});
it("60B.81 regression — after materially informative sufficiency answer, a specific remaining material factor should outrank repeated sufficiency confirmation", () => {
const { graph, ids } = makeProductLaunchClosureFixture({
includeFallbackUnknown: true,
});
const previousQuestion =
"Is there anything else material that could change which option is better?";
const answer =
"The enterprise customer has now confirmed in writing that they will not sign if we launch this year, so the £700,000 of expected annual revenue from them will not be received.";
const result = applyValidatedProposal({
situationGraph: graph,
proposal: {
updatedNodes: [
{
nodeId: ids.enterpriseCustomerSigning,
previousStatus: "unknown",
newStatus: "resolved",
previousValue: null,
newValue: answer,
reason:
"The answer definitively resolves the enterprise-customer signing factor with materially new negative evidence.",
},
{
nodeId: ids.launchThisYear,
previousStatus: "known",
newStatus: "known",
previousValue: null,
newValue:
"If we launch this year, the £700,000 expected annual revenue from the enterprise customer will not be received.",
reason:
"Update the launch-this-year option to reflect the resolved financial consequence of the customer not signing.",
},
],
addedNodes: [],
addedEdges: [],
removedEdgeIds: [],
resolvedUnknownNodeIds: [ids.enterpriseCustomerSigning],
affectedNodeIds: [ids.launchThisYear, ids.productLaunchDecision],
selectedQuestion: {
nodeId: ids.productLaunchDecision,
question: previousQuestion,
reason:
"The prior turn asked the bounded sufficiency-confirmation question at the decision level.",
},
},
previousQuestion,
answer,
});
expect(result.success).toBe(true);
const resolvedCustomer = result.updatedSituationGraph.nodes.find(
(node) => node.id === ids.enterpriseCustomerSigning,
);
const remainingFactor = result.updatedSituationGraph.nodes.find(
(node) => node.id === ids.fallbackUnknown,
);
const launchOption = result.updatedSituationGraph.nodes.find(
(node) => node.id === ids.launchThisYear,
);
expect(resolvedCustomer?.status).toBe("resolved");
expect(remainingFactor?.status).toBe("unknown");
expect(result.updatedSituationGraph.resolvedNodeIds).toContain(
ids.enterpriseCustomerSigning,
);
expect(result.updatedSituationGraph.resolvedNodeIds).not.toContain(
ids.productLaunchDecision,
);
expect(result.newActiveUnknownNodeId).toBe(ids.fallbackUnknown);
expect(result.selectedQuestion?.nodeId).toBe(ids.fallbackUnknown);
expect(result.selectedQuestion?.selectedQuestionTemplate).not.toBe(
"decision_threshold_sufficiency_confirmation",
);
expect(result.selectedQuestion?.question).not.toBe(previousQuestion);
expect(launchOption?.value).toContain("£700,000");
expect(launchOption?.value).toContain("will not be received");
});
it("60B.82 richer live regression — definitive customer rejection must supersede stale overlapping customer-unknown state and must not repeat sufficiency question", () => {
const productLaunchDecision = makeNode({
id: "n_product_launch_decision_rich",
label: "Which option leaves us better off overall?",
description:
"Uncertainty about whether launching this year or waiting twelve months leaves the organisation better off overall.",
kind: "unknown",
status: "unknown",
confidence: "medium",
});
const launchThisYear = makeNode({
id: "opt_launch_this_year_rich",
label: "Launch this year",
description:
"Launch this year with approximately £1.2M expected annual revenue and around £300,000 additional support and implementation cost.",
kind: "option",
status: "known",
confidence: "high",
});
const waitTwelveMonths = makeNode({
id: "opt_wait_twelve_months_rich",
label: "Wait twelve months",
description:
"Wait twelve months to reduce immediate support cost and improve the product, but delay revenue and risk competitor movement.",
kind: "option",
status: "known",
confidence: "high",
});
const customerSigningPrimary = makeNode({
id: "n_enterprise_customer_signing_rich",
label: "Prospective enterprise customer signing status",
description:
"Unknown whether the large enterprise customer will sign if we launch this year, because they account for approximately £700,000 of expected annual revenue.",
kind: "unknown",
status: "unknown",
confidence: "medium",
});
const customerSigningSecondary = makeNode({
id: "n_customer_contract_timeline_pending",
label: "Enterprise customer contract timing still pending",
description:
"The status of the large enterprise customer's contract is currently unknown, including whether signing is still pending on this year's launch timeline.",
kind: "unknown",
status: "unknown",
confidence: "medium",
});
const delayingBenefit = makeNode({
id: "n_delay_improvement_benefit",
label: "Quantifiable benefit of delaying launch for product improvements versus immediate market entry",
description:
"Unknown how much extra value twelve months of product improvement would create relative to launching now, and this could still change which option is better.",
kind: "unknown",
status: "unknown",
confidence: "medium",
});
const competitorRisk = makeNode({
id: "n_competitor_move_first_risk",
label: "External firms that could move first if the product is delayed",
description:
"Unknown which external firms could move first if launch is delayed, and the competitive impact could still materially change the decision.",
kind: "unknown",
status: "unknown",
confidence: "medium",
});
const graph = makeGraph({
centralStatement:
"We need to decide whether there is enough evidence to launch this new software product this year or whether waiting twelve months is safer.",
nodes: [
productLaunchDecision,
launchThisYear,
waitTwelveMonths,
customerSigningPrimary,
customerSigningSecondary,
delayingBenefit,
competitorRisk,
],
edges: [
makeEdge({
id: "e-rich-launch-to-decision",
fromNodeId: launchThisYear.id,
toNodeId: productLaunchDecision.id,
relationship: "contained_in",
confidence: "high",
description: "Launch-this-year is an option in the decision.",
}),
makeEdge({
id: "e-rich-wait-to-decision",
fromNodeId: waitTwelveMonths.id,
toNodeId: productLaunchDecision.id,
relationship: "contained_in",
confidence: "high",
description: "Wait-twelve-months is an option in the decision.",
}),
makeEdge({
id: "e-rich-primary-customer-to-launch",
fromNodeId: customerSigningPrimary.id,
toNodeId: launchThisYear.id,
relationship: "contained_in",
confidence: "high",
description: "Primary customer-signing uncertainty is material to launching this year.",
}),
makeEdge({
id: "e-rich-secondary-customer-to-launch",
fromNodeId: customerSigningSecondary.id,
toNodeId: launchThisYear.id,
relationship: "contained_in",
confidence: "high",
description: "Secondary customer contract-status uncertainty is also material to launching this year.",
}),
makeEdge({
id: "e-rich-delay-benefit-to-wait",
fromNodeId: delayingBenefit.id,
toNodeId: waitTwelveMonths.id,
relationship: "contained_in",
confidence: "high",
description: "The quantified benefit of delay is material to the wait option.",
}),
makeEdge({
id: "e-rich-competitor-risk-to-wait",
fromNodeId: competitorRisk.id,
toNodeId: waitTwelveMonths.id,
relationship: "contained_in",
confidence: "high",
description: "Competitor move-first risk is material to the wait option.",
}),
],
activeUnknownNodeId: productLaunchDecision.id,
resolvedNodeIds: [],
currentSummary:
"The decision is open; customer signing is unresolved; delaying-launch benefits and competitor timing remain material uncertainties.",
});
const previousQuestion =
"Is there anything else material that could change which option is better?";
const answer =
"The enterprise customer has now confirmed in writing that they will not sign if we launch this year, so the £700,000 of expected annual revenue from them will not be received.";
const result = applyValidatedProposal({
situationGraph: graph,
proposal: {
updatedNodes: [
{
nodeId: customerSigningPrimary.id,
previousStatus: "unknown",
newStatus: "resolved",
previousValue: null,
newValue: answer,
reason:
"The answer definitively resolves the primary enterprise-customer signing factor with negative evidence.",
},
{
nodeId: launchThisYear.id,
previousStatus: "known",
newStatus: "known",
previousValue: null,
newValue:
"If we launch this year, the £700,000 expected annual revenue from the enterprise customer will not be received.",
reason:
"Reflect the financial impact of the confirmed customer rejection in the launch-this-year option.",
},
],
addedNodes: [],
addedEdges: [],
removedEdgeIds: [],
resolvedUnknownNodeIds: [customerSigningPrimary.id],
affectedNodeIds: [launchThisYear.id, productLaunchDecision.id],
selectedQuestion: {
nodeId: productLaunchDecision.id,
question: previousQuestion,
reason:
"The previous live turn was the sufficiency confirmation question at the decision level.",
},
},
previousQuestion,
answer,
});
expect(result.success).toBe(true);
const primaryCustomerNode = result.updatedSituationGraph.nodes.find(
(node) => node.id === customerSigningPrimary.id,
);
const staleCustomerNode = result.updatedSituationGraph.nodes.find(
(node) => node.id === customerSigningSecondary.id,
);
expect(primaryCustomerNode?.status).toBe("resolved");
expect(primaryCustomerNode?.value).toContain("will not sign");
expect(staleCustomerNode?.status).not.toBe("unknown");
expect(staleCustomerNode?.description?.toLowerCase()).not.toContain(
"currently unknown",
);
expect(staleCustomerNode?.description?.toLowerCase()).not.toContain(
"pending",
);
expect(result.selectedQuestion?.selectedQuestionTemplate).not.toBe(
"decision_threshold_sufficiency_confirmation",
);
expect(result.selectedQuestion?.question).not.toBe(previousQuestion);
expect([
delayingBenefit.id,
competitorRisk.id,
]).toContain(result.selectedQuestion?.nodeId);
});
describe("60B.83 — reconciliation safety envelope before generic implementation", () => {
function applyRichCustomerRejectionUpdate() {
const { graph, ids } = makeRichReconciliationFixture();
const previousQuestion =
"Is there anything else material that could change which option is better?";
const answer =
"The enterprise customer has now confirmed in writing that they will not sign if we launch this year, so the £700,000 of expected annual revenue from them will not be received.";
const result = applyValidatedProposal({
situationGraph: graph,
proposal: {
updatedNodes: [
{
nodeId: ids.customerSigningPrimary,
previousStatus: "unknown",
newStatus: "resolved",
previousValue: null,
newValue: answer,
reason:
"The answer definitively resolves the primary enterprise-customer signing factor with negative evidence.",
},
{
nodeId: ids.launchThisYear,
previousStatus: "known",
newStatus: "known",
previousValue: null,
newValue:
"If we launch this year, the £700,000 expected annual revenue from the enterprise customer will not be received.",
reason:
"Reflect the financial impact of the confirmed customer rejection in the launch-this-year option.",
},
],
addedNodes: [],
addedEdges: [],
removedEdgeIds: [],
resolvedUnknownNodeIds: [ids.customerSigningPrimary],
affectedNodeIds: [ids.launchThisYear, ids.productLaunchDecision],
selectedQuestion: {
nodeId: ids.productLaunchDecision,
question: previousQuestion,
reason:
"The previous live turn was the sufficiency confirmation question at the decision level.",
},
},
previousQuestion,
answer,
});
return { result, ids, previousQuestion, answer };
}
it("1 SAME_PROPOSITION — same-proposition sibling should no longer remain an active unresolved contradiction", () => {
const { result, ids } = applyRichCustomerRejectionUpdate();
expect(result.success).toBe(true);
const primaryNode = result.updatedSituationGraph.nodes.find(
(node) => node.id === ids.customerSigningPrimary,
);
const siblingNode = result.updatedSituationGraph.nodes.find(
(node) => node.id === ids.customerSigningSecondary,
);
expect(primaryNode?.status).toBe("resolved");
expect(siblingNode?.status).not.toBe("unknown");
});
it("2 SAME_ENTITY_DIFFERENT_DIMENSION — timeline node remains unresolved", () => {
const { result, ids } = applyRichCustomerRejectionUpdate();
expect(result.success).toBe(true);
const timelineNode = result.updatedSituationGraph.nodes.find(
(node) => node.id === ids.customerDecisionTimeline,
);
expect(timelineNode?.status).toBe("unknown");
});
it("3 DERIVED_CONSEQUENCE — structurally linked revenue consequence may be updated from explicit dependency semantics", () => {
const { result, ids } = applyRichCustomerRejectionUpdate();
expect(result.success).toBe(true);
const revenueNode = result.updatedSituationGraph.nodes.find(
(node) => node.id === ids.revenueConsequence,
);
expect(revenueNode?.status).not.toBe("unknown");
});
it("4 DOWNSTREAM_DEPENDENCY — commercial viability without the £700k remains unresolved", () => {
const { result, ids } = applyRichCustomerRejectionUpdate();
expect(result.success).toBe(true);
const viabilityNode = result.updatedSituationGraph.nodes.find(
(node) => node.id === ids.commercialViability,
);
expect(viabilityNode?.status).toBe("unknown");
});
it("5 UNRELATED — competitor factor remains unresolved and untouched", () => {
const { result, ids } = applyRichCustomerRejectionUpdate();
expect(result.success).toBe(true);
const competitorNode = result.updatedSituationGraph.nodes.find(
(node) => node.id === ids.competitorRisk,
);
expect(competitorNode?.status).toBe("unknown");
expect(competitorNode?.value ?? null).toBe(null);
});
it("6 QUESTION_CONTINUATION — next question should continue with a specific remaining factor, not repeated sufficiency confirmation", () => {
const { result, ids, previousQuestion } = applyRichCustomerRejectionUpdate();
expect(result.success).toBe(true);
expect([
ids.customerDecisionTimeline,
ids.commercialViability,
ids.competitorRisk,
]).toContain(result.selectedQuestion?.nodeId);
expect(result.selectedQuestion?.selectedQuestionTemplate).not.toBe(
"decision_threshold_sufficiency_confirmation",
);
expect(result.selectedQuestion?.question).not.toBe(previousQuestion);
});
});
it("returns no active target and no final question when only terminal unknowns remain after a valid mutation", () => {
const graph = makeGraph({
centralStatement: "Known-only terminal graph after mutation",
nodes: [
makeNode({
id: "n_known_terminal",
label: "Known terminal unknown",
description: "This unknown-kind node starts unresolved but becomes known.",
kind: "unknown",
status: "unknown",
confidence: "high",
}),
],
edges: [],
activeUnknownNodeId: "n_known_terminal",
resolvedNodeIds: [],
currentSummary: "A single unknown remains before mutation.",
});
const result = applyValidatedProposal({
situationGraph: graph,
proposal: {
updatedNodes: [
{
nodeId: "n_known_terminal",
previousStatus: "unknown",
newStatus: "known",
previousValue: null,
newValue: "The previously active unknown is now known.",
reason: "The mutation closes the final unknown.",
},
],
addedNodes: [],
addedEdges: [],
removedEdgeIds: [],
resolvedUnknownNodeIds: [],
affectedNodeIds: ["n_known_terminal"],
selectedQuestion: {
nodeId: "n_known_terminal",
question: "What remains to be clarified?",
reason: "This target becomes terminal in the same mutation and must be cleared post-mutation.",
},
},
});
expect(result.success).toBe(true);
expect(result.updatedSituationGraph.activeUnknownNodeId).toBeNull();
expect(result.selectedQuestion).toBeNull();
});
});
describe("60B.49 — reconciles updated-to-resolved unknown into resolvedUnknownNodeIds", () => {
it("accepts the 60B.47-shaped proposal by auto-adding the missing resolved unknown ID", () => {
const productLaunchDecision = makeNode({
id: "n_product_launch_decision",
label: "Which option leaves us better off overall?",
description:
"Uncertainty about which of the two product-launch timing options provides superior net value for the organisation.",
kind: "unknown",
status: "unknown",
confidence: "medium",
});
const launchThisYear = makeNode({
id: "opt_launch_this_year",
label: "Launch this year",
description: "Launch the new software product this year.",
kind: "option",
status: "known",
confidence: "high",
});
const waitTwelveMonths = makeNode({
id: "opt_wait_twelve_months",
label: "Wait twelve months",
description: "Wait twelve months before launching the product.",
kind: "option",
status: "known",
confidence: "high",
});
const enterpriseCustomerSigning = makeNode({
id: "n_enterprise_customer_signing",
label: "Prospective enterprise customer signing status",
description:
"Unknown whether one prospective enterprise customer will sign if we launch this year.",
kind: "unknown",
status: "unknown",
confidence: "medium",
});
const graph = makeGraph({
centralStatement:
"We are evaluating two product-launch timing options: launching this year or waiting twelve months.",
nodes: [
productLaunchDecision,
launchThisYear,
waitTwelveMonths,
enterpriseCustomerSigning,
],
edges: [
makeEdge({
id: "e-opt-launch-to-dec",
fromNodeId: launchThisYear.id,
toNodeId: productLaunchDecision.id,
relationship: "contained_in",
confidence: "high",
description: "Launch option belongs to the decision.",
}),
makeEdge({
id: "e-opt-wait-to-dec",
fromNodeId: waitTwelveMonths.id,
toNodeId: productLaunchDecision.id,
relationship: "contained_in",
confidence: "high",
description: "Wait option belongs to the decision.",
}),
],
activeUnknownNodeId: enterpriseCustomerSigning.id,
resolvedNodeIds: [],
currentSummary: "Customer signing is the active uncertainty.",
});
const result = applyValidatedProposal({
situationGraph: graph,
proposal: {
updatedNodes: [
{
nodeId: enterpriseCustomerSigning.id,
previousStatus: "unknown",
newStatus: "resolved",
previousValue: null,
newValue: "Customer confirmed they will not sign this year. No other material uncertainty remains.",
reason: "Negative outcome resolves the customer factor.",
},
{
nodeId: productLaunchDecision.id,
previousStatus: "unknown",
newStatus: "resolved",
previousValue: null,
newValue: "Waiting twelve months is now the resolved decision.",
reason: "Negative customer outcome resolves the timing decision.",
},
],
addedNodes: [],
addedEdges: [],
removedEdgeIds: [],
resolvedUnknownNodeIds: [enterpriseCustomerSigning.id],
affectedNodeIds: [productLaunchDecision.id],
selectedQuestion: {
nodeId: productLaunchDecision.id,
question: "What outcome would demonstrate enough value to justify launching?",
reason: "Closure-shaped proposal contract.",
},
},
answer: "no remaining material uncertainty",
});
expect(result.success).toBe(true);
// With explicit confirmation, parent closure proceeds normally:
expect(
result.updatedSituationGraph.resolvedNodeIds.filter(
(id) => id === enterpriseCustomerSigning.id,
),
).toHaveLength(1);
expect(
result.updatedSituationGraph.resolvedNodeIds.filter(
(id) => id === productLaunchDecision.id,
),
).toHaveLength(1);
expect(
result.updatedSituationGraph.nodes.find(
(node) => node.id === enterpriseCustomerSigning.id,
)?.status,
).toBe("resolved");
expect(
result.updatedSituationGraph.nodes.find(
(node) => node.id === productLaunchDecision.id,
)?.status,
).toBe("resolved");
expect(result.updatedSituationGraph.nodes).toHaveLength(4);
expect(result.updatedSituationGraph.edges).toHaveLength(2);
expect(result.updatedSituationGraph.activeUnknownNodeId).toBeNull();
expect(result.selectedQuestion).toBeNull();
});
it("clears a stale same-turn resolved selectedQuestion and falls back to another genuine unresolved unknown", () => {
const productLaunchDecision = makeNode({
id: "n_product_launch_decision",
label: "Which option leaves us better off overall?",
description: "Uncertainty about which option provides superior net value.",
kind: "unknown",
status: "unknown",
confidence: "medium",
});
const enterpriseCustomerSigning = makeNode({
id: "n_enterprise_customer_signing",
label: "Prospective enterprise customer signing status",
description: "Unknown whether the customer will sign.",
kind: "unknown",
status: "unknown",
confidence: "medium",
});
const fallbackUnknown = makeNode({
id: "n_remaining_unknown",
label: "Remaining unresolved factor",
description: "Need remaining unresolved market evidence.",
kind: "unknown",
status: "unknown",
confidence: "high",
});
const graph = makeGraph({
centralStatement: "Product-launch negative outcome with remaining unresolved factor.",
nodes: [
productLaunchDecision,
enterpriseCustomerSigning,
fallbackUnknown,
],
edges: [],
activeUnknownNodeId: enterpriseCustomerSigning.id,
resolvedNodeIds: [],
currentSummary: "Customer signing is active but another unresolved factor exists.",
});
const result = applyValidatedProposal({
situationGraph: graph,
proposal: {
updatedNodes: [
{
nodeId: enterpriseCustomerSigning.id,
previousStatus: "unknown",
newStatus: "resolved",
previousValue: null,
newValue: "Customer will not sign.",
reason: "Customer factor resolved negatively.",
},
{
nodeId: productLaunchDecision.id,
previousStatus: "unknown",
newStatus: "resolved",
previousValue: null,
newValue: "Decision resolves to wait.",
reason: "Selected target closes in the same proposal.",
},
],
addedNodes: [],
addedEdges: [],
removedEdgeIds: [],
resolvedUnknownNodeIds: [enterpriseCustomerSigning.id],
affectedNodeIds: [productLaunchDecision.id],
selectedQuestion: {
nodeId: productLaunchDecision.id,
question: "What outcome would demonstrate enough value to justify launching?",
reason: "This target becomes stale because it resolves in the same proposal.",
},
},
});
expect(result.success).toBe(true);
expect(result.selectedQuestion?.nodeId).toBe(fallbackUnknown.id);
expect(result.updatedSituationGraph.activeUnknownNodeId).toBe(
fallbackUnknown.id,
);
});
it("preserves forward reconciliation when resolvedUnknownNodeIds is present without an explicit updatedNode", () => {
const fixture = makeComparabilityUpdateFixture();
const result = applyValidatedProposal({
situationGraph: fixture.graph,
proposal: {
addedNodes: [],
updatedNodes: [],
addedEdges: [],
removedEdgeIds: [],
resolvedUnknownNodeIds: [fixture.comparabilityUnknownId],
affectedNodeIds: [],
selectedQuestion: null,
},
});
expect(result.success).toBe(true);
expect(
result.updatedSituationGraph.nodes.find(
(node) => node.id === fixture.comparabilityUnknownId,
)?.status,
).toBe("resolved");
expect(result.updatedSituationGraph.resolvedNodeIds).toContain(
fixture.comparabilityUnknownId,
);
});
it("does not duplicate already-consistent resolved unknown IDs", () => {
const fixture = makeComparabilityUpdateFixture();
const result = applyValidatedProposal({
situationGraph: fixture.graph,
proposal: fixture.proposal,
});
expect(result.success).toBe(true);
expect(
result.updatedSituationGraph.resolvedNodeIds.filter(
(id) => id === fixture.comparabilityUnknownId,
),
).toHaveLength(1);
});
it("does not auto-add non-unknown nodes updated to resolved", () => {
const stateNode = makeNode({
id: "n_state_node",
label: "State node",
description: "A state node that becomes resolved-like in status only.",
kind: "state",
status: "provisional",
confidence: "medium",
});
const graph = makeGraph({
centralStatement: "Non-unknown resolution graph",
nodes: [stateNode],
edges: [],
activeUnknownNodeId: null,
resolvedNodeIds: [],
currentSummary: "Testing non-unknown reconciliation boundary.",
});
const result = applyValidatedProposal({
situationGraph: graph,
proposal: {
addedNodes: [],
updatedNodes: [
{
nodeId: stateNode.id,
previousStatus: "provisional",
newStatus: "resolved",
previousValue: null,
newValue: "State resolved.",
reason: "Non-unknown node update.",
},
],
addedEdges: [],
removedEdgeIds: [],
resolvedUnknownNodeIds: [],
affectedNodeIds: [],
selectedQuestion: null,
},
});
expect(result.success).toBe(true);
expect(result.updatedSituationGraph.resolvedNodeIds).not.toContain(stateNode.id);
});
it("does not auto-add non-resolved statuses for unknown nodes", () => {
const statuses = ["known", "unknown", "contradicted"];
for (const status of statuses) {
const unknownNode = makeNode({
id: `n_status_${status}`,
label: `Unknown ${status}`,
description: `Unknown node updated to ${status}.`,
kind: "unknown",
status: "unknown",
confidence: "medium",
});
const graph = makeGraph({
centralStatement: `Graph for ${status}`,
nodes: [unknownNode],
edges: [],
activeUnknownNodeId: unknownNode.id,
resolvedNodeIds: [],
currentSummary: `Testing ${status} non-addition.`,
});
const result = applyValidatedProposal({
situationGraph: graph,
proposal: {
addedNodes: [],
updatedNodes: [
{
nodeId: unknownNode.id,
previousStatus: "unknown",
newStatus: status,
previousValue: null,
newValue: `${status} value`,
reason: `Update to ${status}.`,
},
],
addedEdges: [],
removedEdgeIds: [],
resolvedUnknownNodeIds: [],
affectedNodeIds: [],
selectedQuestion: null,
},
});
expect(result.success).toBe(true);
expect(result.updatedSituationGraph.resolvedNodeIds).not.toContain(
unknownNode.id,
);
}
});
});
describe("60B.61 — decision remaining-material-factor detection", () => {
// Test 1 — exact 60B.56 containment-only factor
it("returns true when unresolved unknown is contained_in an option of the decision", () => {
const decision = makeNode({
id: "n_decision",
label: "Decision",
kind: "unknown",
status: "unknown",
});
const optA = makeNode({
id: "opt_a",
label: "Option A",
kind: "option",
status: "known",
});
const factor = makeNode({
id: "n_factor",
label: "Material factor",
kind: "unknown",
status: "unknown",
});
const graph = makeGraph({
centralStatement: "test", currentSummary: "Decision context under test.",
nodes: [decision, optA, factor],
edges: [
makeEdge({ fromNodeId: optA.id, toNodeId: decision.id, relationship: "contained_in" }),
makeEdge({ fromNodeId: factor.id, toNodeId: optA.id, relationship: "contained_in" }),
],
});
expect(hasRemainingMaterialFactors(decision.id, graph)).toBe(true);
});
// Test 2 — same factor resolved
it("returns false when the containment-only factor is resolved", () => {
const decision = makeNode({
id: "n_decision",
label: "Decision",
kind: "unknown",
status: "unknown",
});
const optA = makeNode({
id: "opt_a",
label: "Option A",
kind: "option",
status: "known",
});
const factor = makeNode({
id: "n_factor",
label: "Resolved factor",
kind: "unknown",
status: "resolved",
});
const graph = makeGraph({
centralStatement: "test", currentSummary: "Decision context under test.",
nodes: [decision, optA, factor],
edges: [
makeEdge({ fromNodeId: optA.id, toNodeId: decision.id, relationship: "contained_in" }),
makeEdge({ fromNodeId: factor.id, toNodeId: optA.id, relationship: "contained_in" }),
],
});
expect(hasRemainingMaterialFactors(decision.id, graph)).toBe(false);
});
// Test 3 — consequence-linked factor (Route C)
it("returns true when unresolved unknown has affects/may_cause/causes to an option of the decision", () => {
const decision = makeNode({
id: "n_decision",
label: "Decision",
kind: "unknown",
status: "unknown",
});
const optA = makeNode({
id: "opt_a",
label: "Option A",
kind: "option",
status: "known",
});
const factor = makeNode({
id: "n_factor",
label: "Consequence factor",
kind: "unknown",
status: "unknown",
});
const graph = makeGraph({
centralStatement: "test", currentSummary: "Decision context under test.",
nodes: [decision, optA, factor],
edges: [
makeEdge({ fromNodeId: optA.id, toNodeId: decision.id, relationship: "contained_in" }),
makeEdge({ fromNodeId: factor.id, toNodeId: optA.id, relationship: "may_cause" }),
],
});
expect(hasRemainingMaterialFactors(decision.id, graph)).toBe(true);
});
// Test 4 — direct dependency (Route B)
it("returns true when unresolved unknown directly depends_on the decision, and false once resolved", () => {
const decision = makeNode({
id: "n_decision",
label: "Decision",
kind: "unknown",
status: "unknown",
});
const depUnknown = makeNode({
id: "n_dep",
label: "Dependency unknown",
kind: "unknown",
status: "unknown",
});
const graph = makeGraph({
centralStatement: "test", currentSummary: "Decision context under test.",
nodes: [decision, depUnknown],
edges: [
makeEdge({ fromNodeId: depUnknown.id, toNodeId: decision.id, relationship: "depends_on" }),
],
});
expect(hasRemainingMaterialFactors(decision.id, graph)).toBe(true);
// Once resolved
const resolvedGraph = makeGraph({
centralStatement: "test", currentSummary: "Decision context under test.",
nodes: [
{ ...decision },
{ ...depUnknown, status: "resolved" },
],
edges: [
makeEdge({ fromNodeId: depUnknown.id, toNodeId: decision.id, relationship: "depends_on" }),
],
});
expect(hasRemainingMaterialFactors(decision.id, resolvedGraph)).toBe(false);
});
// Test 5 — hierarchy (Route A)
it("returns true when the decision has an unresolved child unknown via parentId", () => {
const childUnknown = makeNode({
id: "n_child",
label: "Child unknown",
kind: "unknown",
status: "unknown",
});
const decision = makeNode({
id: "n_decision",
label: "Parent Decision",
kind: "unknown",
status: "unknown",
childIds: [childUnknown.id],
});
childUnknown.parentId = decision.id;
const graph = makeGraph({
centralStatement: "test", currentSummary: "Decision context under test.",
nodes: [decision, childUnknown],
edges: [],
});
expect(hasRemainingMaterialFactors(decision.id, graph)).toBe(true);
});
// Test 6 — weak relationship excluded
it("returns false when only supports/measures connects unresolved node to decision or option", () => {
const decision = makeNode({
id: "n_decision",
label: "Decision",
kind: "unknown",
status: "unknown",
});
const optA = makeNode({
id: "opt_a",
label: "Option A",
kind: "option",
status: "known",
});
const weakUnknown = makeNode({
id: "n_weak",
label: "Weakly related unknown",
kind: "unknown",
status: "unknown",
});
const graph = makeGraph({
centralStatement: "test", currentSummary: "Decision context under test.",
nodes: [decision, optA, weakUnknown],
edges: [
makeEdge({ fromNodeId: optA.id, toNodeId: decision.id, relationship: "contained_in" }),
makeEdge({ fromNodeId: weakUnknown.id, toNodeId: optA.id, relationship: "supports" }),
],
});
expect(hasRemainingMaterialFactors(decision.id, graph)).toBe(false);
// Also test measures
const graphMeasures = makeGraph({
centralStatement: "test", currentSummary: "Decision context under test.",
nodes: [decision, optA, weakUnknown],
edges: [
makeEdge({ fromNodeId: optA.id, toNodeId: decision.id, relationship: "contained_in" }),
makeEdge({ fromNodeId: weakUnknown.id, toNodeId: optA.id, relationship: "measures" }),
],
});
expect(hasRemainingMaterialFactors(decision.id, graphMeasures)).toBe(false);
});
// Test 7 — parent decision does not self-count
it("returns false when the decision node itself is kind=unknown status=unknown with no subordinate factors", () => {
const decision = makeNode({
id: "n_decision_self",
label: "Decision itself is unknown",
kind: "unknown",
status: "unknown",
});
const graph = makeGraph({
centralStatement: "test", currentSummary: "Decision context under test.",
nodes: [decision],
edges: [],
});
expect(hasRemainingMaterialFactors(decision.id, graph)).toBe(false);
});
// Test 8 — hierarchy child remains unresolved
it("returns true when a hierarchy child unknown stays unresolved", () => {
const childUnknown = makeNode({
id: "n_child_factor",
label: "Child factor",
kind: "unknown",
status: "unknown",
});
const decision = makeNode({
id: "n_parent_dec",
label: "Parent decision",
kind: "unknown",
status: "unknown",
childIds: [childUnknown.id],
});
childUnknown.parentId = decision.id;
const graph = makeGraph({
centralStatement: "hierarchy child remains", currentSummary: "Decision context under test.",
nodes: [decision, childUnknown],
edges: [],
});
expect(hasRemainingMaterialFactors(decision.id, graph)).toBe(true);
});
// Test — status=known excluded (not terminal but also not unresolved)
it("returns false when unknown has status=known", () => {
const decision = makeNode({ id: "n_dec", label: "D", kind: "unknown", status: "unknown" });
const optA = makeNode({ id: "opt_a", label: "O", kind: "option", status: "known" });
const knownUnknown = makeNode({ id: "n_known", label: "Known unknown", kind: "unknown", status: "known" });
const graph = makeGraph({
centralStatement: "test", currentSummary: "Decision context under test.",
nodes: [decision, optA, knownUnknown],
edges: [
makeEdge({ fromNodeId: optA.id, toNodeId: decision.id, relationship: "contained_in" }),
makeEdge({ fromNodeId: knownUnknown.id, toNodeId: optA.id, relationship: "contained_in" }),
],
});
expect(hasRemainingMaterialFactors(decision.id, graph)).toBe(false);
});
// Test — contradicted excluded
it("returns false when unknown has status=contradicted", () => {
const decision = makeNode({ id: "n_dec", label: "D", kind: "unknown", status: "unknown" });
const optA = makeNode({ id: "opt_a", label: "O", kind: "option", status: "known" });
const contradictedUnknown = makeNode({ id: "n_contra", label: "Contradicted unknown", kind: "unknown", status: "contradicted" });
const graph = makeGraph({
centralStatement: "test", currentSummary: "Decision context under test.",
nodes: [decision, optA, contradictedUnknown],
edges: [
makeEdge({ fromNodeId: optA.id, toNodeId: decision.id, relationship: "contained_in" }),
makeEdge({ fromNodeId: contradictedUnknown.id, toNodeId: optA.id, relationship: "contained_in" }),
],
});
expect(hasRemainingMaterialFactors(decision.id, graph)).toBe(false);
});
// Test — non-unknown nodes excluded even if unresolved
it("returns false when only option-level unknowns of other kinds are present", () => {
const decision = makeNode({ id: "n_dec", label: "D", kind: "unknown", status: "unknown" });
const optA = makeNode({ id: "opt_a", label: "O", kind: "option", status: "known" });
const obsNode = makeNode({ id: "n_obs", label: "Observation", kind: "observation", status: "supported" });
const graph = makeGraph({
centralStatement: "test", currentSummary: "Decision context under test.",
nodes: [decision, optA, obsNode],
edges: [
makeEdge({ fromNodeId: optA.id, toNodeId: decision.id, relationship: "contained_in" }),
makeEdge({ fromNodeId: obsNode.id, toNodeId: optA.id, relationship: "measures" }),
],
});
expect(hasRemainingMaterialFactors(decision.id, graph)).toBe(false);
});
// Test 60B.56 sufficiency regression: all factors resolved, decision still unknown
it("returns false after customer-signing factor is resolved (60B.56 sufficiency)", () => {
const decision = makeNode({ id: "n_decision", label: "Decision", kind: "unknown", status: "unknown" });
const optA = makeNode({ id: "opt_a", label: "Option A", kind: "option", status: "known" });
const optB = makeNode({ id: "opt_b", label: "Option B", kind: "option", status: "known" });
const resolvedFactor = makeNode({ id: "n_factor", label: "Resolved factor", kind: "unknown", status: "resolved" });
const graph = makeGraph({
centralStatement: "60B.56 sufficiency: no unresolved factors remain", currentSummary: "Decision context under test.",
nodes: [decision, optA, optB, resolvedFactor],
edges: [
makeEdge({ fromNodeId: optA.id, toNodeId: decision.id, relationship: "contained_in" }),
makeEdge({ fromNodeId: optB.id, toNodeId: decision.id, relationship: "contained_in" }),
makeEdge({ fromNodeId: resolvedFactor.id, toNodeId: optA.id, relationship: "contained_in" }),
],
activeUnknownNodeId: null,
});
expect(hasRemainingMaterialFactors(decision.id, graph)).toBe(false);
});
// Test — arbitrary connectivity excluded
it("returns false when unknown is only connected via non-approved edges to unrelated nodes", () => {
const decision = makeNode({ id: "n_dec", label: "D", kind: "unknown", status: "unknown" });
const optA = makeNode({ id: "opt_a", label: "O", kind: "option", status: "known" });
const farUnknown = makeNode({ id: "n_far", label: "Far unknown", kind: "unknown", status: "unknown" });
const bridgeOpt = makeNode({ id: "opt_bridge", label: "Bridge opt", kind: "option", status: "known" });
const graph = makeGraph({
centralStatement: "test", currentSummary: "Decision context under test.",
nodes: [decision, optA, farUnknown, bridgeOpt],
edges: [
makeEdge({ fromNodeId: optA.id, toNodeId: decision.id, relationship: "contained_in" }),
makeEdge({ fromNodeId: bridgeOpt.id, toNodeId: decision.id, relationship: "contained_in" }),
// farUnknown connected only via 'other' edge — not an approved route
makeEdge({ fromNodeId: farUnknown.id, toNodeId: bridgeOpt.id, relationship: "other" }),
],
});
expect(hasRemainingMaterialFactors(decision.id, graph)).toBe(false);
});
});
// ── 60B.64 — explicit decision sufficiency closure ────────────────
describe("60B.64 — explicit decision sufficiency closure", () => {
function makeClosureDecisionFixture({ includeFallbackUnknown = false } = {}) {
const productLaunchDecision = makeNode({
id: "n_product_launch_decision",
label: "Which option leaves us better off overall?",
description:
"Uncertainty about which of the two product-launch timing options provides superior net value for the organisation.",
kind: "unknown",
status: "unknown",
confidence: "medium",
});
const launchThisYear = makeNode({
id: "opt_launch_this_year",
label: "Launch this year",
description: "Launch the new software product this year.",
kind: "option",
status: "known",
confidence: "high",
});
const waitTwelveMonths = makeNode({
id: "opt_wait_twelve_months",
label: "Wait twelve months",
description: "Wait twelve months before launching the product.",
kind: "option",
status: "known",
confidence: "high",
});
const enterpriseCustomerSigning = makeNode({
id: "n_enterprise_customer_signing",
label: "Prospective enterprise customer signing status",
description:
"Unknown whether one prospective enterprise customer will sign if we launch this year, because they account for approximately £700,000 of the £1.2 million expected annual revenue.",
kind: "unknown",
status: "unknown",
confidence: "medium",
});
const nodes = [
productLaunchDecision,
launchThisYear,
waitTwelveMonths,
enterpriseCustomerSigning,
];
const edges = [
makeEdge({
id: "e-opt-launch-to-dec",
fromNodeId: launchThisYear.id,
toNodeId: productLaunchDecision.id,
relationship: "contained_in",
confidence: "high",
description: "Launch this year option is a candidate for the product launch decision.",
}),
makeEdge({
id: "e-opt-wait-to-dec",
fromNodeId: waitTwelveMonths.id,
toNodeId: productLaunchDecision.id,
relationship: "contained_in",
confidence: "high",
description: "Wait twelve months option is a candidate for the product launch decision.",
}),
makeEdge({
id: "e-customer-signing-to-launch-option",
fromNodeId: enterpriseCustomerSigning.id,
toNodeId: launchThisYear.id,
relationship: "contained_in",
confidence: "high",
description: "Customer signing status is material to launching this year.",
}),
];
if (includeFallbackUnknown) {
const fallbackUnknown = makeNode({
id: "n_other_market_evidence",
label: "Other market evidence gap",
description:
"Need other market evidence because the remaining launch case still depends on it.",
kind: "unknown",
status: "unknown",
confidence: "high",
});
nodes.push(fallbackUnknown);
edges.push(
makeEdge({
id: "e-fallback-to-launch-option",
fromNodeId: fallbackUnknown.id,
toNodeId: launchThisYear.id,
relationship: "may_cause",
confidence: "medium",
description: "Fallback unresolved evidence remains material to launch timing.",
}),
);
}
const graph = makeGraph({
centralStatement:
"We are evaluating two product-launch timing options: launching the new software product this year or waiting twelve months.",
nodes,
edges,
activeUnknownNodeId: enterpriseCustomerSigning.id,
resolvedNodeIds: [],
currentSummary:
"Customer signing is the active material uncertainty in the product-launch decision.",
});
return {
graph,
ids: {
productLaunchDecision: productLaunchDecision.id,
launchThisYear: launchThisYear.id,
waitTwelveMonths: waitTwelveMonths.id,
enterpriseCustomerSigning: enterpriseCustomerSigning.id,
fallbackUnknown: includeFallbackUnknown
? "n_other_market_evidence"
: null,
},
};
}
// ── Test 1 — exact 60B.56 negative closure (raw confirmation present) ──
it("Test 1 — exact 60B.56 wording closes: customer factor resolved, decision = resolved", () => {
const { graph, ids } = makeClosureDecisionFixture();
const result = applyValidatedProposal({
situationGraph: graph,
proposal: {
updatedNodes: [
{
nodeId: ids.enterpriseCustomerSigning,
previousStatus: "unknown",
newStatus: "resolved",
previousValue: null,
newValue:
"No. The enterprise customer has now confirmed in writing that they will not sign if we launch this year, so the £700,000 of expected annual revenue from them will not be received.",
reason: "The active customer-signing unknown is resolved.",
},
],
addedNodes: [],
addedEdges: [],
removedEdgeIds: [],
resolvedUnknownNodeIds: [ids.enterpriseCustomerSigning],
affectedNodeIds: [ids.productLaunchDecision],
},
answer:
"There are no other material uncertainties between launching this year and waiting twelve months.",
});
expect(result.success).toBe(true);
const customerFactor = result.updatedSituationGraph.nodes.find(
(n) => n.id === ids.enterpriseCustomerSigning,
);
const decision = result.updatedSituationGraph.nodes.find(
(n) => n.id === ids.productLaunchDecision,
);
expect(customerFactor?.status).toBe("resolved");
expect(decision?.status).toBe("resolved");
expect(result.updatedSituationGraph.activeUnknownNodeId).toBeNull();
expect(result.selectedQuestion).toBeNull();
// Decision identity preserved — no new nodes/edges added
expect(result.updatedSituationGraph.nodes).toHaveLength(4);
expect(result.updatedSituationGraph.edges).toHaveLength(3);
});
// ── Test 2 — positive-equivalent closure preserved ──
it("Test 2 — bounded paraphrase confirms and closes the decision", () => {
const { graph, ids } = makeClosureDecisionFixture();
const result = applyValidatedProposal({
situationGraph: graph,
proposal: {
updatedNodes: [
{
nodeId: ids.enterpriseCustomerSigning,
previousStatus: "unknown",
newStatus: "resolved",
previousValue: null,
newValue: "Customer signing confirmed for launch this year.",
reason: "The active customer-signing unknown is resolved.",
},
],
addedNodes: [],
addedEdges: [],
removedEdgeIds: [],
resolvedUnknownNodeIds: [ids.enterpriseCustomerSigning],
affectedNodeIds: [ids.productLaunchDecision],
},
answer:
"no remaining material uncertainty exists between the options.",
});
expect(result.success).toBe(true);
const decision = result.updatedSituationGraph.nodes.find(
(n) => n.id === ids.productLaunchDecision,
);
expect(decision?.status).toBe("resolved");
expect(result.updatedSituationGraph.activeUnknownNodeId).toBeNull();
expect(result.selectedQuestion).toBeNull();
});
// ── Test 3 — last factor resolves but no confirmation ──
it("Test 3 — without explicit raw confirmation decision remains open", () => {
const { graph, ids } = makeClosureDecisionFixture();
const result = applyValidatedProposal({
situationGraph: graph,
proposal: {
updatedNodes: [
{
nodeId: ids.enterpriseCustomerSigning,
previousStatus: "unknown",
newStatus: "resolved",
previousValue: null,
newValue: "Customer signing confirmed for launch this year.",
reason: "The active customer-signing unknown is resolved.",
},
],
addedNodes: [],
addedEdges: [],
removedEdgeIds: [],
resolvedUnknownNodeIds: [ids.enterpriseCustomerSigning],
affectedNodeIds: [ids.productLaunchDecision],
},
answer: "The customer has confirmed they will sign.",
});
expect(result.success).toBe(true);
const decision = result.updatedSituationGraph.nodes.find(
(n) => n.id === ids.productLaunchDecision,
);
expect(decision?.status).not.toBe("resolved");
});
// ── Test 4 — another genuine factor remains ──
it("Test 4 — explicit confirmation but remaining factor blocks closure", () => {
const { graph: baseGraph, ids } = makeClosureDecisionFixture({
includeFallbackUnknown: true,
});
const result = applyValidatedProposal({
situationGraph: baseGraph,
proposal: {
updatedNodes: [
{
nodeId: ids.enterpriseCustomerSigning,
previousStatus: "unknown",
newStatus: "resolved",
previousValue: null,
newValue: "Customer signing confirmed.",
reason: "The active customer-signing unknown is resolved.",
},
],
addedNodes: [],
addedEdges: [],
removedEdgeIds: [],
resolvedUnknownNodeIds: [ids.enterpriseCustomerSigning],
affectedNodeIds: [ids.productLaunchDecision],
},
answer:
"There are no other material uncertainties between launching this year and waiting twelve months.",
});
expect(result.success).toBe(true);
const decision = result.updatedSituationGraph.nodes.find(
(n) => n.id === ids.productLaunchDecision,
);
// Should NOT close because fallback unknown remains unresolved
expect(decision?.status).not.toBe("resolved");
});
// ── Test 5 — contradictory confirmation wording rejected ──
it("Test 5 — contradictory wording rejects closure", () => {
const { graph, ids } = makeClosureDecisionFixture();
const result = applyValidatedProposal({
situationGraph: graph,
proposal: {
updatedNodes: [
{
nodeId: ids.enterpriseCustomerSigning,
previousStatus: "unknown",
newStatus: "resolved",
previousValue: null,
newValue: "Customer signing confirmed.",
reason: "The active customer-signing unknown is resolved.",
},
],
addedNodes: [],
addedEdges: [],
removedEdgeIds: [],
resolvedUnknownNodeIds: [ids.enterpriseCustomerSigning],
affectedNodeIds: [ids.productLaunchDecision],
},
answer: "There is still another material uncertainty.",
});
expect(result.success).toBe(true);
const decision = result.updatedSituationGraph.nodes.find(
(n) => n.id === ids.productLaunchDecision,
);
expect(decision?.status).not.toBe("resolved");
});
// ── Test 6 — negated phrase rejected ──
it("Test 6 — negated phrase rejects confirmation", () => {
const { graph, ids } = makeClosureDecisionFixture();
const result = applyValidatedProposal({
situationGraph: graph,
proposal: {
updatedNodes: [
{
nodeId: ids.enterpriseCustomerSigning,
previousStatus: "unknown",
newStatus: "resolved",
previousValue: null,
newValue: "Customer signing confirmed.",
reason: "The active customer-signing unknown is resolved.",
},
],
addedNodes: [],
addedEdges: [],
removedEdgeIds: [],
resolvedUnknownNodeIds: [ids.enterpriseCustomerSigning],
affectedNodeIds: [ids.productLaunchDecision],
},
answer: "I am not saying there are no other material uncertainties.",
});
expect(result.success).toBe(true);
const decision = result.updatedSituationGraph.nodes.find(
(n) => n.id === ids.productLaunchDecision,
);
expect(decision?.status).not.toBe("resolved");
});
// ── Test 7 — bounded supported paraphrase accepted ──
it("Test 7 — narrow equivalent phrase confirms sufficiency", () => {
const { graph, ids } = makeClosureDecisionFixture();
const result = applyValidatedProposal({
situationGraph: graph,
proposal: {
updatedNodes: [
{
nodeId: ids.enterpriseCustomerSigning,
previousStatus: "unknown",
newStatus: "resolved",
previousValue: null,
newValue: "Customer signing confirmed.",
reason: "The active customer-signing unknown is resolved.",
},
],
addedNodes: [],
addedEdges: [],
removedEdgeIds: [],
resolvedUnknownNodeIds: [ids.enterpriseCustomerSigning],
affectedNodeIds: [ids.productLaunchDecision],
},
answer: "No remaining material uncertainty exists between the options.",
});
expect(result.success).toBe(true);
const decision = result.updatedSituationGraph.nodes.find(
(n) => n.id === ids.productLaunchDecision,
);
expect(decision?.status).toBe("resolved");
});
// ── Test 8 — vague completion language excluded ──
it("Test 8 — vague phrases like 'That's it' do not confirm", () => {
const { graph, ids } = makeClosureDecisionFixture();
const result = applyValidatedProposal({
situationGraph: graph,
proposal: {
updatedNodes: [
{
nodeId: ids.enterpriseCustomerSigning,
previousStatus: "unknown",
newStatus: "resolved",
previousValue: null,
newValue: "Customer signing confirmed.",
reason: "The active customer-signing unknown is resolved.",
},
],
addedNodes: [],
addedEdges: [],
removedEdgeIds: [],
resolvedUnknownNodeIds: [ids.enterpriseCustomerSigning],
affectedNodeIds: [ids.productLaunchDecision],
},
answer: "That's it.",
});
expect(result.success).toBe(true);
const decision = result.updatedSituationGraph.nodes.find(
(n) => n.id === ids.productLaunchDecision,
);
expect(decision?.status).not.toBe("resolved");
});
});
// ── 60B.80 — confirmation-gated model closure ownership ─────────────
describe("60B.80 — confirmation-gated model closure ownership", () => {
function makeClosureProposalFixture() {
const decision = makeNode({
id: "n_product_launch_decision",
label: "Which option leaves us better off overall?",
description: "Uncertainty about which product-launch timing provides superior net value.",
kind: "unknown",
status: "unknown",
confidence: "medium",
});
const optA = makeNode({
id: "opt_launch_this_year",
label: "Launch this year",
description: "Launch the new software product this year.",
kind: "option",
status: "known",
confidence: "high",
});
const optB = makeNode({
id: "opt_wait_twelve_months",
label: "Wait twelve months",
description: "Wait twelve months before launching the product.",
kind: "option",
status: "known",
confidence: "high",
});
const customerFactor = makeNode({
id: "n_enterprise_customer_signing",
label: "Prospective enterprise customer signing status",
description: "Unknown whether one prospective enterprise customer will sign.",
kind: "unknown",
status: "unknown",
confidence: "medium",
});
const marketFactor = makeNode({
id: "n_market_timing_factor",
label: "Competing product announcement timing",
description: "Unknown whether a competitor will announce in Q3.",
kind: "unknown",
status: "unknown",
confidence: "medium",
});
const nodes = [decision, optA, optB, customerFactor, marketFactor];
const edges = [
makeEdge({
id: "e-opt-a-to-dec",
fromNodeId: optA.id,
toNodeId: decision.id,
relationship: "contained_in",
confidence: "high",
}),
makeEdge({
id: "e-opt-b-to-dec",
fromNodeId: optB.id,
toNodeId: decision.id,
relationship: "contained_in",
confidence: "high",
}),
makeEdge({
id: "e-customer-to-optA",
fromNodeId: customerFactor.id,
toNodeId: optA.id,
relationship: "may_cause",
confidence: "medium",
}),
makeEdge({
id: "e-market-to-dec",
fromNodeId: marketFactor.id,
toNodeId: decision.id,
relationship: "may_cause",
confidence: "medium",
}),
];
const graph = makeGraph({
centralStatement: "Evaluating two product-launch timing options.",
nodes,
edges,
activeUnknownNodeId: customerFactor.id,
resolvedNodeIds: [],
currentSummary: "Customer signing is the active uncertainty.",
});
return { graph, ids: { decision: decision.id, optA: optA.id, optB: optB.id, customerFactor: customerFactor.id, marketFactor: marketFactor.id } };
}
// ── Test 1 — resolved/null parent closure stripped (no confirmation) ──
it("Test 1 — resolved/null parent closure stripped", () => {
const { graph, ids } = makeClosureProposalFixture();
const result = applyValidatedProposal({
situationGraph: graph,
proposal: {
updatedNodes: [
{
nodeId: ids.customerFactor,
previousStatus: "unknown",
newStatus: "resolved",
previousValue: null,
newValue: "Customer will not sign if we launch this year.",
reason: "Active unknown resolved.",
},
{
nodeId: ids.decision,
previousStatus: "unknown",
newStatus: "resolved",
previousValue: null,
newValue: null,
reason: "All factors resolved.",
},
],
addedNodes: [],
addedEdges: [],
removedEdgeIds: [],
resolvedUnknownNodeIds: [ids.customerFactor, ids.decision],
affectedNodeIds: [ids.decision],
},
answer: "Customer will not sign",
});
expect(result.success).toBe(true);
const customer = result.updatedSituationGraph.nodes.find((n) => n.id === ids.customerFactor);
const decision = result.updatedSituationGraph.nodes.find((n) => n.id === ids.decision);
// Customer resolution preserved
expect(customer?.status).toBe("resolved");
// Decision closure removed
expect(decision?.status).toBe("unknown");
// Decision removed from resolvedUnknownNodeIds — consistency check via graph
const updatedProposal = result.updatedSituationGraph;
expect(updatedProposal.resolvedNodeIds).not.toContain(ids.decision);
expect(updatedProposal.resolvedNodeIds).toContain(ids.customerFactor);
});
// ── Test 2 — known/directional parent closure stripped ──
it("Test 2 — known/directional parent closure stripped", () => {
const { graph, ids } = makeClosureProposalFixture();
const result = applyValidatedProposal({
situationGraph: graph,
proposal: {
updatedNodes: [
{
nodeId: ids.customerFactor,
previousStatus: "unknown",
newStatus: "resolved",
previousValue: null,
newValue: "Customer confirmed.",
reason: "Resolved.",
},
{
nodeId: ids.decision,
previousStatus: "unknown",
newStatus: "known",
previousValue: null,
newValue: "launch this year",
reason: "Directional conclusion.",
},
],
addedNodes: [],
addedEdges: [],
removedEdgeIds: [],
resolvedUnknownNodeIds: [ids.customerFactor, ids.decision],
affectedNodeIds: [ids.decision],
},
answer: "Customer confirmed",
});
expect(result.success).toBe(true);
const customer = result.updatedSituationGraph.nodes.find((n) => n.id === ids.customerFactor);
const decision = result.updatedSituationGraph.nodes.find((n) => n.id === ids.decision);
// Customer resolution preserved
expect(customer?.status).toBe("resolved");
// Parent terminal update removed entirely
expect(decision?.status).toBe("unknown");
// Directional value NOT retained on unknown decision
expect(decision?.value).toBeNull();
});
// ── Test 3 — resolved/directional parent closure stripped ──
it("Test 3 — resolved/directional parent closure stripped", () => {
const { graph, ids } = makeClosureProposalFixture();
const result = applyValidatedProposal({
situationGraph: graph,
proposal: {
updatedNodes: [
{
nodeId: ids.decision,
previousStatus: "unknown",
newStatus: "resolved",
previousValue: null,
newValue: "launch this year",
reason: "Resolved directionally.",
},
],
addedNodes: [],
addedEdges: [],
removedEdgeIds: [],
resolvedUnknownNodeIds: [ids.decision],
affectedNodeIds: [ids.decision],
},
answer: "Launch is better",
});
expect(result.success).toBe(true);
const decision = result.updatedSituationGraph.nodes.find((n) => n.id === ids.decision);
// Terminal parent update removed
expect(decision?.status).toBe("unknown");
// No hybrid unknown + directional value
expect(decision?.value).toBeNull();
});
// ── Test 4 — explicit confirmation allows closure ──
it("Test 4 — explicit confirmation allows closure", () => {
const { graph, ids } = makeClosureProposalFixture();
const result = applyValidatedProposal({
situationGraph: graph,
proposal: {
updatedNodes: [
{
nodeId: ids.customerFactor,
previousStatus: "unknown",
newStatus: "resolved",
previousValue: null,
newValue: "Customer confirmed.",
reason: "Resolved.",
},
{
nodeId: ids.decision,
previousStatus: "unknown",
newStatus: "known",
previousValue: null,
newValue: "launch this year",
reason: "All factors resolved.",
},
],
addedNodes: [],
addedEdges: [],
removedEdgeIds: [],
resolvedUnknownNodeIds: [ids.customerFactor, ids.decision],
affectedNodeIds: [ids.decision],
},
answer: "There are no other material uncertainties remain.",
});
expect(result.success).toBe(true);
const customer = result.updatedSituationGraph.nodes.find((n) => n.id === ids.customerFactor);
const decision = result.updatedSituationGraph.nodes.find((n) => n.id === ids.decision);
// Explicit confirmation permits closure: normalisation strips model's terminal
// update, then deterministic gate re-closes the decision because confirmation is present.
expect(customer?.status).toBe("resolved");
expect(decision?.status).toBe("resolved");
});
// ── Test 5 — ordinary child unknown resolution unaffected ──
it("Test 5 — ordinary child unknown resolution unaffected", () => {
const { graph, ids } = makeClosureProposalFixture();
const result = applyValidatedProposal({
situationGraph: graph,
proposal: {
updatedNodes: [
{
nodeId: ids.marketFactor,
previousStatus: "unknown",
newStatus: "resolved",
previousValue: null,
newValue: "No competitor announcement expected.",
reason: "Resolved by evidence.",
},
{
nodeId: ids.decision,
previousStatus: "unknown",
newStatus: "known",
previousValue: null,
newValue: "launch this year",
reason: "Terminal closure.",
},
],
addedNodes: [],
addedEdges: [],
removedEdgeIds: [],
resolvedUnknownNodeIds: [ids.marketFactor, ids.decision],
affectedNodeIds: [ids.decision],
},
answer: "No competitor announcement expected",
});
expect(result.success).toBe(true);
const market = result.updatedSituationGraph.nodes.find((n) => n.id === ids.marketFactor);
const decision = result.updatedSituationGraph.nodes.find((n) => n.id === ids.decision);
const customer = result.updatedSituationGraph.nodes.find((n) => n.id === ids.customerFactor);
// market is NOT a parent decision (no contained_in from options), so its
// terminal update survives regardless of confirmation absence. Decision IS a parent,
// and without explicit confirmation its closure gets stripped.
// customerFactor is not referenced in this test proposal and stays unknown.
expect(market?.status).toBe("resolved");
expect(decision?.status).toBe("unknown");
});
// ── Test 6 — unrelated option/evidence updates survive ──
it("Test 6 — unrelated option/evidence updates survive", () => {
const { graph, ids } = makeClosureProposalFixture();
const result = applyValidatedProposal({
situationGraph: graph,
proposal: {
updatedNodes: [
{
nodeId: ids.customerFactor,
previousStatus: "unknown",
newStatus: "resolved",
previousValue: null,
newValue: "Customer confirmed.",
reason: "Resolved.",
},
{
nodeId: ids.decision,
previousStatus: "unknown",
newStatus: "known",
previousValue: null,
newValue: "launch this year",
reason: "Terminal closure.",
},
],
addedNodes: [],
addedEdges: [],
removedEdgeIds: [],
resolvedUnknownNodeIds: [ids.customerFactor, ids.decision],
affectedNodeIds: [ids.decision],
},
answer: "Customer confirmed",
});
expect(result.success).toBe(true);
const decision = result.updatedSituationGraph.nodes.find((n) => n.id === ids.decision);
const customer = result.updatedSituationGraph.nodes.find((n) => n.id === ids.customerFactor);
// Customer resolution preserved
expect(customer?.status).toBe("resolved");
// Decision closure stripped (no explicit confirmation phrase in answer)
expect(decision?.status).toBe("unknown");
});
// ── Test 7 — answerMeaning survives normalisation ──
it("Test 7 — answerMeaning survives normalisation", () => {
const { graph, ids } = makeClosureProposalFixture();
const result = applyValidatedProposal({
situationGraph: graph,
proposal: {
updatedNodes: [
{
nodeId: ids.customerFactor,
previousStatus: "unknown",
newStatus: "resolved",
previousValue: null,
newValue: "Customer confirmed.",
reason: "Resolved.",
},
{
nodeId: ids.decision,
previousStatus: "unknown",
newStatus: "known",
previousValue: null,
newValue: "launch this year",
reason: "Terminal closure.",
},
],
answerMeaning: {
userSupportedMeaning: "Customer confirmed they will not sign.",
supportCategory: "other",
},
structuralActionRequired: true,
addedNodes: [],
addedEdges: [],
removedEdgeIds: [],
resolvedUnknownNodeIds: [ids.customerFactor, ids.decision],
affectedNodeIds: [ids.decision],
},
answer: "Customer confirmed",
});
expect(result.success).toBe(true);
const decision = result.updatedSituationGraph.nodes.find((n) => n.id === ids.decision);
const customer = result.updatedSituationGraph.nodes.find((n) => n.id === ids.customerFactor);
// Decision closure stripped (no explicit confirmation phrase in answer)
expect(decision?.status).toBe("unknown");
// Customer resolution preserved
expect(customer?.status).toBe("resolved");
});
// ── Test 8 — resolved bookkeeping remains consistent ──
it("Test 8 — resolved bookkeeping remains consistent", () => {
const { graph, ids } = makeClosureProposalFixture();
const result = applyValidatedProposal({
situationGraph: graph,
proposal: {
updatedNodes: [
{
nodeId: ids.customerFactor,
previousStatus: "unknown",
newStatus: "resolved",
previousValue: null,
newValue: "Customer confirmed.",
reason: "Resolved.",
},
{
nodeId: ids.decision,
previousStatus: "unknown",
newStatus: "resolved",
previousValue: null,
newValue: null,
reason: "Parent closure.",
},
],
addedNodes: [],
addedEdges: [],
removedEdgeIds: [],
resolvedUnknownNodeIds: [ids.customerFactor, ids.decision],
affectedNodeIds: [ids.decision],
},
answer: "Customer confirmed",
});
expect(result.success).toBe(true);
const decision = result.updatedSituationGraph.nodes.find((n) => n.id === ids.decision);
const customer = result.updatedSituationGraph.nodes.find((n) => n.id === ids.customerFactor);
const resolvedIds = result.updatedSituationGraph.resolvedNodeIds;
// No parent in resolvedUnknownNodeIds after stripped closure
expect(resolvedIds).not.toContain(ids.decision);
// Customer remains present
expect(resolvedIds).toContain(ids.customerFactor);
// Decision remains unknown
expect(decision?.status).toBe("unknown");
});
// ── Test 9 — State B becomes reachable ──
it("Test 9 — State B sufficiency question path reached", () => {
const decision = makeNode({
id: "n_product_launch_decision",
label: "Which option leaves us better off overall?",
description: "Uncertainty about which product-launch timing provides superior net value.",
kind: "unknown",
status: "unknown",
confidence: "medium",
});
const optA = makeNode({
id: "opt_launch_this_year",
label: "Launch this year",
description: "Launch the new software product this year.",
kind: "option",
status: "known",
confidence: "high",
});
const optB = makeNode({
id: "opt_wait_twelve_months",
label: "Wait twelve months",
description: "Wait twelve months before launching.",
kind: "option",
status: "known",
confidence: "high",
});
const customerFactor = makeNode({
id: "n_customer_signing",
label: "Enterprise customer signing status",
description: "Unknown whether enterprise customer will sign.",
kind: "unknown",
status: "unknown",
confidence: "medium",
});
const nodes = [decision, optA, optB, customerFactor];
const edgesArr = [
makeEdge({ id: "e1", fromNodeId: optA.id, toNodeId: decision.id, relationship: "contained_in", confidence: "high" }),
makeEdge({ id: "e2", fromNodeId: optB.id, toNodeId: decision.id, relationship: "contained_in", confidence: "high" }),
makeEdge({ id: "e3", fromNodeId: customerFactor.id, toNodeId: optA.id, relationship: "may_cause", confidence: "medium" }),
];
const graph = makeGraph({
centralStatement: "Evaluating product-launch timing.",
nodes,
edges: edgesArr,
activeUnknownNodeId: decision.id,
resolvedNodeIds: [],
currentSummary: "Decision remains unresolved.",
});
// Model tries to close the parent without explicit confirmation
const result = applyValidatedProposal({
situationGraph: graph,
proposal: {
updatedNodes: [
{
nodeId: customerFactor.id,
previousStatus: "unknown",
newStatus: "resolved",
previousValue: null,
newValue: "Customer signed.",
reason: "Resolved.",
},
{
nodeId: decision.id,
previousStatus: "unknown",
newStatus: "resolved",
previousValue: null,
newValue: null,
reason: "All factors resolved — closing parent.",
},
],
addedNodes: [],
addedEdges: [],
removedEdgeIds: [],
resolvedUnknownNodeIds: [customerFactor.id, decision.id],
affectedNodeIds: [decision.id],
},
answer: "Customer signed. That resolves it.",
});
expect(result.success).toBe(true);
const decisionNode = result.updatedSituationGraph.nodes.find((n) => n.id === decision.id);
const customer = result.updatedSituationGraph.nodes.find((n) => n.id === customerFactor.id);
// Decision remains unknown (closure stripped)
expect(decisionNode?.status).toBe("unknown");
// activeUnknownNodeId = the unresolved decision
expect(result.updatedSituationGraph.activeUnknownNodeId).toBe(decision.id);
// Customer resolution preserved
expect(customer?.status).toBe("resolved");
});
// ── Test 10 — ordinary decision_threshold unaffected ──
it("Test 10 — ordinary decision_threshold preserved", () => {
const decision = makeNode({
id: "n_regular_decision",
label: "Which vendor to choose?",
description: "Decision between three vendors.",
kind: "unknown",
status: "unknown",
confidence: "medium",
});
const optA = makeNode({
id: "opt_vendor_a",
label: "Vendor A",
description: "Vendor A option.",
kind: "option",
status: "known",
confidence: "high",
});
const remainingFactor = makeNode({
id: "n_pricing_details",
label: "Remaining pricing details unknown",
description: "Need pricing details from Vendor A.",
kind: "unknown",
status: "unknown",
confidence: "medium",
});
const nodes = [decision, optA, remainingFactor];
const edgesArr = [
makeEdge({ id: "e1", fromNodeId: optA.id, toNodeId: decision.id, relationship: "contained_in", confidence: "high" }),
makeEdge({ id: "e2", fromNodeId: remainingFactor.id, toNodeId: optA.id, relationship: "may_cause", confidence: "medium" }),
];
const graph = makeGraph({
centralStatement: "Evaluating vendor options.",
nodes,
edges: edgesArr,
activeUnknownNodeId: decision.id,
resolvedNodeIds: [],
currentSummary: "Pricing details still unknown.",
});
// Model resolves customer-like factor but not the remaining material factor
const result = applyValidatedProposal({
situationGraph: graph,
proposal: {
updatedNodes: [
{
nodeId: optA.id,
previousStatus: "known",
newStatus: "known",
previousValue: null,
newValue: "Vendor A selected.",
reason: "Directional update only.",
},
],
addedNodes: [],
addedEdges: [],
removedEdgeIds: [],
resolvedUnknownNodeIds: [],
affectedNodeIds: [decision.id],
},
answer: "We've looked at Vendor A thoroughly.",
});
expect(result.success).toBe(true);
// Decision_threshold_outcome behaviour unchanged — decision stays open
const decisionNode = result.updatedSituationGraph.nodes.find((n) => n.id === decision.id);
expect(decisionNode?.status).toBe("unknown");
});
});