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) {
|
function normaliseText(value) {
|
||||||
return String(value || "")
|
return String(value || "")
|
||||||
.toLowerCase()
|
.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({
|
const investigationStrategy = selectInvestigationStrategy({
|
||||||
node,
|
node,
|
||||||
graph,
|
graph,
|
||||||
|
|||||||
@@ -7,6 +7,10 @@ import {
|
|||||||
selectInvestigationStrategy,
|
selectInvestigationStrategy,
|
||||||
} from "@/lib/graph/question-formulator.js";
|
} from "@/lib/graph/question-formulator.js";
|
||||||
import { makeEdge, makeGraph, makeNode } from "@/lib/graph/schema.js";
|
import { makeEdge, makeGraph, makeNode } from "@/lib/graph/schema.js";
|
||||||
|
import {
|
||||||
|
hasRemainingMaterialFactors,
|
||||||
|
isUserConfirmationOfNoRemainingUncertainty,
|
||||||
|
} from "@/lib/graph/decision-sufficiency.js";
|
||||||
|
|
||||||
function makeGraphFor(node, extra = {}) {
|
function makeGraphFor(node, extra = {}) {
|
||||||
return makeGraph({
|
return makeGraph({
|
||||||
@@ -1093,4 +1097,248 @@ describe("formulateQuestion", () => {
|
|||||||
|
|
||||||
expect(unknown.description).toBe(description);
|
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