Feature/product platform foundation v0.62 #1
@@ -58,6 +58,64 @@ function normaliseText(value) {
|
|||||||
.trim();
|
.trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── 60B.64 — raw-answer explicit sufficiency confirmation ──────────
|
||||||
|
|
||||||
|
const CONTRADICTION_PHRASES = [
|
||||||
|
/\bam not\b/i,
|
||||||
|
/\bnot (?:saying|claiming|asserting)\b/i,
|
||||||
|
/still \w+ material/i,
|
||||||
|
];
|
||||||
|
|
||||||
|
const CONFIRMATION_PHRASES = [
|
||||||
|
"no other material uncertainty remains",
|
||||||
|
"no other material uncertainties remain",
|
||||||
|
"no further material uncertainty remains",
|
||||||
|
"no further material uncertainties remain",
|
||||||
|
"no remaining material uncertainty",
|
||||||
|
"no remaining material uncertainties",
|
||||||
|
"no remaining material difference",
|
||||||
|
"no remaining material differences",
|
||||||
|
"nothing else material is uncertain",
|
||||||
|
"nothing else material remains uncertain",
|
||||||
|
];
|
||||||
|
|
||||||
|
const CONFIRMATION_PATTERNS = [
|
||||||
|
/\bno (?:other|further) material \w+?(?:\s+between\b)/i,
|
||||||
|
/\bthe\s+\w+\s+is\s+(?:complete|resolved|closed|settled)\s*(?:now|already)?/i,
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deterministic raw-answer confirmation that no other material
|
||||||
|
* uncertainty remains after a decision factor has been resolved.
|
||||||
|
*
|
||||||
|
* Returns true only when the raw user answer directly states
|
||||||
|
* sufficiency using a bounded explicit phrase family.
|
||||||
|
*
|
||||||
|
* Does NOT use: model-generated meaning, node reason text, or NLP.
|
||||||
|
*/
|
||||||
|
export function isUserConfirmationOfNoRemainingUncertainty(answer) {
|
||||||
|
if (!answer || typeof answer !== "string") return false;
|
||||||
|
|
||||||
|
const lower = answer.toLowerCase();
|
||||||
|
|
||||||
|
// Reject contradictory wording first
|
||||||
|
for (const phrase of CONTRADICTION_PHRASES) {
|
||||||
|
if (phrase.test(lower)) return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check explicit confirmation phrases
|
||||||
|
for (const phrase of CONFIRMATION_PHRASES) {
|
||||||
|
if (lower.includes(phrase)) return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check bounded regex patterns
|
||||||
|
for (const pattern of CONFIRMATION_PATTERNS) {
|
||||||
|
if (pattern.test(lower)) return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
function buildNodeById(graph, addedNodes = []) {
|
function buildNodeById(graph, addedNodes = []) {
|
||||||
return new Map(
|
return new Map(
|
||||||
[...graph.nodes, ...addedNodes].map((node) => [node.id, node]),
|
[...graph.nodes, ...addedNodes].map((node) => [node.id, node]),
|
||||||
@@ -3773,6 +3831,158 @@ export function applyValidatedProposal({
|
|||||||
reasoningResolution.reasoningStateOverride,
|
reasoningResolution.reasoningStateOverride,
|
||||||
);
|
);
|
||||||
updatedSituationGraph.reasoningState = nextReasoningState;
|
updatedSituationGraph.reasoningState = nextReasoningState;
|
||||||
|
|
||||||
|
// ── 60B.64 — explicit decision-sufficiency closure ───────────────
|
||||||
|
// Integrate after post-mutation / post-propagation and before
|
||||||
|
// final active-target / selectedQuestion selection.
|
||||||
|
//
|
||||||
|
// When all represented material factors are resolved AND the raw
|
||||||
|
// user answer explicitly confirms no further material uncertainty,
|
||||||
|
// resolve the existing parent decision in place.
|
||||||
|
|
||||||
|
let closureApplied = false;
|
||||||
|
|
||||||
|
// Build a virtual "resolved this turn" set — at this point node statuses
|
||||||
|
// in updatedSituationGraph have NOT been reconciled yet, so we must
|
||||||
|
// derive what is resolved from proposalSnapshot instead of reading graph.
|
||||||
|
const pendingResolvedIds = new Set([
|
||||||
|
...proposalSnapshot.resolvedUnknownNodeIds,
|
||||||
|
...proposalSnapshot.updatedNodes
|
||||||
|
.filter((u) => u.newStatus === "resolved")
|
||||||
|
.map((u) => u.nodeId),
|
||||||
|
]);
|
||||||
|
|
||||||
|
function checkRemainingFactorsVirtual(decisionNodeId) {
|
||||||
|
const nodes = updatedSituationGraph.nodes || [];
|
||||||
|
const edges = updatedSituationGraph.edges || [];
|
||||||
|
const nodesById = new Map(nodes.map((n) => [n.id, n]));
|
||||||
|
|
||||||
|
const decisionNode = nodesById.get(decisionNodeId);
|
||||||
|
if (!decisionNode) return 0;
|
||||||
|
|
||||||
|
// Collect all option IDs that belong to this decision via contained_in
|
||||||
|
const decisionOptionIds = new Set();
|
||||||
|
for (const edge of edges) {
|
||||||
|
if (
|
||||||
|
edge.relationship === "contained_in" &&
|
||||||
|
edge.toNodeId === decisionNodeId
|
||||||
|
) {
|
||||||
|
decisionOptionIds.add(edge.fromNodeId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getAncestorNode(nodeId, depth = 0) {
|
||||||
|
if (depth > 50) return null;
|
||||||
|
const n = nodesById.get(nodeId);
|
||||||
|
if (!n?.parentId) return null;
|
||||||
|
return nodesById.get(n.parentId) ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isVirtualUnresolvedUnknown(candidateNode) {
|
||||||
|
// A node is "virtually unresolved" only if it hasn't been resolved this turn
|
||||||
|
// and its current graph status isn't terminal.
|
||||||
|
if (pendingResolvedIds.has(candidateNode.id)) return false;
|
||||||
|
if (!TERMINAL_STATUSES.includes(candidateNode.status)) return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const materialFactorIds = new Set();
|
||||||
|
|
||||||
|
for (const node of nodes) {
|
||||||
|
if (node.id === decisionNodeId || !isVirtualUnresolvedUnknown(node)) continue;
|
||||||
|
let currentParent = getAncestorNode(node.id);
|
||||||
|
while (currentParent) {
|
||||||
|
if (currentParent.id === decisionNodeId) {
|
||||||
|
materialFactorIds.add(node.id);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
currentParent = getAncestorNode(currentParent.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const childId of decisionNode.childIds || []) {
|
||||||
|
const childNode = nodesById.get(childId);
|
||||||
|
if (childNode && isVirtualUnresolvedUnknown(childNode)) {
|
||||||
|
materialFactorIds.add(childId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const edge of edges) {
|
||||||
|
if (edge.toNodeId !== decisionNodeId || edge.relationship !== "depends_on") continue;
|
||||||
|
const source = nodesById.get(edge.fromNodeId);
|
||||||
|
if (source && isVirtualUnresolvedUnknown(source)) {
|
||||||
|
materialFactorIds.add(edge.fromNodeId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const edge of edges) {
|
||||||
|
if (edge.relationship !== "affects" && edge.relationship !== "may_cause" && edge.relationship !== "causes") continue;
|
||||||
|
const source = nodesById.get(edge.fromNodeId);
|
||||||
|
const targetOptionId = edge.toNodeId;
|
||||||
|
if (!source || !isVirtualUnresolvedUnknown(source) || !decisionOptionIds.has(targetOptionId)) continue;
|
||||||
|
materialFactorIds.add(edge.fromNodeId);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const edge of edges) {
|
||||||
|
if (edge.relationship !== "contained_in") continue;
|
||||||
|
const fromNode = nodesById.get(edge.fromNodeId);
|
||||||
|
if (!fromNode || !isVirtualUnresolvedUnknown(fromNode)) continue;
|
||||||
|
for (const innerEdge of edges) {
|
||||||
|
if (
|
||||||
|
innerEdge.relationship === "contained_in" &&
|
||||||
|
innerEdge.fromNodeId === edge.toNodeId &&
|
||||||
|
innerEdge.toNodeId === decisionNodeId
|
||||||
|
) {
|
||||||
|
materialFactorIds.add(edge.fromNodeId);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return materialFactorIds.size;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const parentNode of updatedSituationGraph.nodes || []) {
|
||||||
|
if (parentNode.kind !== "unknown") continue;
|
||||||
|
if (TERMINAL_STATUSES.includes(parentNode.status)) continue;
|
||||||
|
if (!(updatedSituationGraph.edges || []).some(
|
||||||
|
(e) => e.relationship === "contained_in" && e.toNodeId === parentNode.id,
|
||||||
|
)) continue;
|
||||||
|
|
||||||
|
const remaining = checkRemainingFactorsVirtual(parentNode.id);
|
||||||
|
const explicitConfirmation = isUserConfirmationOfNoRemainingUncertainty(answer);
|
||||||
|
|
||||||
|
if (remaining === 0 && explicitConfirmation) {
|
||||||
|
parentNode.status = "resolved";
|
||||||
|
ensureResolvedUnknownId(proposalSnapshot, parentNode.id);
|
||||||
|
|
||||||
|
const existingUpdate = proposalSnapshot.updatedNodes.find(
|
||||||
|
(u) => u.nodeId === parentNode.id,
|
||||||
|
);
|
||||||
|
if (!existingUpdate) {
|
||||||
|
proposalSnapshot.updatedNodes.push({
|
||||||
|
nodeId: parentNode.id,
|
||||||
|
previousStatus: "unknown",
|
||||||
|
newStatus: "resolved",
|
||||||
|
previousValue: parentNode.value ?? null,
|
||||||
|
newValue: parentNode.value ?? null,
|
||||||
|
reason:
|
||||||
|
"All represented material factors resolved and raw user answer explicitly confirmed no further material uncertainty remains.",
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
existingUpdate.newStatus = "resolved";
|
||||||
|
if (existingUpdate.previousStatus == null) {
|
||||||
|
existingUpdate.previousStatus = "unknown";
|
||||||
|
}
|
||||||
|
if (existingUpdate.previousValue === undefined) {
|
||||||
|
existingUpdate.previousValue = parentNode.value ?? null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
closureApplied = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const resolvedCurrentTurnNodeIds = [
|
const resolvedCurrentTurnNodeIds = [
|
||||||
...new Set(proposalSnapshot.resolvedUnknownNodeIds || []),
|
...new Set(proposalSnapshot.resolvedUnknownNodeIds || []),
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -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