Adds rejectedProposalSnapshot to orchestrator diagnostics for proposal_compatibility rejections — exposing answerMeaning (userSupportedMeaning, possibleInference), addedNodes structural fields, addedEdges structural fields, updatedNodes summaries, and resolvedUnknownNodeIds. Diagnostic evidence only; does not alter validation, mutation, or error messages. Stage-gated to proposal_compatibility only.
3269 lines
113 KiB
JavaScript
3269 lines
113 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 { 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: "relative_priority_only",
|
|
resolutionGuidance: "must_remain_unresolved",
|
|
},
|
|
},
|
|
});
|
|
|
|
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: "relative_priority_only",
|
|
resolutionGuidance: "must_remain_unresolved",
|
|
},
|
|
},
|
|
});
|
|
|
|
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: "conditional_tradeoff",
|
|
resolutionGuidance: "may_resolve",
|
|
},
|
|
},
|
|
});
|
|
|
|
expect(result.success).toBe(false);
|
|
expect(result.stage).toBe("proposal_compatibility");
|
|
expect(result.errors.join(" ")).toContain("conditional qualification");
|
|
});
|
|
|
|
it("accepts the Experiment 56A supportCategory wording variant and still applies the existing Regression B guard", () => {
|
|
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("conditional qualification");
|
|
});
|
|
|
|
it("accepts the Experiment 56B live wording variants and still applies the existing Regression B guard", () => {
|
|
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("conditional qualification");
|
|
});
|
|
|
|
it("B live-variant 1: negated hard-constraint mention stays conditional rather than explicit 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, 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("conditional qualification");
|
|
expect(result.errors.join(" ")).not.toContain(
|
|
"explicitly stated hard constraint",
|
|
);
|
|
});
|
|
|
|
it("B live-variant 2: default preference plus override stays conditional rather than other", () => {
|
|
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("conditional qualification");
|
|
expect(result.errors.join(" ")).not.toContain(
|
|
"does not clearly establish one of the protected reasoning categories",
|
|
);
|
|
});
|
|
|
|
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.",
|
|
},
|
|
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: "other",
|
|
resolutionGuidance: "may_resolve",
|
|
},
|
|
},
|
|
});
|
|
|
|
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.",
|
|
},
|
|
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,
|
|
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: "other",
|
|
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: "uncertain",
|
|
resolutionGuidance: "must_remain_unresolved",
|
|
},
|
|
},
|
|
});
|
|
|
|
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: "explicit_hard_constraint",
|
|
resolutionGuidance: "must_resolve",
|
|
},
|
|
},
|
|
});
|
|
|
|
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,
|
|
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 safely when answerMeaning does not clearly establish one of the protected categories", () => {
|
|
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(
|
|
"unsupported stronger meaning than answerMeaning.userSupportedMeaning establishes",
|
|
);
|
|
});
|
|
|
|
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:
|
|
"What evidence would clarify how the two observations were measured?",
|
|
});
|
|
expect(result.selectedQuestion?.question.toLowerCase()).not.toMatch(
|
|
/dso|debtor days|receivables turnover|working capital|receivables/,
|
|
);
|
|
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(
|
|
"What evidence would clarify 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(
|
|
"What evidence would clarify 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: "uncertain",
|
|
resolutionGuidance: "may_resolve",
|
|
},
|
|
},
|
|
});
|
|
|
|
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: "uncertain",
|
|
resolutionGuidance: "may_resolve",
|
|
},
|
|
},
|
|
});
|
|
|
|
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: "uncertain",
|
|
resolutionGuidance: "may_resolve",
|
|
},
|
|
},
|
|
});
|
|
|
|
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);
|
|
});
|
|
|
|
// ── 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: "uncertain",
|
|
resolutionGuidance: "may_resolve",
|
|
},
|
|
},
|
|
});
|
|
|
|
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: "uncertain",
|
|
resolutionGuidance: "may_resolve",
|
|
},
|
|
},
|
|
});
|
|
|
|
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: "uncertain",
|
|
resolutionGuidance: "may_resolve",
|
|
},
|
|
},
|
|
});
|
|
|
|
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.",
|
|
);
|
|
});
|
|
});
|