fix(reasoning): ask for missing sufficiency confirmation

This commit is contained in:
2026-08-14 18:01:27 +01:00
parent c335bf0a9a
commit 8311a176a5
2 changed files with 289 additions and 0 deletions
+41
View File
@@ -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,
+248
View File
@@ -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");
});
});