import { describe, expect, it } from "vitest"; import { assessUnknownAtomicity, formulateQuestion, formulateTieResolutionQuestion, selectReasoningPattern, selectInvestigationStrategy, } from "@/lib/graph/question-formulator.js"; import { makeEdge, makeGraph, makeNode } from "@/lib/graph/schema.js"; import { countRemainingMaterialFactors, hasRemainingMaterialFactors, isUserConfirmationOfNoRemainingUncertainty, } from "@/lib/graph/decision-sufficiency.js"; function makeGraphFor(node, extra = {}) { return makeGraph({ centralStatement: extra.centralStatement || "Decision context", nodes: [node, ...(extra.nodes || [])], edges: extra.edges || [], activeUnknownNodeId: node.id, resolvedNodeIds: extra.resolvedNodeIds || [], currentSummary: "Test summary", }); } describe("formulateQuestion", () => { it("atomicity assessment leaves focused unknowns direct and marks broad explanation unknowns composite", () => { const atomicUnknown = makeNode({ id: "n-atomic", label: "Complaint rate denominator", description: "Need the denominator because it directly determines the complaint rate.", kind: "unknown", status: "unknown", confidence: "high", }); const compositeUnknown = makeNode({ id: "n-composite", 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 compositeGraph = makeGraphFor(compositeUnknown, { centralStatement: "Revenue increased by 18%, but cash in the bank fell over the same period.", nodes: [ makeNode({ id: "n-revenue-observation", label: "Revenue increased by 18%.", description: "Revenue increased by 18%.", kind: "observation", status: "supported", confidence: "high", }), 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", }), ], }); expect( assessUnknownAtomicity({ node: atomicUnknown, graph: makeGraphFor(atomicUnknown), }).atomicity, ).toBe("atomic"); expect( assessUnknownAtomicity({ node: compositeUnknown, graph: compositeGraph, }).atomicity, ).toBe("composite"); }); it("commercial viability plus build decision produces a decision-threshold question", () => { 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: "state", status: "known", confidence: "medium", childIds: [unknown.id], value: "Deciding whether to build the product", }); const graph = makeGraphFor(unknown, { nodes: [decision], resolvedNodeIds: [decision.id], }); const result = formulateQuestion({ node: unknown, graph }); expect(result.strategy).toBe("decision_threshold"); expect(result.reasoningPattern).toBe("decision"); expect(result.questionFamily).toBe("decision_threshold"); expect(result.question).toContain("What outcome"); expect(result.question.toLowerCase()).toContain("justify"); }); it("commercial viability does not produce a pricing-first question", () => { const unknown = makeNode({ id: "n-commercial", label: "Commercial viability", description: "Commercial viability remains unresolved because the decision depends on it.", kind: "unknown", status: "unknown", confidence: "high", }); const graph = makeGraphFor(unknown); const result = formulateQuestion({ node: unknown, graph }); expect(result.question.toLowerCase()).not.toContain("price"); expect(result.question.toLowerCase()).not.toContain("pricing"); }); it("undefined term produces a definition question", () => { const unknown = makeNode({ id: "n-term", label: "Success criteria definition", description: "Need a definition of the term because the team uses it inconsistently.", kind: "unknown", status: "unknown", confidence: "medium", }); const result = formulateQuestion({ node: unknown, graph: makeGraphFor(unknown), }); expect(result.strategy).toBe("definition"); expect(result.reasoningPattern).toBe("definition"); expect(result.questionFamily).toBe("definition"); expect(result.question).toMatch(/^What does /); }); it("unsupported claim produces an evidence question", () => { const unknown = makeNode({ id: "n-claim", label: "Demand claim", description: "Need evidence because the claim has not been validated.", kind: "reported_claim", status: "provisional", confidence: "low", }); const result = formulateQuestion({ node: unknown, graph: makeGraphFor(unknown), }); expect(result.strategy).toBe("evidence_gathering"); expect(result.question).toContain("What evidence"); }); it("missing previous state produces a baseline question", () => { const unknown = makeNode({ id: "n-baseline", label: "Baseline conversion rate", description: "Need the previous baseline because the change cannot be assessed without it.", kind: "unknown", status: "unknown", confidence: "medium", }); const result = formulateQuestion({ node: unknown, graph: makeGraphFor(unknown), }); expect(result.strategy).toBe("baseline_reconstruction"); expect(result.question).toContain("What was the comparable state before"); }); it("conflicting claim produces a contradiction-resolution question", () => { const unknown = makeNode({ id: "n-conflict", label: "Conflicting churn claim", description: "Need to resolve the inconsistency because the current figures contradict each other.", kind: "unknown", status: "unknown", confidence: "medium", }); const contradiction = makeNode({ id: "n-contradiction", label: "Contradicted report", description: "Two sources disagree about churn.", kind: "conclusion", status: "contradicted", confidence: "low", childIds: [unknown.id], }); const result = formulateQuestion({ node: unknown, graph: makeGraphFor(unknown, { nodes: [contradiction] }), }); expect(result.strategy).toBe("contradiction_resolution"); expect(result.reasoningPattern).toBe("contradiction"); expect(result.question).toContain("resolve the contradiction"); }); it("reasoning pattern selection marks commercial validation as decision rather than explanation", () => { const unknown = makeNode({ id: "n-commercial-pattern", label: "Whether the method addresses a genuine, high-priority problem for a specific audience", description: "Need to know whether this solves a real problem for a clear audience before continuing development.", kind: "unknown", status: "unknown", confidence: "high", }); const graph = makeGraphFor(unknown, { centralStatement: "Before investing more, we need to know whether continuing development is commercially justified.", }); const result = selectReasoningPattern({ node: unknown, graph }); expect(result.pattern).toBe("decision"); }); it("comparison scenario selects the comparison pattern", () => { const unknown = makeNode({ id: "n-comparison-pattern", label: "How the two observations were measured", description: "Need evidence about the measure used for each observation, because that could help explain the difference.", kind: "unknown", status: "unknown", confidence: "medium", }); const graph = makeGraphFor(unknown, { centralStatement: "Traffic increased, but sales stayed flat.", nodes: [ makeNode({ id: "n-traffic-observation", label: "Traffic increased.", description: "Traffic increased.", kind: "observation", status: "supported", confidence: "high", }), makeNode({ id: "n-sales-observation", label: "Sales stayed flat.", description: "Sales stayed flat.", kind: "observation", status: "supported", confidence: "high", }), ], }); const result = selectReasoningPattern({ node: unknown, graph }); expect(result.pattern).toBe("comparison"); }); it("user-owned constraint ambiguity produces a clarification question rather than an evidence request", () => { const unknown = makeNode({ id: "n-constraint", label: "Whether avoiding additional 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: "medium", }); const result = formulateQuestion({ node: unknown, graph: makeGraphFor(unknown), }); expect(result.strategy).toBeNull(); expect(result.question).toBe( "Is avoiding additional risk a hard constraint or a preference/trade-off?", ); }); it("60B.21 signing-status node does not route to decision_audience and preserves the proposition", () => { const unknown = makeNode({ id: "n-customer-signing-status", label: "Prospective enterprise customer signing status", description: "Uncertainty about whether the prospective enterprise customer will sign if we launch this year, so that its resolution is needed to decide which timing option provides superior net value.", kind: "unknown", status: "unknown", confidence: "medium", }); const graph = makeGraphFor(unknown, { centralStatement: "We need to decide which launch timing option provides superior net value.", }); const result = formulateQuestion({ node: unknown, graph }); expect(result.questionFamily).not.toBe("decision_foundation"); expect(result.selectedQuestionTemplate).not.toBe("decision_audience"); expect(result.question.toLowerCase()).toContain( "whether the prospective enterprise customer will sign if we launch this year", ); expect(result.question.toLowerCase()).not.toContain( "relevant customer, user, or value recipient", ); }); it("legitimate audience identity unknown still routes to decision_audience", () => { const unknown = makeNode({ id: "n-target-customer", label: "Who is the target customer?", description: "Need to know who the target customer is before continuing development.", kind: "unknown", status: "unknown", confidence: "medium", }); const graph = makeGraphFor(unknown, { centralStatement: "Before investing more, we need to know whether continuing development is commercially justified.", }); const result = formulateQuestion({ node: unknown, graph }); expect(result.questionFamily).toBe("decision_foundation"); expect(result.selectedQuestionTemplate).toBe("decision_audience"); expect(result.question).toBe("Who experiences this problem?"); }); it("customer as proposition subject does not imply audience family", () => { const unknown = makeNode({ id: "n-customer-renewal", label: "Customer renewal likelihood", description: "Uncertainty about whether the customer will renew next year, because that affects the decision.", kind: "unknown", status: "unknown", confidence: "medium", }); const result = formulateQuestion({ node: unknown, graph: makeGraphFor(unknown), }); expect(result.selectedQuestionTemplate).not.toBe("decision_audience"); expect(result.question.toLowerCase()).toContain( "whether the customer will renew next year", ); }); it("user as proposition subject does not imply audience family", () => { const unknown = makeNode({ id: "n-user-adoption", label: "User adoption uncertainty", description: "Uncertainty about whether users will adopt the change, because that affects the decision.", kind: "unknown", status: "unknown", confidence: "medium", }); const result = formulateQuestion({ node: unknown, graph: makeGraphFor(unknown), }); expect(result.selectedQuestionTemplate).not.toBe("decision_audience"); expect(result.question.toLowerCase()).toContain( "whether users will adopt the change", ); }); it("buyer or stakeholder lexical mentions do not automatically trigger audience discovery", () => { const unknown = makeNode({ id: "n-stakeholder-approval", label: "Stakeholder approval uncertainty", description: "Uncertainty about whether the stakeholder will approve the plan, because that affects the decision.", kind: "unknown", status: "unknown", confidence: "medium", }); const result = formulateQuestion({ node: unknown, graph: makeGraphFor(unknown), }); expect(result.selectedQuestionTemplate).not.toBe("decision_audience"); expect(result.question.toLowerCase()).toContain( "whether the stakeholder will approve the plan", ); }); it("existing direct interrogative decision behaviour is preserved", () => { const unknown = makeNode({ id: "n-client-leave", label: "Will our largest client leave if we relocate?", description: "Uncertainty regarding whether our largest client would depart following a relocation to Manchester; matters because their departure would cost approximately £5M per year.", kind: "unknown", status: "unknown", confidence: "medium", }); const graph = makeGraphFor(unknown, { centralStatement: "We need to decide whether relocating is commercially justified.", }); const result = formulateQuestion({ node: unknown, graph }); expect(result.question.toLowerCase()).toBe( "will our largest client leave if we relocate?", ); }); it("60B.24 regression strips whether-rationale and uses evidence framing without mutating the source node", () => { const description = "Whether one prospective enterprise customer will sign if we launch this year; they account for ~£700k of the £1.2M expected annual revenue, so that resolving their intent is needed to assess the financial impact of launching now."; const unknown = makeNode({ id: "n-60b24-signing-status", label: "Prospective enterprise customer signing status", description, kind: "unknown", status: "unknown", confidence: "medium", }); const graph = makeGraphFor(unknown, { centralStatement: "We need to decide which launch timing option provides superior net value.", }); const result = formulateQuestion({ node: unknown, graph }); expect(result.question).toContain( "whether one prospective enterprise customer will sign if we launch this year", ); expect(result.question).toContain("What evidence would clarify"); expect(result.question).not.toContain("£700k"); expect(result.question).not.toContain("£1.2M"); expect(result.question).not.toContain("resolving their intent"); expect(result.question).not.toContain("financial impact"); expect(unknown.description).toBe(description); }); it("clean whether proposition uses evidence framing rather than a direct whether-question", () => { const unknown = makeNode({ id: "n-supplier-renewal", label: "Supplier renewal likelihood", description: "Whether the supplier will renew the contract.", kind: "unknown", status: "unknown", confidence: "medium", }); const result = formulateQuestion({ node: unknown, graph: makeGraphFor(unknown), }); expect(result.question).toBe( "What evidence would clarify whether the supplier will renew the contract?", ); expect(result.question).not.toBe( "Whether the supplier will renew the contract?", ); }); it("wh-question remains unchanged", () => { const unknown = makeNode({ id: "n-wh-question", label: "What would change the preferred option?", description: "What would change the preferred option?", kind: "unknown", status: "unknown", confidence: "medium", }); const result = formulateQuestion({ node: unknown, graph: makeGraphFor(unknown), }); expect(result.question).toBe("What would change the preferred option?"); }); it("non-whether semicolon content is not globally truncated", () => { const unknown = makeNode({ id: "n-semicolon-baseline", label: "Unknown baseline measurement basis; current and previous figures were prepared differently.", description: "Baseline measurement basis; current and previous figures were prepared differently.", kind: "unknown", status: "unknown", confidence: "medium", }); const result = formulateQuestion({ node: unknown, graph: makeGraphFor(unknown), }); expect(result.question).toContain( "baseline measurement basis; current and previous figures were prepared differently", ); expect(result.question).not.toContain("What would clarify whether"); }); it("evidence-resolvable competing-cause unknown stays on an evidence route rather than neutral clarification", () => { const unknown = makeNode({ id: "n-delivery-cause", label: "Possible causes of the delivery delay", description: "Need to determine whether staff capacity or supplier lead times are responsible for the delivery delay.", kind: "unknown", status: "unknown", confidence: "medium", }); const result = formulateQuestion({ node: unknown, graph: makeGraphFor(unknown, { centralStatement: "Delivery is delayed and the cause is still unknown.", }), }); expect(result.reasoningPattern).toBe("diagnosis"); expect(result.question).toContain("What evidence"); expect(result.question).not.toContain("What would clarify"); }); it("the same unknown can produce different questions when paired with different strategies", () => { const thresholdUnknown = makeNode({ id: "n-threshold-unknown", label: "Value threshold", description: "Need to resolve the value threshold.", kind: "unknown", status: "unknown", confidence: "high", }); const definitionUnknown = makeNode({ id: "n-definition-unknown", label: "Value term", description: "Need to resolve what value term refers to in this context.", kind: "unknown", status: "unknown", confidence: "high", }); const decisionGraph = makeGraphFor(thresholdUnknown, { centralStatement: "We are deciding whether to launch this product.", nodes: [ makeNode({ id: "n-decision", label: "Launch decision", description: "Decision depends on the value threshold.", kind: "state", status: "known", confidence: "medium", childIds: [thresholdUnknown.id], value: "Deciding whether to launch the product", }), ], }); const definitionGraph = makeGraphFor(definitionUnknown, { centralStatement: "The team uses the term value threshold inconsistently.", nodes: [ makeNode({ id: "n-definition", label: "Definition disagreement about value threshold", description: "Need a definition of value threshold because the term is used inconsistently before comparing options.", kind: "state", status: "known", confidence: "medium", childIds: [definitionUnknown.id], }), ], }); const decisionResult = formulateQuestion({ node: thresholdUnknown, graph: decisionGraph, }); const definitionResult = formulateQuestion({ node: definitionUnknown, graph: definitionGraph, }); expect(decisionResult.strategy).toBe("decision_threshold"); expect(definitionResult.strategy).toBe("definition"); expect(decisionResult.question).not.toBe(definitionResult.question); }); it("strategy selection is deterministic and explainable", () => { const unknown = makeNode({ id: "n-threshold", label: "Success threshold", description: "Need the success threshold because the decision depends on it.", kind: "unknown", status: "unknown", confidence: "high", }); const graph = makeGraphFor(unknown, { centralStatement: "We need to decide whether to continue investing.", }); const first = selectInvestigationStrategy({ node: unknown, graph }); const second = selectInvestigationStrategy({ node: unknown, graph }); expect(first).toEqual(second); expect(first.key).toBe("decision_threshold"); expect(first.reason).toContain("threshold"); }); it("question is singular and answerable", () => { const unknown = makeNode({ id: "n-evidence", label: "Evidence of demand", description: "Need evidence of demand because the decision depends on it.", kind: "unknown", status: "unknown", confidence: "medium", }); const result = formulateQuestion({ node: unknown, graph: makeGraphFor(unknown), }); expect(result.question.match(/\?/g) || []).toHaveLength(1); expect(result.question.toLowerCase()).not.toContain(" and "); }); it("awkward uncertainty phrasing is rejected via fallback", () => { const unknown = makeNode({ id: "n-weird", label: "Uncertainty regarding service reliability", description: "Unknown service reliability.", kind: "unknown", status: "unknown", confidence: "medium", }); const result = formulateQuestion({ node: unknown, graph: makeGraphFor(unknown), }); expect(result.question).not.toContain("How should uncertainty regarding"); expect(result.question).not.toContain( "What would resolve uncertainty regarding", ); }); it("ambiguous contradiction produces a broad distinguishing question without accounting jargon", () => { const unknown = makeNode({ id: "n-cause-a", label: "Cash outflow cause", description: "Unclear explanation for the contradiction.", kind: "unknown", status: "unknown", confidence: "high", }); const contradiction = makeNode({ id: "n-contradiction", label: "Divergent movement between revenue and cash", description: "Two signals moved in opposite directions.", kind: "relationship", status: "supported", confidence: "medium", }); 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 graph = makeGraphFor(unknown, { centralStatement: "Revenue increased by 18%, but cash in the bank fell over the same period.", nodes: [contradiction, revenueObservation, cashObservation], }); const result = formulateTieResolutionQuestion({ graph }); expect(result.question).toBe( "Were these figures measured on the same basis and at the same scale?", ); expect(result.comparabilityStatus).toBe("uncertain"); expect(result.question.toLowerCase()).not.toMatch( /accounts receivable|capex|debt repayments|working capital/, ); }); it("definition is selected only for genuine definition unknowns", () => { const unknown = makeNode({ id: "n-definition-only", label: "Definition of success criteria", description: "The term is used inconsistently and needs a definition.", kind: "unknown", status: "unknown", confidence: "medium", }); const result = formulateQuestion({ node: unknown, graph: makeGraphFor(unknown), }); expect(result.strategy).toBe("definition"); }); it("an unknown about possible causes does not become a definition question", () => { const unknown = makeNode({ id: "n-causes", label: "Possible causes of the divergence", description: "Several causes may explain the divergence.", kind: "unknown", status: "unknown", confidence: "medium", }); const result = formulateQuestion({ node: unknown, graph: makeGraphFor(unknown), }); expect(result.reasoningPattern).toBe("diagnosis"); expect(result.strategy).toBeNull(); expect(result.question).toBe( "What would clarify possible causes of the divergence in this situation?", ); }); it("malformed punctuation is rejected", () => { const unknown = makeNode({ id: "n-punct", label: "Magnitude and nature of cash outflows (operating expenses).", description: "Magnitude and nature of cash outflows (operating expenses).", kind: "unknown", status: "unknown", confidence: "medium", }); const result = formulateQuestion({ node: unknown, graph: makeGraphFor(unknown), }); expect(result.question).not.toContain("). is true?"); expect(result.question).toBe( "What would clarify magnitude and nature of cash outflows (operating expenses) in this situation?", ); }); // ── 60B.28 / 60B.30: uncertainty-prefix coverage ─────────────────── it("60B.27 regression: 'Unknown whether...' preserves full proposition", () => { const unknown = makeNode({ id: "unc-60b27", label: "Prospective enterprise customer signing likelihood", description: "Unknown whether one prospective enterprise customer will sign if we launch this year, so that the remaining £700k of the expected £1.2M annual revenue is realized.", kind: "unknown", status: "unknown", confidence: "medium", }); const graph = makeGraphFor(unknown, { centralStatement: "We need to decide which launch timing option provides superior net value.", }); const result = formulateQuestion({ node: unknown, graph }); expect(result.question).toBe( "What evidence would clarify whether one prospective enterprise customer will sign if we launch this year?", ); // Rationale must NOT leak into the final question expect(result.question).not.toContain("£700k"); expect(result.question).not.toContain("£1.2M"); expect(result.question.toLowerCase()).not.toContain("annual revenue"); }); it("'Uncertain whether...' exposes the full proposition with evidence framing", () => { const unknown = makeNode({ id: "unc-uncertain", label: "Supplier renewal uncertainty", description: "Uncertain whether the supplier will renew, because the contract is still under review.", kind: "unknown", status: "unknown", confidence: "medium", }); const result = formulateQuestion({ node: unknown, graph: makeGraphFor(unknown), }); expect(result.question).toBe( "What evidence would clarify whether the supplier will renew?", ); }); it("60B.29 regression: 'Uncertainty over whether...' preserves full proposition", () => { const unknown = makeNode({ id: "unc-60b29", label: "Prospective enterprise customer signing status", description: "Uncertainty over whether the prospective enterprise customer will sign if we launch this year, because their contract accounts for approximately £700,000 of the expected first-year revenue and could materially flip the net-value comparison.", kind: "unknown", status: "unknown", confidence: "medium", }); const graph = makeGraphFor(unknown, { centralStatement: "We need to decide which launch timing option provides superior net value.", }); const result = formulateQuestion({ node: unknown, graph }); expect(result.question).toBe( "What evidence would clarify whether the prospective enterprise customer will sign if we launch this year?", ); expect(result.question).not.toContain("£700,000"); expect(result.question).not.toContain("expected first-year revenue"); expect(result.question).not.toContain("materially flip"); expect(result.question).not.toContain("because their contract"); }); it("'Uncertainty over whether...' without rationale uses evidence framing", () => { const unknown = makeNode({ id: "unc-over", label: "Supplier renewal status", description: "Uncertainty over whether the supplier will renew.", kind: "unknown", status: "unknown", confidence: "medium", }); const result = formulateQuestion({ node: unknown, graph: makeGraphFor(unknown), }); expect(result.question).toBe( "What evidence would clarify whether the supplier will renew?", ); }); it("60B.31 regression: bare 'Whether...' description defines proposition even with nominal label", () => { const unknown = makeNode({ id: "unc-60b31", label: "Enterprise customer signing decision", description: "Whether the prospective enterprise customer will commit this year, because resolving this uncertainty is needed to decide if launching this year provides superior net value over waiting twelve months.", kind: "unknown", status: "unknown", confidence: "medium", }); const graph = makeGraphFor(unknown, { centralStatement: "We need to decide whether launching this year provides superior net value over waiting twelve months.", }); const result = formulateQuestion({ node: unknown, graph }); expect(result.question).toBe( "What evidence would clarify whether the prospective enterprise customer will commit this year?", ); expect(result.question).not.toContain("launching"); expect(result.question).not.toContain("superior net value"); expect(result.question).not.toContain("waiting twelve months"); expect(result.question).not.toContain("because"); }); it("bare 'Whether...' with unrelated nominal label remains proposition-specific", () => { const unknown = makeNode({ id: "unc-bare-nominal", label: "Supplier contract decision", description: "Whether the supplier will renew the contract.", kind: "unknown", status: "unknown", confidence: "medium", }); const result = formulateQuestion({ node: unknown, graph: makeGraphFor(unknown), }); expect(result.question).toBe( "What evidence would clarify whether the supplier will renew the contract?", ); }); it("bare 'Whether...' remains unchanged (existing behaviour)", () => { const unknown = makeNode({ id: "unc-bare-whether", label: "Supplier renewal likelihood", description: "Whether the supplier will renew the contract.", kind: "unknown", status: "unknown", confidence: "medium", }); const result = formulateQuestion({ node: unknown, graph: makeGraphFor(unknown), }); expect(result.question).toBe( "What evidence would clarify whether the supplier will renew the contract?", ); }); it("'Uncertainty about whether...' remains unchanged (existing behaviour)", () => { const unknown = makeNode({ id: "unc-about", label: "Customer renewal likelihood", description: "Uncertainty about whether the customer will renew next year, because that affects the decision.", kind: "unknown", status: "unknown", confidence: "medium", }); const result = formulateQuestion({ node: unknown, graph: makeGraphFor(unknown), }); expect(result.question.toLowerCase()).toContain( "whether the customer will renew next year", ); }); it("'Uncertainty regarding whether...' remains unchanged (existing behaviour)", () => { const unknown = makeNode({ id: "unc-regarding", label: "Client retention uncertainty", description: "Uncertainty regarding whether our largest client would depart following a relocation to Manchester; matters because their departure would cost approximately £5M per year.", kind: "unknown", status: "unknown", confidence: "medium", }); const graph = makeGraphFor(unknown, { centralStatement: "We need to decide whether relocating is commercially justified.", }); const result = formulateQuestion({ node: unknown, graph }); expect(result.question.toLowerCase()).toContain( "whether our largest client would depart following a relocation to manchester", ); }); it("direct interrogative label remains direct", () => { const unknown = makeNode({ id: "unc-direct", label: "Will our largest client leave if we relocate?", description: "Uncertainty regarding whether our largest client would depart following a relocation to Manchester; matters because their departure would cost approximately £5M per year.", kind: "unknown", status: "unknown", confidence: "medium", }); const graph = makeGraphFor(unknown, { centralStatement: "We need to decide whether relocating is commercially justified.", }); const result = formulateQuestion({ node: unknown, graph }); expect(result.question).toBe( "Will our largest client leave if we relocate?", ); }); it("generic non-proposition decision unknown remains on its existing non-proposition path", () => { const unknown = makeNode({ id: "unc-generic-decision", label: "Launch decision value threshold", description: "Need to determine what outcome would be sufficient to justify launching this year.", kind: "unknown", status: "unknown", confidence: "medium", }); const result = formulateQuestion({ node: unknown, graph: makeGraphFor(unknown, { centralStatement: "We need to decide whether launching this year provides superior net value over waiting twelve months.", }), }); expect(result.question).not.toContain( "whether the prospective enterprise customer will commit this year", ); expect(result.question).not.toContain( "whether the supplier will renew the contract", ); }); it("nominal label without explicit 'whether...' falls back to label-based extraction", () => { const unknown = makeNode({ id: "unc-nominal", label: "Service reliability uncertainty", description: "Unknown service reliability.", kind: "unknown", status: "unknown", confidence: "medium", }); const result = formulateQuestion({ node: unknown, graph: makeGraphFor(unknown), }); expect(result.question).not.toContain("How should uncertainty regarding"); }); it("source description is never mutated by question formulation", () => { const description = "Unknown whether one prospective enterprise customer will sign if we launch this year, so that the remaining £700k of the expected £1.2M annual revenue is realized."; const unknown = makeNode({ id: "unc-preserve", label: "Prospective enterprise customer signing likelihood", description, kind: "unknown", status: "unknown", confidence: "medium", }); const graph = makeGraphFor(unknown, { centralStatement: "We need to decide which launch timing option provides superior net value.", }); formulateQuestion({ node: unknown, graph }); expect(unknown.description).toBe(description); }); // ── 60B.73 — missing sufficiency confirmation question ────────── it("60B.73 Test 1 — exact State B gets sufficiency question", () => { const decision = makeNode({ id: "n_product_launch_decision", label: "Whether to launch product X", description: "Launch vs not launch decision for product X.", kind: "unknown", status: "unknown", confidence: "medium", childIds: [], }); const option = makeNode({ id: "opt_launch_product_x", label: "Launch product X", description: "Launch product X now.", kind: "option", status: "known", confidence: "high", }); const graph = makeGraphFor(decision, { nodes: [option], edges: [ makeEdge({ fromNodeId: option.id, toNodeId: decision.id, relationship: "contained_in", }), ], }); const result = formulateQuestion({ node: decision, graph, context: { resolvedValues: [] }, }); // Decision identity preserved expect(result.questionFamily).toBe("decision_threshold"); expect(result.selectedQuestionTemplate).toBe( "decision_threshold_sufficiency_confirmation", ); // Generic threshold wording ABSENT expect(result.question).not.toContain("What outcome would demonstrate enough value"); expect(result.question.toLowerCase()).not.toContain("what outcome would be sufficient to justify this decision"); }); it("60B.73 Test 2 — question allows missing-factor discovery", () => { const decision = makeNode({ id: "n_product_launch_decision", label: "Whether to launch product X", description: "Launch vs not launch decision for product X.", kind: "unknown", status: "unknown", confidence: "medium", }); const option = makeNode({ id: "opt_launch_product_x_2", label: "Launch product X", description: "Launch product X now.", kind: "option", status: "known", confidence: "high", }); const graph = makeGraphFor(decision, { nodes: [option], edges: [ makeEdge({ fromNodeId: option.id, toNodeId: decision.id, relationship: "contained_in", }), ], }); const result = formulateQuestion({ node: decision, graph, context: { resolvedValues: [] }, }); // Open enough to support: "Yes — customer implementation capacity is still uncertain." expect(result.question).toContain("anything else material"); expect(result.question).toContain("could change which option is better"); }); it("60B.73 Test 3 — genuine remaining factor preserves existing path", () => { 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: "Whether to launch product X", description: "Launch vs not launch decision.", kind: "unknown", status: "unknown", confidence: "medium", childIds: [factor.id], }); const graph = makeGraphFor(factor, { nodes: [decision], edges: [], }); // Verify factor is genuinely unresolved expect(hasRemainingMaterialFactors(decision.id, graph)).toBe(true); const result = formulateQuestion({ node: decision, graph, context: { resolvedValues: [] }, }); // Should NOT select sufficiency template — normal path preserved expect(result.selectedQuestionTemplate).not.toBe( "decision_threshold_sufficiency_confirmation", ); }); it("60B.73 Test 4 — explicit sufficiency confirmation preserves closure/no-question path", () => { const decision = makeNode({ id: "n_product_launch_decision", label: "Whether to launch product X", description: "Launch vs not launch decision.", 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"], }, }); // Should NOT select sufficiency template — confirmation present → normal path expect(result.selectedQuestionTemplate).not.toBe( "decision_threshold_sufficiency_confirmation", ); }); it("60B.73 Test 5 — non-decision unknown unchanged", () => { 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, }); // Should NOT be sufficiency confirmation — normal path expect(result.selectedQuestionTemplate).not.toBe( "decision_threshold_sufficiency_confirmation", ); }); it("60B.73 Test 6 — ordinary decision_threshold preserved for genuine threshold cases", () => { 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], value: "Deciding whether to build the product", }); const graph = makeGraphFor(unknown, { nodes: [decision], resolvedNodeIds: [decision.id], }); const result = formulateQuestion({ node: unknown, graph }); // Should still produce decision_threshold (the normal path for unknown factors) expect(result.questionFamily).toBe("decision_threshold"); }); it("60B.73 Test 7 — resolved factor remains resolved", () => { const factor = makeNode({ id: "n_customer_readiness", label: "Customer readiness confirmed", description: "Ready to adopt.", kind: "unknown", status: "resolved", confidence: "high", parentId: "n_product_launch_decision", }); const decision = makeNode({ id: "n_product_launch_decision", label: "Whether to launch product X", description: "Launch vs not launch decision.", kind: "unknown", status: "unknown", confidence: "medium", childIds: [factor.id], }); const option = makeNode({ id: "opt_launch_product_x_7", label: "Launch product X", description: "Launch product X now.", kind: "option", status: "known", confidence: "high", }); const graph = makeGraphFor(decision, { nodes: [factor, option], edges: [ makeEdge({ fromNodeId: option.id, toNodeId: decision.id, relationship: "contained_in", }), ], }); // Verified: zero remaining factors (factor is resolved) expect(hasRemainingMaterialFactors(decision.id, graph)).toBe(false); const result = formulateQuestion({ node: decision, graph, context: { resolvedValues: [] }, }); // Should get sufficiency confirmation (State B — no remaining factors, no confirmation) expect(result.selectedQuestionTemplate).toBe( "decision_threshold_sufficiency_confirmation", ); }); it("60B.73 Test 8 — same decision identity preserved", () => { const decision = makeNode({ id: "n_product_launch_decision", label: "Whether to launch product X", description: "Launch vs not launch decision.", kind: "unknown", status: "unknown", confidence: "medium", }); const option = makeNode({ id: "opt_launch_product_x_8", label: "Launch product X", description: "Launch product X now.", kind: "option", status: "known", confidence: "high", }); const graph = makeGraphFor(decision, { nodes: [option], edges: [ makeEdge({ fromNodeId: option.id, toNodeId: decision.id, relationship: "contained_in", }), ], }); const result = formulateQuestion({ node: decision, graph, context: { resolvedValues: [] }, }); // Decision identity not changed by sufficiency question 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"); }); }); // ── 60B.80 — sufficiency re-selection after proposal application (no false State B) ──── describe("60B.80 — no false State B after proposal adds unresolved material factor", () => { it("Test 1 — new unresolved unknown with parentId correctly counted as remaining factor", () => { // Start: decision with NO edges → hasRemainingMaterialFactors = 0 → State B triggers const decision = makeNode({ id: "n_launch_decision", label: "Whether to launch product X", description: "Launch vs not launch decision for product X.", kind: "unknown", status: "unknown", confidence: "medium", }); const option = makeNode({ id: "opt_launch_decision_80_1", label: "Launch product X", description: "Launch product X now.", kind: "option", status: "known", confidence: "high", }); const graph = makeGraphFor(decision, { nodes: [option], edges: [ makeEdge({ fromNodeId: option.id, toNodeId: decision.id, relationship: "contained_in", }), ], }); expect(hasRemainingMaterialFactors(decision.id, graph)).toBe(false); // State B correctly triggers here (no remaining factors) const initialResult = formulateQuestion({ node: decision, graph, context: { resolvedValues: [] }, }); expect(initialResult.selectedQuestionTemplate).toBe( "decision_threshold_sufficiency_confirmation", ); // Now simulate what happens AFTER a proposal adds a NEW unresolved unknown // The new unknown represents customer signing uncertainty const newUnknown = makeNode({ id: "n_customer_signing", label: "Customer signing commitment timeline", description: "Uncertainty about whether the key customer will sign.", kind: "unknown", status: "unknown", confidence: "high", parentId: "n_launch_decision", }); // Update decision to include new unknown in childIds and add it to graph const updatedDecision = { ...decision, childIds: [newUnknown.id] }; const updatedGraph = makeGraph({ centralStatement: "Launch decision context", nodes: [updatedDecision, newUnknown], edges: [], activeUnknownNodeId: decision.id, resolvedNodeIds: [], currentSummary: "Updated summary", }); // After the proposal adds a material factor, count should be 1 expect(countRemainingMaterialFactors(updatedDecision.id, updatedGraph)).toBe(1); }); it("Test 2 — formulated question does NOT re-trigger State B when new unresolved factor exists", () => { // Setup: decision + new unresolved unknown with proper parentId link const decision = makeNode({ id: "n_launch_decision", label: "Whether to launch product X", description: "Launch vs not launch.", kind: "unknown", status: "unknown", confidence: "medium", childIds: ["n_customer_signing"], }); const customerSigning = makeNode({ id: "n_customer_signing", label: "Customer signing commitment timeline", description: "Uncertainty about whether the key customer will sign.", kind: "unknown", status: "unknown", confidence: "high", parentId: "n_launch_decision", }); const graph = makeGraph({ centralStatement: "Launch decision context", nodes: [decision, customerSigning], edges: [], activeUnknownNodeId: decision.id, resolvedNodeIds: [], currentSummary: "Updated summary", }); // Core invariant: countRemainingMaterialFactors must detect the new factor expect(countRemainingMaterialFactors(decision.id, graph)).toBe(1); // When we formulate a question for the decision (e.g., after proposal application), // State B should NOT trigger because remaining factors exist. const result = formulateQuestion({ node: decision, graph, context: { resolvedValues: [] }, }); // The key assertion: template must NOT be sufficiency_confirmation // When there ARE remaining material factors, the engine should produce // a normal decision_threshold question (seeking the missing factor), not // the sufficiency confirmation that State B would wrongly provide. expect(result.selectedQuestionTemplate).not.toBe( "decision_threshold_sufficiency_confirmation", ); }); it("Test 3 — customer signing resolution preserves new remaining factors correctly", () => { // Setup: two unknowns, both unresolved, decision in State B (would trigger) const decision = makeNode({ id: "n_launch_decision", label: "Whether to launch product X", description: "Launch vs not launch.", kind: "unknown", status: "unknown", confidence: "medium", childIds: ["n_customer_signing"], }); const customerSigning = makeNode({ id: "n_customer_signing", label: "Customer signing commitment timeline", description: "Uncertainty about whether the key customer will sign.", kind: "unknown", status: "unknown", confidence: "high", parentId: "n_launch_decision", }); const graph = makeGraph({ centralStatement: "Launch decision context", nodes: [decision, customerSigning], edges: [], activeUnknownNodeId: decision.id, resolvedNodeIds: [], currentSummary: "Updated summary", }); // customer signing remains unresolved — count = 1 → no State B expect(countRemainingMaterialFactors(decision.id, graph)).toBe(1); const result = formulateQuestion({ node: decision, graph, context: { resolvedValues: [] }, }); // Should NOT produce sufficiency confirmation because customer signing remains unresolved expect(result.selectedQuestionTemplate).not.toBe( "decision_threshold_sufficiency_confirmation", ); }); it("Test 4 — after resolving ONE factor, State B correctly re-triggers only when ALL factors resolved", () => { const decision = makeNode({ id: "n_launch_decision", label: "Whether to launch product X", description: "Launch vs not launch.", kind: "unknown", status: "unknown", confidence: "medium", childIds: ["n_customer_signing"], }); // customerSigning starts unresolved, then gets resolved (status changes) const customerSigning = makeNode({ id: "n_customer_signing", label: "Customer signing commitment timeline", description: "Uncertainty about whether the key customer will sign.", kind: "unknown", status: "resolved", // resolved via node.status (State B checks this) confidence: "high", parentId: "n_launch_decision", }); const graph = makeGraph({ centralStatement: "Launch decision context", nodes: [ decision, customerSigning, makeNode({ id: "opt_launch_decision_80_4", label: "Launch product X", description: "Launch product X now.", kind: "option", status: "known", confidence: "high", }), ], edges: [ makeEdge({ fromNodeId: "opt_launch_decision_80_4", toNodeId: decision.id, relationship: "contained_in", }), ], activeUnknownNodeId: decision.id, resolvedNodeIds: ["n_customer_signing"], currentSummary: "Updated summary", }); // After resolution (status = "resolved"): hasRemainingMaterialFactors = 0 → State B SHOULD trigger expect(hasRemainingMaterialFactors(decision.id, graph)).toBe(false); const result = formulateQuestion({ node: decision, graph, context: { resolvedValues: [] }, }); // Now State B should correctly trigger because all factors are resolved expect(result.selectedQuestionTemplate).toBe( "decision_threshold_sufficiency_confirmation", ); }); it("Test 5 — explicit confirmation prevents State B even with zero remaining factors", () => { const decision = makeNode({ id: "n_launch_decision", label: "Whether to launch product X", description: "Launch vs not launch.", kind: "unknown", status: "unknown", confidence: "medium", }); const graph = makeGraphFor(decision, { nodes: [], edges: [] }); expect(hasRemainingMaterialFactors(decision.id, graph)).toBe(false); // User confirms sufficiency during State B question const result = formulateQuestion({ node: decision, graph, context: { resolvedValues: [ "no other material uncertainty remains", "Customer signing is the last factor and they will sign.", ], }, }); // Should NOT be sufficiency confirmation because user already confirmed expect(result.selectedQuestionTemplate).not.toBe( "decision_threshold_sufficiency_confirmation", ); }); it("60B.84 — State B must not fire for a specific factor node merely because it is inside a decision context", () => { const decision = makeNode({ id: "n_launch_decision_factor_guard", label: "Which option leaves us better off overall?", description: "Decision about whether to launch this year or wait.", kind: "unknown", status: "unknown", confidence: "medium", }); const launchOption = makeNode({ id: "opt_launch_factor_guard", label: "Launch this year", description: "Launch this year.", kind: "option", status: "known", confidence: "high", }); const selectedFactor = makeNode({ id: "n_specific_remaining_factor", 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", }); const additionalFactor = makeNode({ id: "n_additional_remaining_factor", label: "Competitor response uncertainty", description: "Unknown whether competitors would move first if launch is delayed.", kind: "unknown", status: "unknown", confidence: "medium", }); const graph = makeGraph({ centralStatement: "We need to decide whether to launch this year or wait twelve months.", nodes: [decision, launchOption, selectedFactor, additionalFactor], edges: [ makeEdge({ fromNodeId: launchOption.id, toNodeId: decision.id, relationship: "contained_in", }), makeEdge({ fromNodeId: selectedFactor.id, toNodeId: launchOption.id, relationship: "may_cause", }), makeEdge({ fromNodeId: additionalFactor.id, toNodeId: launchOption.id, relationship: "contained_in", }), ], activeUnknownNodeId: selectedFactor.id, resolvedNodeIds: [], currentSummary: "Specific unresolved factors remain.", }); const pattern = selectReasoningPattern({ node: selectedFactor, graph }); expect(pattern.pattern).toBe("decision"); const result = formulateQuestion({ node: selectedFactor, graph, context: { resolvedValues: [] }, }); expect(selectedFactor.id).not.toBe(decision.id); expect(result.selectedQuestionTemplate).not.toBe( "decision_threshold_sufficiency_confirmation", ); }); });