From 912680b967e8ccd2c73fee4ccbb5f27e2980812d Mon Sep 17 00:00:00 2001 From: robbond Date: Fri, 14 Aug 2026 19:04:17 +0100 Subject: [PATCH] fix(reasoning): recognise decision in sufficiency question --- lib/graph/question-formulator.js | 1 - tests/graph/question-formulator.test.js | 222 +++++++++++++++++++++++- 2 files changed, 215 insertions(+), 8 deletions(-) diff --git a/lib/graph/question-formulator.js b/lib/graph/question-formulator.js index f9708f4..f5cac5a 100644 --- a/lib/graph/question-formulator.js +++ b/lib/graph/question-formulator.js @@ -2007,7 +2007,6 @@ export function formulateQuestion({ node, graph, context = {} }) { // Detected transiently from existing state; no persisted field required. if ( reasoningPatternSelection.pattern === "decision" && - node.kind !== "unknown" && node.status !== "known" && node.status !== "resolved" && node.status !== "contradicted" && diff --git a/tests/graph/question-formulator.test.js b/tests/graph/question-formulator.test.js index 9478e83..3d3f922 100644 --- a/tests/graph/question-formulator.test.js +++ b/tests/graph/question-formulator.test.js @@ -1105,7 +1105,7 @@ describe("formulateQuestion", () => { id: "n_product_launch_decision", label: "Whether to launch product X", description: "Launch vs not launch decision for product X.", - kind: "state", + kind: "unknown", status: "unknown", confidence: "medium", childIds: [], @@ -1137,7 +1137,7 @@ describe("formulateQuestion", () => { id: "n_product_launch_decision", label: "Whether to launch product X", description: "Launch vs not launch decision for product X.", - kind: "state", + kind: "unknown", status: "unknown", confidence: "medium", }); @@ -1170,7 +1170,7 @@ describe("formulateQuestion", () => { id: "n_product_launch_decision", label: "Whether to launch product X", description: "Launch vs not launch decision.", - kind: "state", + kind: "unknown", status: "unknown", confidence: "medium", childIds: [factor.id], @@ -1201,7 +1201,7 @@ describe("formulateQuestion", () => { id: "n_product_launch_decision", label: "Whether to launch product X", description: "Launch vs not launch decision.", - kind: "state", + kind: "unknown", status: "unknown", confidence: "medium", }); @@ -1261,7 +1261,7 @@ describe("formulateQuestion", () => { id: "n-decision", label: "Build decision", description: "Decision introduced by the answer.", - kind: "state", + kind: "unknown", status: "known", confidence: "medium", childIds: [unknown.id], @@ -1294,7 +1294,7 @@ describe("formulateQuestion", () => { id: "n_product_launch_decision", label: "Whether to launch product X", description: "Launch vs not launch decision.", - kind: "state", + kind: "unknown", status: "unknown", confidence: "medium", childIds: [factor.id], @@ -1325,7 +1325,7 @@ describe("formulateQuestion", () => { id: "n_product_launch_decision", label: "Whether to launch product X", description: "Launch vs not launch decision.", - kind: "state", + kind: "unknown", status: "unknown", confidence: "medium", }); @@ -1342,3 +1342,211 @@ describe("formulateQuestion", () => { expect(result.reason).toContain("material factors"); }); }); + +// ── 60B.75 — real decision detection for sufficiency question ──── + +describe("60B.75 — real decision detection for sufficiency question", () => { + it("Test 1 — actual decision representation reaches State B", () => { + const decision = makeNode({ + id: "n_product_launch_decision", + label: "Which option leaves us better off overall?", + description: + "Uncertainty about which product-launch timing option provides superior net value.", + kind: "unknown", + status: "unknown", + confidence: "medium", + }); + + const graph = makeGraphFor(decision, { nodes: [], edges: [] }); + + expect(hasRemainingMaterialFactors(decision.id, graph)).toBe(false); + + const result = formulateQuestion({ + node: decision, + graph, + context: { resolvedValues: [] }, + }); + + expect(result.questionFamily).toBe("decision_threshold"); + expect(result.selectedQuestionTemplate).toBe( + "decision_threshold_sufficiency_confirmation", + ); + expect(result.question).toContain("anything else material"); + expect(result.reasoningPattern).toBe("decision"); + }); + + it("Test 2 — ordinary unknown factor does NOT trigger State B", () => { + const unknown = makeNode({ + id: "n_evidence_unknown", + label: "Evidence of demand", + description: "Need evidence of demand.", + kind: "unknown", + status: "unknown", + confidence: "medium", + }); + + const graph = makeGraphFor(unknown); + + const result = formulateQuestion({ + node: unknown, + graph, + }); + + expect(result.selectedQuestionTemplate).not.toBe( + "decision_threshold_sufficiency_confirmation", + ); + }); + + it("Test 3 — genuine decision with remaining factor does NOT trigger State B", () => { + const factor = makeNode({ + id: "n_customer_readiness", + label: "Customer readiness level", + description: "How ready the customer is to adopt.", + kind: "unknown", + status: "unknown", + confidence: "medium", + parentId: "n_product_launch_decision", + }); + + const decision = makeNode({ + id: "n_product_launch_decision", + label: "Which option leaves us better off overall?", + description: "Uncertainty about which product-launch timing option provides superior net value.", + kind: "unknown", + status: "unknown", + confidence: "medium", + childIds: [factor.id], + }); + + const graph = makeGraphFor(factor, { + nodes: [decision], + edges: [], + }); + + expect(hasRemainingMaterialFactors(decision.id, graph)).toBe(true); + + const result = formulateQuestion({ + node: decision, + graph, + context: { resolvedValues: [] }, + }); + + expect(result.selectedQuestionTemplate).not.toBe( + "decision_threshold_sufficiency_confirmation", + ); + }); + + it("Test 4 — explicit confirmation still prevents question path", () => { + const decision = makeNode({ + id: "n_product_launch_decision", + label: "Which option leaves us better off overall?", + description: + "Uncertainty about which product-launch timing option provides superior net value.", + kind: "unknown", + status: "unknown", + confidence: "medium", + }); + + const graph = makeGraphFor(decision, { nodes: [], edges: [] }); + + const result = formulateQuestion({ + node: decision, + graph, + context: { + resolvedValues: ["no other material uncertainty remains"], + }, + }); + + expect(result.selectedQuestionTemplate).not.toBe( + "decision_threshold_sufficiency_confirmation", + ); + }); + + it("Test 5 — existing decision_threshold case preserved", () => { + const unknown = makeNode({ + id: "n-commercial", + label: "Uncertainty regarding the commercial value of the product", + description: + "Commercial justification remains unclear because the decision depends on it.", + kind: "unknown", + status: "unknown", + confidence: "high", + parentId: "n-decision", + }); + + const decision = makeNode({ + id: "n-decision", + label: "Build decision", + description: "Decision introduced by the answer.", + kind: "unknown", + status: "known", + confidence: "medium", + childIds: [unknown.id], + }); + + const graph = makeGraphFor(unknown, { + nodes: [decision], + resolvedNodeIds: [decision.id], + }); + + const result = formulateQuestion({ node: unknown, graph }); + + expect(result.questionFamily).toBe("decision_threshold"); + }); + + it("Test 6 — exact 60B.73-style production-shaped fixture", () => { + const optLaunch = makeNode({ + id: "opt_launch_this_year", + label: "Launch this year", + description: "New software product launches this year.", + kind: "unknown", + status: "known", + confidence: "high", + parentId: "n_product_launch_decision", + }); + + const optWait = makeNode({ + id: "opt_wait_twelve_months", + label: "Wait twelve months", + description: "Defer product launch by twelve months.", + kind: "unknown", + status: "known", + confidence: "high", + parentId: "n_product_launch_decision", + }); + + const decision = 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 — launch this year or wait twelve months — provides superior net value for the organisation.", + kind: "unknown", + status: "unknown", + confidence: "medium", + childIds: [optLaunch.id, optWait.id], + }); + + const graph = makeGraphFor(decision, { + nodes: [optLaunch, optWait], + edges: [ + { id: "e-opt-launch-to-dec", fromNodeId: optLaunch.id, toNodeId: decision.id, relationship: "contained_in", confidence: "high", description: "Launch this year option is a candidate for the product launch decision" }, + { id: "e-opt-wait-to-dec", fromNodeId: optWait.id, toNodeId: decision.id, relationship: "contained_in", confidence: "high", description: "Wait twelve months option is a candidate for the product launch decision" }, + ], + }); + + expect(hasRemainingMaterialFactors(decision.id, graph)).toBe(false); + + const result = formulateQuestion({ + node: decision, + graph, + context: { resolvedValues: [] }, + }); + + expect(result.questionFamily).toBe("decision_threshold"); + expect(result.selectedQuestionTemplate).toBe( + "decision_threshold_sufficiency_confirmation", + ); + expect(result.reasoningPattern).toBe("decision"); + expect(result.question).toContain("anything else material"); + }); +});