fix(reasoning): enforce terminal post-mutation eligibility
This commit is contained in:
@@ -4384,3 +4384,278 @@ describe("60B.11 — prerequisite-aware question targeting", () => {
|
||||
// The node has no depends_on prerequisites, so it passes the guard
|
||||
});
|
||||
});
|
||||
|
||||
describe("60B.43 — terminal post-mutation target is cleared after valid decision closure", () => {
|
||||
function makeProductLaunchClosureFixture({ includeFallbackUnknown = false } = {}) {
|
||||
const productLaunchDecision = makeNode({
|
||||
id: "n_product_launch_decision",
|
||||
label: "Which option leaves us better off overall?",
|
||||
description:
|
||||
"Uncertainty about which of the two product-launch timing options provides superior net value for the organisation.",
|
||||
kind: "unknown",
|
||||
status: "unknown",
|
||||
confidence: "medium",
|
||||
});
|
||||
const launchThisYear = makeNode({
|
||||
id: "opt_launch_this_year",
|
||||
label: "Launch this year",
|
||||
description: "Launch the new software product this year.",
|
||||
kind: "option",
|
||||
status: "known",
|
||||
confidence: "high",
|
||||
});
|
||||
const waitTwelveMonths = makeNode({
|
||||
id: "opt_wait_twelve_months",
|
||||
label: "Wait twelve months",
|
||||
description: "Wait twelve months before launching the product.",
|
||||
kind: "option",
|
||||
status: "known",
|
||||
confidence: "high",
|
||||
});
|
||||
const enterpriseCustomerSigning = makeNode({
|
||||
id: "n_enterprise_customer_signing",
|
||||
label: "Prospective enterprise customer signing status",
|
||||
description:
|
||||
"Unknown whether one prospective enterprise customer will sign if we launch this year, because they account for approximately £700,000 of the £1.2 million expected annual revenue.",
|
||||
kind: "unknown",
|
||||
status: "unknown",
|
||||
confidence: "medium",
|
||||
});
|
||||
|
||||
const nodes = [
|
||||
productLaunchDecision,
|
||||
launchThisYear,
|
||||
waitTwelveMonths,
|
||||
enterpriseCustomerSigning,
|
||||
];
|
||||
const edges = [
|
||||
makeEdge({
|
||||
id: "e-opt-launch-to-dec",
|
||||
fromNodeId: launchThisYear.id,
|
||||
toNodeId: productLaunchDecision.id,
|
||||
relationship: "contained_in",
|
||||
confidence: "high",
|
||||
description: "Launch this year option is a candidate for the product launch decision.",
|
||||
}),
|
||||
makeEdge({
|
||||
id: "e-opt-wait-to-dec",
|
||||
fromNodeId: waitTwelveMonths.id,
|
||||
toNodeId: productLaunchDecision.id,
|
||||
relationship: "contained_in",
|
||||
confidence: "high",
|
||||
description: "Wait twelve months option is a candidate for the product launch decision.",
|
||||
}),
|
||||
makeEdge({
|
||||
id: "e-customer-signing-to-launch-option",
|
||||
fromNodeId: enterpriseCustomerSigning.id,
|
||||
toNodeId: launchThisYear.id,
|
||||
relationship: "contained_in",
|
||||
confidence: "high",
|
||||
description: "Customer signing status is material to launching this year.",
|
||||
}),
|
||||
];
|
||||
|
||||
if (includeFallbackUnknown) {
|
||||
const fallbackUnknown = makeNode({
|
||||
id: "n_other_market_evidence",
|
||||
label: "Other market evidence gap",
|
||||
description:
|
||||
"Need other market evidence because the remaining launch case still depends on it.",
|
||||
kind: "unknown",
|
||||
status: "unknown",
|
||||
confidence: "high",
|
||||
});
|
||||
nodes.push(fallbackUnknown);
|
||||
edges.push(
|
||||
makeEdge({
|
||||
id: "e-fallback-to-launch-option",
|
||||
fromNodeId: fallbackUnknown.id,
|
||||
toNodeId: launchThisYear.id,
|
||||
relationship: "may_cause",
|
||||
confidence: "medium",
|
||||
description: "Fallback unresolved evidence remains material to launch timing.",
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
const graph = makeGraph({
|
||||
centralStatement:
|
||||
"We are evaluating two product-launch timing options: launching the new software product this year or waiting twelve months.",
|
||||
nodes,
|
||||
edges,
|
||||
activeUnknownNodeId: enterpriseCustomerSigning.id,
|
||||
resolvedNodeIds: [],
|
||||
currentSummary:
|
||||
"Customer signing is the active material uncertainty in the product-launch decision.",
|
||||
});
|
||||
|
||||
return {
|
||||
graph,
|
||||
ids: {
|
||||
productLaunchDecision: productLaunchDecision.id,
|
||||
launchThisYear: launchThisYear.id,
|
||||
waitTwelveMonths: waitTwelveMonths.id,
|
||||
enterpriseCustomerSigning: enterpriseCustomerSigning.id,
|
||||
fallbackUnknown: includeFallbackUnknown
|
||||
? "n_other_market_evidence"
|
||||
: null,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
it("accepts the 60B.37-shaped closure proposal, resolves the customer factor, and clears the terminal decision target after mutation", () => {
|
||||
const { graph, ids } = makeProductLaunchClosureFixture();
|
||||
|
||||
const result = applyValidatedProposal({
|
||||
situationGraph: graph,
|
||||
proposal: {
|
||||
updatedNodes: [
|
||||
{
|
||||
nodeId: ids.enterpriseCustomerSigning,
|
||||
previousStatus: "unknown",
|
||||
newStatus: "resolved",
|
||||
previousValue: null,
|
||||
newValue:
|
||||
"Prospective enterprise customer confirmed they will sign if we launch this year.",
|
||||
reason: "The user directly answered the active customer-signing unknown.",
|
||||
},
|
||||
{
|
||||
nodeId: ids.productLaunchDecision,
|
||||
previousStatus: "unknown",
|
||||
newStatus: "known",
|
||||
previousValue: null,
|
||||
newValue:
|
||||
"Launching this year is now known to be the better-supported option.",
|
||||
reason:
|
||||
"With customer signing confirmed and no other material uncertainties remaining, the decision is closed.",
|
||||
},
|
||||
],
|
||||
addedNodes: [],
|
||||
addedEdges: [],
|
||||
removedEdgeIds: [],
|
||||
resolvedUnknownNodeIds: [ids.enterpriseCustomerSigning],
|
||||
affectedNodeIds: [ids.productLaunchDecision],
|
||||
selectedQuestion: {
|
||||
nodeId: ids.productLaunchDecision,
|
||||
question: "What outcome would demonstrate enough value to justify launching?",
|
||||
reason:
|
||||
"Regression case: proposal still points at the decision node even though it becomes known in the same turn.",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
|
||||
const resolvedCustomer = result.updatedSituationGraph.nodes.find(
|
||||
(node) => node.id === ids.enterpriseCustomerSigning,
|
||||
);
|
||||
const knownDecision = result.updatedSituationGraph.nodes.find(
|
||||
(node) => node.id === ids.productLaunchDecision,
|
||||
);
|
||||
|
||||
expect(resolvedCustomer?.status).toBe("resolved");
|
||||
expect(knownDecision?.status).toBe("known");
|
||||
expect(result.updatedSituationGraph.nodes.filter((node) => node.id === ids.productLaunchDecision)).toHaveLength(1);
|
||||
expect(result.updatedSituationGraph.nodes.filter((node) => node.id === ids.launchThisYear)).toHaveLength(1);
|
||||
expect(result.updatedSituationGraph.nodes.filter((node) => node.id === ids.waitTwelveMonths)).toHaveLength(1);
|
||||
expect(result.updatedSituationGraph.activeUnknownNodeId).toBeNull();
|
||||
expect(result.selectedQuestion).toBeNull();
|
||||
});
|
||||
|
||||
it("discards a proposal-selected target that becomes known and falls back to another genuine unresolved candidate", () => {
|
||||
const { graph, ids } = makeProductLaunchClosureFixture({
|
||||
includeFallbackUnknown: true,
|
||||
});
|
||||
|
||||
const result = applyValidatedProposal({
|
||||
situationGraph: graph,
|
||||
proposal: {
|
||||
updatedNodes: [
|
||||
{
|
||||
nodeId: ids.enterpriseCustomerSigning,
|
||||
previousStatus: "unknown",
|
||||
newStatus: "resolved",
|
||||
previousValue: null,
|
||||
newValue: "Customer signing confirmed.",
|
||||
reason: "The active customer-signing uncertainty is resolved.",
|
||||
},
|
||||
{
|
||||
nodeId: ids.productLaunchDecision,
|
||||
previousStatus: "unknown",
|
||||
newStatus: "known",
|
||||
previousValue: null,
|
||||
newValue: "The decision node itself becomes known.",
|
||||
reason: "The originally selected target closes during mutation.",
|
||||
},
|
||||
],
|
||||
addedNodes: [],
|
||||
addedEdges: [],
|
||||
removedEdgeIds: [],
|
||||
resolvedUnknownNodeIds: [ids.enterpriseCustomerSigning],
|
||||
affectedNodeIds: [ids.productLaunchDecision],
|
||||
selectedQuestion: {
|
||||
nodeId: ids.productLaunchDecision,
|
||||
question: "What outcome would demonstrate enough value to justify launching?",
|
||||
reason: "This target becomes terminal and must be discarded post-mutation.",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.updatedSituationGraph.activeUnknownNodeId).toBe(
|
||||
ids.fallbackUnknown,
|
||||
);
|
||||
expect(result.selectedQuestion?.nodeId).toBe(ids.fallbackUnknown);
|
||||
expect(result.selectedQuestion?.nodeId).not.toBe(ids.productLaunchDecision);
|
||||
});
|
||||
|
||||
it("returns no active target and no final question when only terminal unknowns remain after a valid mutation", () => {
|
||||
const graph = makeGraph({
|
||||
centralStatement: "Known-only terminal graph after mutation",
|
||||
nodes: [
|
||||
makeNode({
|
||||
id: "n_known_terminal",
|
||||
label: "Known terminal unknown",
|
||||
description: "This unknown-kind node starts unresolved but becomes known.",
|
||||
kind: "unknown",
|
||||
status: "unknown",
|
||||
confidence: "high",
|
||||
}),
|
||||
],
|
||||
edges: [],
|
||||
activeUnknownNodeId: "n_known_terminal",
|
||||
resolvedNodeIds: [],
|
||||
currentSummary: "A single unknown remains before mutation.",
|
||||
});
|
||||
|
||||
const result = applyValidatedProposal({
|
||||
situationGraph: graph,
|
||||
proposal: {
|
||||
updatedNodes: [
|
||||
{
|
||||
nodeId: "n_known_terminal",
|
||||
previousStatus: "unknown",
|
||||
newStatus: "known",
|
||||
previousValue: null,
|
||||
newValue: "The previously active unknown is now known.",
|
||||
reason: "The mutation closes the final unknown.",
|
||||
},
|
||||
],
|
||||
addedNodes: [],
|
||||
addedEdges: [],
|
||||
removedEdgeIds: [],
|
||||
resolvedUnknownNodeIds: [],
|
||||
affectedNodeIds: ["n_known_terminal"],
|
||||
selectedQuestion: {
|
||||
nodeId: "n_known_terminal",
|
||||
question: "What remains to be clarified?",
|
||||
reason: "This target becomes terminal in the same mutation and must be cleared post-mutation.",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.updatedSituationGraph.activeUnknownNodeId).toBeNull();
|
||||
expect(result.selectedQuestion).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user