feat(reasoning): integrate explicit decision-sufficiency closure (60B.64)
Add two new capabilities:
1. isUserConfirmationOfNoRemainingUncertainty(answer) — bounded,
deterministic raw-answer confirmation that no other material uncertainty
remains after a decision factor has been resolved. Matches an explicit
phrase family (e.g. 'no remaining material uncertainty', 'no other
material uncertainties remain') plus two bounded regex patterns, while
rejecting contradictory wording ('still another material uncertainty',
'I am not saying...').
2. Decision-sufficiency closure integration point in applyValidatedProposal,
positioned after post-mutation/post-propagation and before final
active-target selection. When all represented material factors are
resolved AND the raw user answer confirms sufficiency, resolves the
existing parent decision in place (status → 'resolved') and clears
the active unknown target.
Uses a virtual 'resolved this turn' set because node statuses have not
yet been reconciled at the integration point. Tests cover: exact fixture
wording from 60B.56, bounded paraphrases, absence-of-confirmation
(non-closure), remaining-factors (blockage), contradictory wording
(rejection), negated phrases (rejection), and vague completion language
(exclusion).
This commit is contained in:
@@ -5362,3 +5362,423 @@ describe("60B.61 — decision remaining-material-factor detection", () => {
|
||||
});
|
||||
});
|
||||
|
||||
// ── 60B.64 — explicit decision sufficiency closure ────────────────
|
||||
|
||||
describe("60B.64 — explicit decision sufficiency closure", () => {
|
||||
function makeClosureDecisionFixture({ includeFallbackUnknown = false } = {}) {
|
||||
const productLaunchDecision = 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 provides superior net value for the organisation.",
|
||||
kind: "unknown",
|
||||
status: "unknown",
|
||||
confidence: "medium",
|
||||
});
|
||||
const launchThisYear = makeNode({
|
||||
id: "opt_launch_this_year",
|
||||
label: "Launch this year",
|
||||
description: "Launch the new software product this year.",
|
||||
kind: "option",
|
||||
status: "known",
|
||||
confidence: "high",
|
||||
});
|
||||
const waitTwelveMonths = makeNode({
|
||||
id: "opt_wait_twelve_months",
|
||||
label: "Wait twelve months",
|
||||
description: "Wait twelve months before launching the product.",
|
||||
kind: "option",
|
||||
status: "known",
|
||||
confidence: "high",
|
||||
});
|
||||
const enterpriseCustomerSigning = makeNode({
|
||||
id: "n_enterprise_customer_signing",
|
||||
label: "Prospective enterprise customer signing status",
|
||||
description:
|
||||
"Unknown whether one prospective enterprise customer will sign if we launch this year, because they account for approximately £700,000 of the £1.2 million expected annual revenue.",
|
||||
kind: "unknown",
|
||||
status: "unknown",
|
||||
confidence: "medium",
|
||||
});
|
||||
|
||||
const nodes = [
|
||||
productLaunchDecision,
|
||||
launchThisYear,
|
||||
waitTwelveMonths,
|
||||
enterpriseCustomerSigning,
|
||||
];
|
||||
const edges = [
|
||||
makeEdge({
|
||||
id: "e-opt-launch-to-dec",
|
||||
fromNodeId: launchThisYear.id,
|
||||
toNodeId: productLaunchDecision.id,
|
||||
relationship: "contained_in",
|
||||
confidence: "high",
|
||||
description: "Launch this year option is a candidate for the product launch decision.",
|
||||
}),
|
||||
makeEdge({
|
||||
id: "e-opt-wait-to-dec",
|
||||
fromNodeId: waitTwelveMonths.id,
|
||||
toNodeId: productLaunchDecision.id,
|
||||
relationship: "contained_in",
|
||||
confidence: "high",
|
||||
description: "Wait twelve months option is a candidate for the product launch decision.",
|
||||
}),
|
||||
makeEdge({
|
||||
id: "e-customer-signing-to-launch-option",
|
||||
fromNodeId: enterpriseCustomerSigning.id,
|
||||
toNodeId: launchThisYear.id,
|
||||
relationship: "contained_in",
|
||||
confidence: "high",
|
||||
description: "Customer signing status is material to launching this year.",
|
||||
}),
|
||||
];
|
||||
|
||||
if (includeFallbackUnknown) {
|
||||
const fallbackUnknown = makeNode({
|
||||
id: "n_other_market_evidence",
|
||||
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",
|
||||
});
|
||||
nodes.push(fallbackUnknown);
|
||||
edges.push(
|
||||
makeEdge({
|
||||
id: "e-fallback-to-launch-option",
|
||||
fromNodeId: fallbackUnknown.id,
|
||||
toNodeId: launchThisYear.id,
|
||||
relationship: "may_cause",
|
||||
confidence: "medium",
|
||||
description: "Fallback unresolved evidence remains material to launch timing.",
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
const graph = makeGraph({
|
||||
centralStatement:
|
||||
"We are evaluating two product-launch timing options: launching the new software product this year or waiting twelve months.",
|
||||
nodes,
|
||||
edges,
|
||||
activeUnknownNodeId: enterpriseCustomerSigning.id,
|
||||
resolvedNodeIds: [],
|
||||
currentSummary:
|
||||
"Customer signing is the active material uncertainty in the product-launch decision.",
|
||||
});
|
||||
|
||||
return {
|
||||
graph,
|
||||
ids: {
|
||||
productLaunchDecision: productLaunchDecision.id,
|
||||
launchThisYear: launchThisYear.id,
|
||||
waitTwelveMonths: waitTwelveMonths.id,
|
||||
enterpriseCustomerSigning: enterpriseCustomerSigning.id,
|
||||
fallbackUnknown: includeFallbackUnknown
|
||||
? "n_other_market_evidence"
|
||||
: null,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// ── Test 1 — exact 60B.56 negative closure (raw confirmation present) ──
|
||||
it("Test 1 — exact 60B.56 wording closes: customer factor resolved, decision = resolved", () => {
|
||||
const { graph, ids } = makeClosureDecisionFixture();
|
||||
|
||||
const result = applyValidatedProposal({
|
||||
situationGraph: graph,
|
||||
proposal: {
|
||||
updatedNodes: [
|
||||
{
|
||||
nodeId: ids.enterpriseCustomerSigning,
|
||||
previousStatus: "unknown",
|
||||
newStatus: "resolved",
|
||||
previousValue: null,
|
||||
newValue:
|
||||
"No. The enterprise customer has now confirmed in writing that they will not sign if we launch this year, so the £700,000 of expected annual revenue from them will not be received.",
|
||||
reason: "The active customer-signing unknown is resolved.",
|
||||
},
|
||||
],
|
||||
addedNodes: [],
|
||||
addedEdges: [],
|
||||
removedEdgeIds: [],
|
||||
resolvedUnknownNodeIds: [ids.enterpriseCustomerSigning],
|
||||
affectedNodeIds: [ids.productLaunchDecision],
|
||||
},
|
||||
answer:
|
||||
"There are no other material uncertainties between launching this year and waiting twelve months.",
|
||||
});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
|
||||
const customerFactor = result.updatedSituationGraph.nodes.find(
|
||||
(n) => n.id === ids.enterpriseCustomerSigning,
|
||||
);
|
||||
const decision = result.updatedSituationGraph.nodes.find(
|
||||
(n) => n.id === ids.productLaunchDecision,
|
||||
);
|
||||
|
||||
expect(customerFactor?.status).toBe("resolved");
|
||||
expect(decision?.status).toBe("resolved");
|
||||
expect(result.updatedSituationGraph.activeUnknownNodeId).toBeNull();
|
||||
expect(result.selectedQuestion).toBeNull();
|
||||
|
||||
// Decision identity preserved — no new nodes/edges added
|
||||
expect(result.updatedSituationGraph.nodes).toHaveLength(4);
|
||||
expect(result.updatedSituationGraph.edges).toHaveLength(3);
|
||||
});
|
||||
|
||||
// ── Test 2 — positive-equivalent closure preserved ──
|
||||
it("Test 2 — bounded paraphrase confirms and closes the decision", () => {
|
||||
const { graph, ids } = makeClosureDecisionFixture();
|
||||
|
||||
const result = applyValidatedProposal({
|
||||
situationGraph: graph,
|
||||
proposal: {
|
||||
updatedNodes: [
|
||||
{
|
||||
nodeId: ids.enterpriseCustomerSigning,
|
||||
previousStatus: "unknown",
|
||||
newStatus: "resolved",
|
||||
previousValue: null,
|
||||
newValue: "Customer signing confirmed for launch this year.",
|
||||
reason: "The active customer-signing unknown is resolved.",
|
||||
},
|
||||
],
|
||||
addedNodes: [],
|
||||
addedEdges: [],
|
||||
removedEdgeIds: [],
|
||||
resolvedUnknownNodeIds: [ids.enterpriseCustomerSigning],
|
||||
affectedNodeIds: [ids.productLaunchDecision],
|
||||
},
|
||||
answer:
|
||||
"no remaining material uncertainty exists between the options.",
|
||||
});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
|
||||
const decision = result.updatedSituationGraph.nodes.find(
|
||||
(n) => n.id === ids.productLaunchDecision,
|
||||
);
|
||||
|
||||
expect(decision?.status).toBe("resolved");
|
||||
expect(result.updatedSituationGraph.activeUnknownNodeId).toBeNull();
|
||||
expect(result.selectedQuestion).toBeNull();
|
||||
});
|
||||
|
||||
// ── Test 3 — last factor resolves but no confirmation ──
|
||||
it("Test 3 — without explicit raw confirmation decision remains open", () => {
|
||||
const { graph, ids } = makeClosureDecisionFixture();
|
||||
|
||||
const result = applyValidatedProposal({
|
||||
situationGraph: graph,
|
||||
proposal: {
|
||||
updatedNodes: [
|
||||
{
|
||||
nodeId: ids.enterpriseCustomerSigning,
|
||||
previousStatus: "unknown",
|
||||
newStatus: "resolved",
|
||||
previousValue: null,
|
||||
newValue: "Customer signing confirmed for launch this year.",
|
||||
reason: "The active customer-signing unknown is resolved.",
|
||||
},
|
||||
],
|
||||
addedNodes: [],
|
||||
addedEdges: [],
|
||||
removedEdgeIds: [],
|
||||
resolvedUnknownNodeIds: [ids.enterpriseCustomerSigning],
|
||||
affectedNodeIds: [ids.productLaunchDecision],
|
||||
},
|
||||
answer: "The customer has confirmed they will sign.",
|
||||
});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
|
||||
const decision = result.updatedSituationGraph.nodes.find(
|
||||
(n) => n.id === ids.productLaunchDecision,
|
||||
);
|
||||
|
||||
expect(decision?.status).not.toBe("resolved");
|
||||
});
|
||||
|
||||
// ── Test 4 — another genuine factor remains ──
|
||||
it("Test 4 — explicit confirmation but remaining factor blocks closure", () => {
|
||||
const { graph: baseGraph, ids } = makeClosureDecisionFixture({
|
||||
includeFallbackUnknown: true,
|
||||
});
|
||||
|
||||
const result = applyValidatedProposal({
|
||||
situationGraph: baseGraph,
|
||||
proposal: {
|
||||
updatedNodes: [
|
||||
{
|
||||
nodeId: ids.enterpriseCustomerSigning,
|
||||
previousStatus: "unknown",
|
||||
newStatus: "resolved",
|
||||
previousValue: null,
|
||||
newValue: "Customer signing confirmed.",
|
||||
reason: "The active customer-signing unknown is resolved.",
|
||||
},
|
||||
],
|
||||
addedNodes: [],
|
||||
addedEdges: [],
|
||||
removedEdgeIds: [],
|
||||
resolvedUnknownNodeIds: [ids.enterpriseCustomerSigning],
|
||||
affectedNodeIds: [ids.productLaunchDecision],
|
||||
},
|
||||
answer:
|
||||
"There are no other material uncertainties between launching this year and waiting twelve months.",
|
||||
});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
|
||||
const decision = result.updatedSituationGraph.nodes.find(
|
||||
(n) => n.id === ids.productLaunchDecision,
|
||||
);
|
||||
|
||||
// Should NOT close because fallback unknown remains unresolved
|
||||
expect(decision?.status).not.toBe("resolved");
|
||||
});
|
||||
|
||||
// ── Test 5 — contradictory confirmation wording rejected ──
|
||||
it("Test 5 — contradictory wording rejects closure", () => {
|
||||
const { graph, ids } = makeClosureDecisionFixture();
|
||||
|
||||
const result = applyValidatedProposal({
|
||||
situationGraph: graph,
|
||||
proposal: {
|
||||
updatedNodes: [
|
||||
{
|
||||
nodeId: ids.enterpriseCustomerSigning,
|
||||
previousStatus: "unknown",
|
||||
newStatus: "resolved",
|
||||
previousValue: null,
|
||||
newValue: "Customer signing confirmed.",
|
||||
reason: "The active customer-signing unknown is resolved.",
|
||||
},
|
||||
],
|
||||
addedNodes: [],
|
||||
addedEdges: [],
|
||||
removedEdgeIds: [],
|
||||
resolvedUnknownNodeIds: [ids.enterpriseCustomerSigning],
|
||||
affectedNodeIds: [ids.productLaunchDecision],
|
||||
},
|
||||
answer: "There is still another material uncertainty.",
|
||||
});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
|
||||
const decision = result.updatedSituationGraph.nodes.find(
|
||||
(n) => n.id === ids.productLaunchDecision,
|
||||
);
|
||||
|
||||
expect(decision?.status).not.toBe("resolved");
|
||||
});
|
||||
|
||||
// ── Test 6 — negated phrase rejected ──
|
||||
it("Test 6 — negated phrase rejects confirmation", () => {
|
||||
const { graph, ids } = makeClosureDecisionFixture();
|
||||
|
||||
const result = applyValidatedProposal({
|
||||
situationGraph: graph,
|
||||
proposal: {
|
||||
updatedNodes: [
|
||||
{
|
||||
nodeId: ids.enterpriseCustomerSigning,
|
||||
previousStatus: "unknown",
|
||||
newStatus: "resolved",
|
||||
previousValue: null,
|
||||
newValue: "Customer signing confirmed.",
|
||||
reason: "The active customer-signing unknown is resolved.",
|
||||
},
|
||||
],
|
||||
addedNodes: [],
|
||||
addedEdges: [],
|
||||
removedEdgeIds: [],
|
||||
resolvedUnknownNodeIds: [ids.enterpriseCustomerSigning],
|
||||
affectedNodeIds: [ids.productLaunchDecision],
|
||||
},
|
||||
answer: "I am not saying there are no other material uncertainties.",
|
||||
});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
|
||||
const decision = result.updatedSituationGraph.nodes.find(
|
||||
(n) => n.id === ids.productLaunchDecision,
|
||||
);
|
||||
|
||||
expect(decision?.status).not.toBe("resolved");
|
||||
});
|
||||
|
||||
// ── Test 7 — bounded supported paraphrase accepted ──
|
||||
it("Test 7 — narrow equivalent phrase confirms sufficiency", () => {
|
||||
const { graph, ids } = makeClosureDecisionFixture();
|
||||
|
||||
const result = applyValidatedProposal({
|
||||
situationGraph: graph,
|
||||
proposal: {
|
||||
updatedNodes: [
|
||||
{
|
||||
nodeId: ids.enterpriseCustomerSigning,
|
||||
previousStatus: "unknown",
|
||||
newStatus: "resolved",
|
||||
previousValue: null,
|
||||
newValue: "Customer signing confirmed.",
|
||||
reason: "The active customer-signing unknown is resolved.",
|
||||
},
|
||||
],
|
||||
addedNodes: [],
|
||||
addedEdges: [],
|
||||
removedEdgeIds: [],
|
||||
resolvedUnknownNodeIds: [ids.enterpriseCustomerSigning],
|
||||
affectedNodeIds: [ids.productLaunchDecision],
|
||||
},
|
||||
answer: "No remaining material uncertainty exists between the options.",
|
||||
});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
|
||||
const decision = result.updatedSituationGraph.nodes.find(
|
||||
(n) => n.id === ids.productLaunchDecision,
|
||||
);
|
||||
|
||||
expect(decision?.status).toBe("resolved");
|
||||
});
|
||||
|
||||
// ── Test 8 — vague completion language excluded ──
|
||||
it("Test 8 — vague phrases like 'That's it' do not confirm", () => {
|
||||
const { graph, ids } = makeClosureDecisionFixture();
|
||||
|
||||
const result = applyValidatedProposal({
|
||||
situationGraph: graph,
|
||||
proposal: {
|
||||
updatedNodes: [
|
||||
{
|
||||
nodeId: ids.enterpriseCustomerSigning,
|
||||
previousStatus: "unknown",
|
||||
newStatus: "resolved",
|
||||
previousValue: null,
|
||||
newValue: "Customer signing confirmed.",
|
||||
reason: "The active customer-signing unknown is resolved.",
|
||||
},
|
||||
],
|
||||
addedNodes: [],
|
||||
addedEdges: [],
|
||||
removedEdgeIds: [],
|
||||
resolvedUnknownNodeIds: [ids.enterpriseCustomerSigning],
|
||||
affectedNodeIds: [ids.productLaunchDecision],
|
||||
},
|
||||
answer: "That's it.",
|
||||
});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
|
||||
const decision = result.updatedSituationGraph.nodes.find(
|
||||
(n) => n.id === ids.productLaunchDecision,
|
||||
);
|
||||
|
||||
expect(decision?.status).not.toBe("resolved");
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user