fix(reasoning): ask for missing sufficiency confirmation
This commit is contained in:
@@ -1,3 +1,8 @@
|
||||
import {
|
||||
hasRemainingMaterialFactors,
|
||||
isUserConfirmationOfNoRemainingUncertainty,
|
||||
} from "./decision-sufficiency.js";
|
||||
|
||||
function normaliseText(value) {
|
||||
return String(value || "")
|
||||
.toLowerCase()
|
||||
@@ -1997,6 +2002,42 @@ export function formulateQuestion({ node, graph, context = {} }) {
|
||||
};
|
||||
}
|
||||
|
||||
// ── State B: unresolved decision with zero remaining factors but
|
||||
// no explicit sufficiency confirmation yet ────────────────
|
||||
// Detected transiently from existing state; no persisted field required.
|
||||
if (
|
||||
reasoningPatternSelection.pattern === "decision" &&
|
||||
node.kind !== "unknown" &&
|
||||
node.status !== "known" &&
|
||||
node.status !== "resolved" &&
|
||||
node.status !== "contradicted" &&
|
||||
hasRemainingMaterialFactors(node.id, graph) === false
|
||||
) {
|
||||
const resolved = context.resolvedValues || [];
|
||||
const hasConfirmation = resolved.some((v) =>
|
||||
isUserConfirmationOfNoRemainingUncertainty(v),
|
||||
);
|
||||
|
||||
if (!hasConfirmation) {
|
||||
return {
|
||||
question:
|
||||
"Is there anything else material that could change which option is better?",
|
||||
reason:
|
||||
"All represented material factors are resolved but explicit sufficiency confirmation has not yet been provided.",
|
||||
strategy: null,
|
||||
investigationStrategy: null,
|
||||
reasoningPattern: reasoningPatternSelection.pattern,
|
||||
reasoningPatternReason: reasoningPatternSelection.reason,
|
||||
questionFamily: "decision_threshold",
|
||||
allowedQuestionFamilies,
|
||||
rejectedQuestionFamilies,
|
||||
selectedQuestionTemplate: "decision_threshold_sufficiency_confirmation",
|
||||
questionComplexity: null,
|
||||
plainLanguageNormalisations: [],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const investigationStrategy = selectInvestigationStrategy({
|
||||
node,
|
||||
graph,
|
||||
|
||||
@@ -7,6 +7,10 @@ import {
|
||||
selectInvestigationStrategy,
|
||||
} from "@/lib/graph/question-formulator.js";
|
||||
import { makeEdge, makeGraph, makeNode } from "@/lib/graph/schema.js";
|
||||
import {
|
||||
hasRemainingMaterialFactors,
|
||||
isUserConfirmationOfNoRemainingUncertainty,
|
||||
} from "@/lib/graph/decision-sufficiency.js";
|
||||
|
||||
function makeGraphFor(node, extra = {}) {
|
||||
return makeGraph({
|
||||
@@ -1093,4 +1097,248 @@ describe("formulateQuestion", () => {
|
||||
|
||||
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: "state",
|
||||
status: "unknown",
|
||||
confidence: "medium",
|
||||
childIds: [],
|
||||
});
|
||||
|
||||
const graph = makeGraphFor(decision, {
|
||||
nodes: [],
|
||||
edges: [],
|
||||
});
|
||||
|
||||
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: "state",
|
||||
status: "unknown",
|
||||
confidence: "medium",
|
||||
});
|
||||
|
||||
const graph = makeGraphFor(decision, { nodes: [], edges: [] });
|
||||
|
||||
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: "state",
|
||||
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: "state",
|
||||
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: "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 });
|
||||
|
||||
// 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: "state",
|
||||
status: "unknown",
|
||||
confidence: "medium",
|
||||
childIds: [factor.id],
|
||||
});
|
||||
|
||||
const graph = makeGraphFor(factor, {
|
||||
nodes: [decision],
|
||||
edges: [],
|
||||
});
|
||||
|
||||
// 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: "state",
|
||||
status: "unknown",
|
||||
confidence: "medium",
|
||||
});
|
||||
|
||||
const graph = makeGraphFor(decision, { nodes: [], edges: [] });
|
||||
|
||||
const result = formulateQuestion({
|
||||
node: decision,
|
||||
graph,
|
||||
context: { resolvedValues: [] },
|
||||
});
|
||||
|
||||
// Decision identity not changed by sufficiency question
|
||||
expect(result.reason).toContain("material factors");
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user