1962 lines
63 KiB
JavaScript
1962 lines
63 KiB
JavaScript
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",
|
|
);
|
|
});
|
|
});
|