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

4387 lines
150 KiB
JavaScript

import { describe, expect, it } from "vitest";
import { applyValidatedProposal } from "@/lib/graph/apply-proposal.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
});
});