fix(reasoning): scope structural context admission
This commit is contained in:
+261
-1
@@ -1805,6 +1805,7 @@ function determineActiveReasoningPattern(node, graph) {
|
|||||||
if (!node || !graph) {
|
if (!node || !graph) {
|
||||||
return {
|
return {
|
||||||
pattern: null,
|
pattern: null,
|
||||||
|
sourceNodeId: null,
|
||||||
reason: "No active reasoning pattern could be determined.",
|
reason: "No active reasoning pattern could be determined.",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -1818,6 +1819,7 @@ function determineActiveReasoningPattern(node, graph) {
|
|||||||
if (parentSelection.pattern && parentSelection.pattern !== "definition") {
|
if (parentSelection.pattern && parentSelection.pattern !== "definition") {
|
||||||
return {
|
return {
|
||||||
pattern: parentSelection.pattern,
|
pattern: parentSelection.pattern,
|
||||||
|
sourceNodeId: parentNode.id,
|
||||||
reason: `Inherited active reasoning pattern from parent node because ${parentSelection.reason}`,
|
reason: `Inherited active reasoning pattern from parent node because ${parentSelection.reason}`,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -1827,6 +1829,7 @@ function determineActiveReasoningPattern(node, graph) {
|
|||||||
const selection = selectReasoningPattern({ node, graph });
|
const selection = selectReasoningPattern({ node, graph });
|
||||||
return {
|
return {
|
||||||
pattern: selection.pattern,
|
pattern: selection.pattern,
|
||||||
|
sourceNodeId: node.id,
|
||||||
reason: selection.reason,
|
reason: selection.reason,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -1880,7 +1883,101 @@ function inferIntrinsicNodePattern(node, graph) {
|
|||||||
return selectReasoningPattern({ node, graph }).pattern;
|
return selectReasoningPattern({ node, graph }).pattern;
|
||||||
}
|
}
|
||||||
|
|
||||||
function assessReasoningPatternCompatibility({ node, graph, activePattern }) {
|
// ── Structural embedding predicate (60B.16) ──────────────────
|
||||||
|
|
||||||
|
const STRUCTURAL_CONSEQUENCE_RELATIONSHIPS = ["may_cause", "causes", "affects"];
|
||||||
|
|
||||||
|
function checkRouteAEmbedding({
|
||||||
|
node,
|
||||||
|
graph,
|
||||||
|
activePattern,
|
||||||
|
activeNodeId,
|
||||||
|
nodePattern,
|
||||||
|
}) {
|
||||||
|
if (
|
||||||
|
activePattern !== "decision" ||
|
||||||
|
nodePattern !== "diagnosis" ||
|
||||||
|
!activeNodeId
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const nodesById = new Map((graph.nodes || []).map((item) => [item.id, item]));
|
||||||
|
let current = node?.parentId ? nodesById.get(node.parentId) : null;
|
||||||
|
|
||||||
|
while (current) {
|
||||||
|
if (current.id === activeNodeId) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
current = current.parentId ? nodesById.get(current.parentId) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkRouteBEmbedding({
|
||||||
|
node,
|
||||||
|
graph,
|
||||||
|
activePattern,
|
||||||
|
activeNodeId,
|
||||||
|
nodePattern,
|
||||||
|
}) {
|
||||||
|
if (
|
||||||
|
activePattern !== "decision" ||
|
||||||
|
nodePattern !== "diagnosis" ||
|
||||||
|
!activeNodeId
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const nodesById = new Map((graph.nodes || []).map((item) => [item.id, item]));
|
||||||
|
const edges = graph.edges || [];
|
||||||
|
|
||||||
|
// Find candidate option Z: X --(may_cause/causes/affects)--> Z
|
||||||
|
let candidateOptionZ = null;
|
||||||
|
for (const edge of edges) {
|
||||||
|
if (
|
||||||
|
edge.fromNodeId === node.id &&
|
||||||
|
STRUCTURAL_CONSEQUENCE_RELATIONSHIPS.includes(edge.relationship)
|
||||||
|
) {
|
||||||
|
candidateOptionZ = nodesById.get(edge.toNodeId);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!candidateOptionZ) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Z must be kind=option and contained_in active decision
|
||||||
|
if (candidateOptionZ.kind !== "option") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check the contained_in edge from Z to a decision node that matches activeNodeId
|
||||||
|
for (const edge of edges) {
|
||||||
|
if (
|
||||||
|
edge.fromNodeId === candidateOptionZ.id &&
|
||||||
|
edge.relationship === "contained_in"
|
||||||
|
) {
|
||||||
|
const targetNode = nodesById.get(edge.toNodeId);
|
||||||
|
if (targetNode && targetNode.id === activeNodeId) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// If contained_in points to a different decision, reject
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function assessReasoningPatternCompatibility({
|
||||||
|
node,
|
||||||
|
graph,
|
||||||
|
activePattern,
|
||||||
|
activeNodeId,
|
||||||
|
structurallyAdmittedNodeIds,
|
||||||
|
}) {
|
||||||
if (!node || !activePattern) {
|
if (!node || !activePattern) {
|
||||||
return {
|
return {
|
||||||
compatible: true,
|
compatible: true,
|
||||||
@@ -1895,18 +1992,139 @@ function assessReasoningPatternCompatibility({ node, graph, activePattern }) {
|
|||||||
activePattern
|
activePattern
|
||||||
] ?? [activePattern];
|
] ?? [activePattern];
|
||||||
const compatible = allowedPatterns.includes(nodePattern);
|
const compatible = allowedPatterns.includes(nodePattern);
|
||||||
|
const admittedSet = structurallyAdmittedNodeIds
|
||||||
|
? structurallyAdmittedNodeIds instanceof Set
|
||||||
|
? structurallyAdmittedNodeIds
|
||||||
|
: new Set(structurallyAdmittedNodeIds)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
if (!compatible && admittedSet?.has(node.id)) {
|
||||||
|
return {
|
||||||
|
compatible: true,
|
||||||
|
activePattern,
|
||||||
|
nodePattern,
|
||||||
|
allowedPatterns,
|
||||||
|
structuralEmbedding: true,
|
||||||
|
reason:
|
||||||
|
"Node remains eligible because this same-turn unknown was admitted through bounded structural context fallback at the pre-mutation proposal boundary.",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
compatible,
|
compatible,
|
||||||
activePattern,
|
activePattern,
|
||||||
nodePattern,
|
nodePattern,
|
||||||
allowedPatterns,
|
allowedPatterns,
|
||||||
|
structuralEmbedding: false,
|
||||||
reason: compatible
|
reason: compatible
|
||||||
? `Node remains compatible because ${nodePattern} is allowed during ${activePattern} reasoning.`
|
? `Node remains compatible because ${nodePattern} is allowed during ${activePattern} reasoning.`
|
||||||
: `Node is incompatible because ${nodePattern} is not allowed during ${activePattern} reasoning.`,
|
: `Node is incompatible because ${nodePattern} is not allowed during ${activePattern} reasoning.`,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function assessStructuralContextAdmission({
|
||||||
|
node,
|
||||||
|
graph,
|
||||||
|
activePattern,
|
||||||
|
activeNodeId,
|
||||||
|
}) {
|
||||||
|
const compatibility = assessReasoningPatternCompatibility({
|
||||||
|
node,
|
||||||
|
graph,
|
||||||
|
activePattern,
|
||||||
|
activeNodeId,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (compatibility.compatible) {
|
||||||
|
return {
|
||||||
|
admitted: false,
|
||||||
|
intrinsicCompatible: true,
|
||||||
|
activePattern,
|
||||||
|
activeNodeId: activeNodeId ?? null,
|
||||||
|
nodePattern: compatibility.nodePattern,
|
||||||
|
routeA: false,
|
||||||
|
routeB: false,
|
||||||
|
structuralEmbedding: false,
|
||||||
|
reason: compatibility.reason,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let routeA = false;
|
||||||
|
let routeB = false;
|
||||||
|
|
||||||
|
try {
|
||||||
|
routeA = checkRouteAEmbedding({
|
||||||
|
node,
|
||||||
|
graph,
|
||||||
|
activePattern,
|
||||||
|
activeNodeId: activeNodeId ?? null,
|
||||||
|
nodePattern: compatibility.nodePattern,
|
||||||
|
});
|
||||||
|
} catch (_) {
|
||||||
|
// Non-fatal — bounded structural admission is optional.
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
routeB = checkRouteBEmbedding({
|
||||||
|
node,
|
||||||
|
graph,
|
||||||
|
activePattern,
|
||||||
|
activeNodeId: activeNodeId ?? null,
|
||||||
|
nodePattern: compatibility.nodePattern,
|
||||||
|
});
|
||||||
|
} catch (_) {
|
||||||
|
// Non-fatal.
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
admitted: routeA || routeB,
|
||||||
|
intrinsicCompatible: false,
|
||||||
|
activePattern,
|
||||||
|
activeNodeId: activeNodeId ?? null,
|
||||||
|
nodePattern: compatibility.nodePattern,
|
||||||
|
routeA,
|
||||||
|
routeB,
|
||||||
|
structuralEmbedding: routeA || routeB,
|
||||||
|
reason:
|
||||||
|
routeA || routeB
|
||||||
|
? `Node is structurally embedded in the original active decision context (intrinsic pattern ${compatibility.nodePattern} preserved).`
|
||||||
|
: compatibility.reason,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function collectStructurallyAdmittedUnknownNodeIds({ graph, proposal }) {
|
||||||
|
const activeNodeId = graph?.activeUnknownNodeId ?? null;
|
||||||
|
const activeNode = activeNodeId ? findNodeById(graph, activeNodeId) : null;
|
||||||
|
const activePattern = activeNode
|
||||||
|
? determineActiveReasoningPattern(activeNode, graph).pattern
|
||||||
|
: null;
|
||||||
|
|
||||||
|
if (activePattern !== "decision" || !activeNodeId) {
|
||||||
|
return new Set();
|
||||||
|
}
|
||||||
|
|
||||||
|
const proposalGraph = {
|
||||||
|
...graph,
|
||||||
|
nodes: [...(graph.nodes || []), ...(proposal?.addedNodes || [])],
|
||||||
|
edges: [...(graph.edges || []), ...(proposal?.addedEdges || [])],
|
||||||
|
};
|
||||||
|
|
||||||
|
return new Set(
|
||||||
|
(proposal?.addedNodes || [])
|
||||||
|
.filter((node) => node.kind === "unknown" && node.status !== "resolved")
|
||||||
|
.filter(
|
||||||
|
(node) =>
|
||||||
|
assessStructuralContextAdmission({
|
||||||
|
node,
|
||||||
|
graph: proposalGraph,
|
||||||
|
activePattern,
|
||||||
|
activeNodeId,
|
||||||
|
}).admitted,
|
||||||
|
)
|
||||||
|
.map((node) => node.id),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function isExplicitComparisonFamilyUnknown(node) {
|
function isExplicitComparisonFamilyUnknown(node) {
|
||||||
const text = normaliseText(`${node?.label || ""} ${node?.description || ""}`);
|
const text = normaliseText(`${node?.label || ""} ${node?.description || ""}`);
|
||||||
return /\b(two observations|measured|measurement|basis|scale|same period|different timing|comparable)\b/.test(
|
return /\b(two observations|measured|measurement|basis|scale|same period|different timing|comparable)\b/.test(
|
||||||
@@ -1930,11 +2148,14 @@ function buildRejectedSelectionDiagnostics({
|
|||||||
graph,
|
graph,
|
||||||
activePattern,
|
activePattern,
|
||||||
reason,
|
reason,
|
||||||
|
structurallyAdmittedNodeIds,
|
||||||
}) {
|
}) {
|
||||||
const compatibility = assessReasoningPatternCompatibility({
|
const compatibility = assessReasoningPatternCompatibility({
|
||||||
node,
|
node,
|
||||||
graph,
|
graph,
|
||||||
activePattern,
|
activePattern,
|
||||||
|
activeNodeId: graph.activeUnknownNodeId ?? null,
|
||||||
|
structurallyAdmittedNodeIds,
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -1950,6 +2171,7 @@ function selectPatternCompatibleUnknownCandidate({
|
|||||||
resolvedNodeIds = [],
|
resolvedNodeIds = [],
|
||||||
activePattern,
|
activePattern,
|
||||||
excludedNodeIds = [],
|
excludedNodeIds = [],
|
||||||
|
structurallyAdmittedNodeIds,
|
||||||
}) {
|
}) {
|
||||||
if (!activePattern) {
|
if (!activePattern) {
|
||||||
return selectActiveUnknownCandidate(graph, resolvedNodeIds);
|
return selectActiveUnknownCandidate(graph, resolvedNodeIds);
|
||||||
@@ -1969,6 +2191,8 @@ function selectPatternCompatibleUnknownCandidate({
|
|||||||
node,
|
node,
|
||||||
graph,
|
graph,
|
||||||
activePattern,
|
activePattern,
|
||||||
|
activeNodeId: graph.activeUnknownNodeId ?? null,
|
||||||
|
structurallyAdmittedNodeIds,
|
||||||
}).compatible,
|
}).compatible,
|
||||||
)
|
)
|
||||||
.map((node) => node.id);
|
.map((node) => node.id);
|
||||||
@@ -2021,6 +2245,7 @@ function collectPatternCompatibilityDiagnostics({
|
|||||||
graph,
|
graph,
|
||||||
activePattern,
|
activePattern,
|
||||||
candidateNodeIds = [],
|
candidateNodeIds = [],
|
||||||
|
structurallyAdmittedNodeIds,
|
||||||
}) {
|
}) {
|
||||||
if (!activePattern) {
|
if (!activePattern) {
|
||||||
return {
|
return {
|
||||||
@@ -2050,6 +2275,8 @@ function collectPatternCompatibilityDiagnostics({
|
|||||||
node,
|
node,
|
||||||
graph,
|
graph,
|
||||||
activePattern,
|
activePattern,
|
||||||
|
activeNodeId: graph.activeUnknownNodeId ?? null,
|
||||||
|
structurallyAdmittedNodeIds,
|
||||||
}),
|
}),
|
||||||
}))
|
}))
|
||||||
.filter(({ compatibility }) => !compatibility.compatible)
|
.filter(({ compatibility }) => !compatibility.compatible)
|
||||||
@@ -2079,6 +2306,7 @@ function selectDecompositionChildCandidate(
|
|||||||
graph,
|
graph,
|
||||||
parentNodeId,
|
parentNodeId,
|
||||||
activePattern = null,
|
activePattern = null,
|
||||||
|
structurallyAdmittedNodeIds,
|
||||||
) {
|
) {
|
||||||
const childCandidates = findDirectChildUnknowns(graph, parentNodeId)
|
const childCandidates = findDirectChildUnknowns(graph, parentNodeId)
|
||||||
.filter(
|
.filter(
|
||||||
@@ -2097,6 +2325,8 @@ function selectDecompositionChildCandidate(
|
|||||||
node,
|
node,
|
||||||
graph,
|
graph,
|
||||||
activePattern,
|
activePattern,
|
||||||
|
activeNodeId: graph.activeUnknownNodeId ?? null,
|
||||||
|
structurallyAdmittedNodeIds,
|
||||||
});
|
});
|
||||||
return compatibility.compatible;
|
return compatibility.compatible;
|
||||||
});
|
});
|
||||||
@@ -2272,6 +2502,7 @@ function reseatSelectionAfterQuestionRejection({
|
|||||||
deterministicSelection,
|
deterministicSelection,
|
||||||
activePattern = null,
|
activePattern = null,
|
||||||
excludedNodeIds = [],
|
excludedNodeIds = [],
|
||||||
|
structurallyAdmittedNodeIds,
|
||||||
}) {
|
}) {
|
||||||
const nextSelection = activePattern
|
const nextSelection = activePattern
|
||||||
? selectPatternCompatibleUnknownCandidate({
|
? selectPatternCompatibleUnknownCandidate({
|
||||||
@@ -2279,6 +2510,7 @@ function reseatSelectionAfterQuestionRejection({
|
|||||||
resolvedNodeIds: graph.resolvedNodeIds || [],
|
resolvedNodeIds: graph.resolvedNodeIds || [],
|
||||||
activePattern,
|
activePattern,
|
||||||
excludedNodeIds,
|
excludedNodeIds,
|
||||||
|
structurallyAdmittedNodeIds,
|
||||||
})
|
})
|
||||||
: selectActiveUnknownCandidate(graph, [
|
: selectActiveUnknownCandidate(graph, [
|
||||||
...(graph.resolvedNodeIds || []),
|
...(graph.resolvedNodeIds || []),
|
||||||
@@ -2439,6 +2671,7 @@ function runDeterministicDecomposition({
|
|||||||
updatedSituationGraph,
|
updatedSituationGraph,
|
||||||
reasoningResolution,
|
reasoningResolution,
|
||||||
deterministicSelection,
|
deterministicSelection,
|
||||||
|
structurallyAdmittedNodeIds = new Set(),
|
||||||
}) {
|
}) {
|
||||||
let workingGraph = updatedSituationGraph;
|
let workingGraph = updatedSituationGraph;
|
||||||
let workingSelection = deterministicSelection;
|
let workingSelection = deterministicSelection;
|
||||||
@@ -2466,6 +2699,7 @@ function runDeterministicDecomposition({
|
|||||||
let selectedContainerUnknown = null;
|
let selectedContainerUnknown = null;
|
||||||
let activeReasoningPattern = null;
|
let activeReasoningPattern = null;
|
||||||
let activeReasoningPatternReason = null;
|
let activeReasoningPatternReason = null;
|
||||||
|
let activeReasoningContextNodeId = null;
|
||||||
let incompatibleNodeIds = [];
|
let incompatibleNodeIds = [];
|
||||||
let compatibilityFailures = [];
|
let compatibilityFailures = [];
|
||||||
let replacementActions = [];
|
let replacementActions = [];
|
||||||
@@ -2485,12 +2719,15 @@ function runDeterministicDecomposition({
|
|||||||
);
|
);
|
||||||
activeReasoningPattern = activePatternSelection.pattern;
|
activeReasoningPattern = activePatternSelection.pattern;
|
||||||
activeReasoningPatternReason = activePatternSelection.reason;
|
activeReasoningPatternReason = activePatternSelection.reason;
|
||||||
|
activeReasoningContextNodeId = activePatternSelection.sourceNodeId;
|
||||||
}
|
}
|
||||||
|
|
||||||
const selectedNodeCompatibility = assessReasoningPatternCompatibility({
|
const selectedNodeCompatibility = assessReasoningPatternCompatibility({
|
||||||
node: selectedNode,
|
node: selectedNode,
|
||||||
graph: workingGraph,
|
graph: workingGraph,
|
||||||
activePattern: activeReasoningPattern,
|
activePattern: activeReasoningPattern,
|
||||||
|
activeNodeId: activeReasoningContextNodeId,
|
||||||
|
structurallyAdmittedNodeIds,
|
||||||
});
|
});
|
||||||
if (!selectedNodeCompatibility.compatible) {
|
if (!selectedNodeCompatibility.compatible) {
|
||||||
incompatibleNodeIds = appendUniqueValue(
|
incompatibleNodeIds = appendUniqueValue(
|
||||||
@@ -2509,6 +2746,7 @@ function runDeterministicDecomposition({
|
|||||||
resolvedNodeIds: workingGraph.resolvedNodeIds,
|
resolvedNodeIds: workingGraph.resolvedNodeIds,
|
||||||
activePattern: activeReasoningPattern,
|
activePattern: activeReasoningPattern,
|
||||||
excludedNodeIds: [selectedNode.id],
|
excludedNodeIds: [selectedNode.id],
|
||||||
|
structurallyAdmittedNodeIds,
|
||||||
});
|
});
|
||||||
if (replacementSelection?.status === "selected") {
|
if (replacementSelection?.status === "selected") {
|
||||||
replacementActions.push({
|
replacementActions.push({
|
||||||
@@ -2570,6 +2808,7 @@ function runDeterministicDecomposition({
|
|||||||
workingGraph,
|
workingGraph,
|
||||||
selectedNode.id,
|
selectedNode.id,
|
||||||
activeReasoningPattern,
|
activeReasoningPattern,
|
||||||
|
structurallyAdmittedNodeIds,
|
||||||
);
|
);
|
||||||
if (childSelection.status === "selected") {
|
if (childSelection.status === "selected") {
|
||||||
workingSelection = childSelection;
|
workingSelection = childSelection;
|
||||||
@@ -2654,6 +2893,7 @@ function runDeterministicDecomposition({
|
|||||||
workingGraph,
|
workingGraph,
|
||||||
selectedNode.id,
|
selectedNode.id,
|
||||||
activeReasoningPattern,
|
activeReasoningPattern,
|
||||||
|
structurallyAdmittedNodeIds,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (workingSelection?.status !== "selected") {
|
if (workingSelection?.status !== "selected") {
|
||||||
@@ -2707,6 +2947,7 @@ function runDeterministicDecomposition({
|
|||||||
selectedContainerUnknown,
|
selectedContainerUnknown,
|
||||||
activeReasoningPattern,
|
activeReasoningPattern,
|
||||||
activeReasoningPatternReason,
|
activeReasoningPatternReason,
|
||||||
|
activeReasoningContextNodeId,
|
||||||
incompatibleNodeIds,
|
incompatibleNodeIds,
|
||||||
compatibilityFailures,
|
compatibilityFailures,
|
||||||
replacementActions,
|
replacementActions,
|
||||||
@@ -3376,6 +3617,13 @@ export function applyValidatedProposal({
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const structurallyAdmittedNodeIds = collectStructurallyAdmittedUnknownNodeIds(
|
||||||
|
{
|
||||||
|
graph: situationGraph,
|
||||||
|
proposal: validatedProposal,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
const graphSnapshot = cloneJsonSafe(situationGraph);
|
const graphSnapshot = cloneJsonSafe(situationGraph);
|
||||||
const proposalSnapshot = cloneJsonSafe(validatedProposal);
|
const proposalSnapshot = cloneJsonSafe(validatedProposal);
|
||||||
const previousActiveUnknownNodeId = graphSnapshot.activeUnknownNodeId ?? null;
|
const previousActiveUnknownNodeId = graphSnapshot.activeUnknownNodeId ?? null;
|
||||||
@@ -3482,6 +3730,7 @@ export function applyValidatedProposal({
|
|||||||
updatedSituationGraph,
|
updatedSituationGraph,
|
||||||
reasoningResolution,
|
reasoningResolution,
|
||||||
deterministicSelection,
|
deterministicSelection,
|
||||||
|
structurallyAdmittedNodeIds,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!decompositionResult.success) {
|
if (!decompositionResult.success) {
|
||||||
@@ -3526,6 +3775,8 @@ export function applyValidatedProposal({
|
|||||||
node: preservedSelectedChildNode,
|
node: preservedSelectedChildNode,
|
||||||
graph: updatedSituationGraph,
|
graph: updatedSituationGraph,
|
||||||
activePattern: decompositionResult.activeReasoningPattern,
|
activePattern: decompositionResult.activeReasoningPattern,
|
||||||
|
activeNodeId: decompositionResult.activeReasoningContextNodeId ?? null,
|
||||||
|
structurallyAdmittedNodeIds,
|
||||||
})
|
})
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
@@ -3547,6 +3798,7 @@ export function applyValidatedProposal({
|
|||||||
activePattern: decompositionResult.activeReasoningPattern,
|
activePattern: decompositionResult.activeReasoningPattern,
|
||||||
reason:
|
reason:
|
||||||
"Preserved decomposition child violated the active reasoning pattern after propagation.",
|
"Preserved decomposition child violated the active reasoning pattern after propagation.",
|
||||||
|
structurallyAdmittedNodeIds,
|
||||||
});
|
});
|
||||||
postPropagationIncompatibleNodeIds =
|
postPropagationIncompatibleNodeIds =
|
||||||
rejectionDiagnostics.incompatibleNodeIds;
|
rejectionDiagnostics.incompatibleNodeIds;
|
||||||
@@ -3561,6 +3813,7 @@ export function applyValidatedProposal({
|
|||||||
excludedNodeIds: preservedSelectedChildNode
|
excludedNodeIds: preservedSelectedChildNode
|
||||||
? [preservedSelectedChildNode.id]
|
? [preservedSelectedChildNode.id]
|
||||||
: [],
|
: [],
|
||||||
|
structurallyAdmittedNodeIds,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (
|
if (
|
||||||
@@ -3608,6 +3861,8 @@ export function applyValidatedProposal({
|
|||||||
node: carriedActiveUnknownNode,
|
node: carriedActiveUnknownNode,
|
||||||
graph: updatedSituationGraph,
|
graph: updatedSituationGraph,
|
||||||
activePattern: decompositionResult.activeReasoningPattern,
|
activePattern: decompositionResult.activeReasoningPattern,
|
||||||
|
activeNodeId: decompositionResult.activeReasoningContextNodeId ?? null,
|
||||||
|
structurallyAdmittedNodeIds,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!carriedActiveCompatibility.compatible) {
|
if (!carriedActiveCompatibility.compatible) {
|
||||||
@@ -3650,6 +3905,8 @@ export function applyValidatedProposal({
|
|||||||
node,
|
node,
|
||||||
graph: updatedSituationGraph,
|
graph: updatedSituationGraph,
|
||||||
activePattern: decompositionResult.activeReasoningPattern,
|
activePattern: decompositionResult.activeReasoningPattern,
|
||||||
|
activeNodeId: decompositionResult.activeReasoningContextNodeId ?? null,
|
||||||
|
structurallyAdmittedNodeIds,
|
||||||
}).compatible,
|
}).compatible,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -3844,6 +4101,7 @@ export function applyValidatedProposal({
|
|||||||
deterministicSelection,
|
deterministicSelection,
|
||||||
activePattern: decompositionResult.activeReasoningPattern,
|
activePattern: decompositionResult.activeReasoningPattern,
|
||||||
excludedNodeIds: [deterministicSelection.nodeId],
|
excludedNodeIds: [deterministicSelection.nodeId],
|
||||||
|
structurallyAdmittedNodeIds,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (
|
if (
|
||||||
@@ -3932,6 +4190,8 @@ export function applyValidatedProposal({
|
|||||||
),
|
),
|
||||||
graph: updatedSituationGraph,
|
graph: updatedSituationGraph,
|
||||||
activePattern: decompositionResult.activeReasoningPattern,
|
activePattern: decompositionResult.activeReasoningPattern,
|
||||||
|
activeNodeId: decompositionResult.activeReasoningContextNodeId ?? null,
|
||||||
|
structurallyAdmittedNodeIds,
|
||||||
})
|
})
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,682 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import {
|
||||||
|
applyValidatedProposal,
|
||||||
|
assessReasoningPatternCompatibility,
|
||||||
|
assessStructuralContextAdmission,
|
||||||
|
} from "@/lib/graph/apply-proposal.js";
|
||||||
|
import { selectReasoningPattern } from "@/lib/graph/question-formulator.js";
|
||||||
|
import { makeGraph, makeNode } from "@/lib/graph/schema.js";
|
||||||
|
|
||||||
|
// ── Helpers ──────────────────────────────────────────────
|
||||||
|
|
||||||
|
function buildDecisionContext() {
|
||||||
|
const decision = makeNode({
|
||||||
|
id: "n_relocation_decision",
|
||||||
|
label: "Whether continuing development is commercially justified",
|
||||||
|
description:
|
||||||
|
"Need to know whether continuing development is commercially justified before committing resources.",
|
||||||
|
kind: "unknown",
|
||||||
|
status: "unknown",
|
||||||
|
confidence: "medium",
|
||||||
|
});
|
||||||
|
|
||||||
|
return makeGraph({
|
||||||
|
centralStatement:
|
||||||
|
"Relocation decision context for structural embedding tests.",
|
||||||
|
nodes: [decision],
|
||||||
|
edges: [],
|
||||||
|
activeUnknownNodeId: decision.id,
|
||||||
|
resolvedNodeIds: [],
|
||||||
|
currentSummary: "Decision fixture",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function addOption(graph, optionId) {
|
||||||
|
const option = makeNode({
|
||||||
|
id: optionId,
|
||||||
|
label: "Relocate to new office",
|
||||||
|
description: "Move operations to a new location.",
|
||||||
|
kind: "option",
|
||||||
|
status: "provisional",
|
||||||
|
});
|
||||||
|
graph.nodes.push(option);
|
||||||
|
graph.edges.push({
|
||||||
|
id: `${optionId}_contained_in`,
|
||||||
|
fromNodeId: optionId,
|
||||||
|
toNodeId: graph.activeUnknownNodeId,
|
||||||
|
relationship: "contained_in",
|
||||||
|
confidence: "high",
|
||||||
|
description: "Option belongs to this decision.",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveActivePattern(graph) {
|
||||||
|
const activeNode = graph.nodes.find(
|
||||||
|
(n) => n.id === graph.activeUnknownNodeId,
|
||||||
|
);
|
||||||
|
if (!activeNode) return null;
|
||||||
|
return selectReasoningPattern({ node: activeNode, graph }).pattern;
|
||||||
|
}
|
||||||
|
|
||||||
|
function assessBoundedAdmission(graph, node, overrides = {}) {
|
||||||
|
return assessStructuralContextAdmission({
|
||||||
|
node,
|
||||||
|
graph,
|
||||||
|
activePattern: overrides.activePattern ?? resolveActivePattern(graph),
|
||||||
|
activeNodeId: overrides.activeNodeId ?? graph.activeUnknownNodeId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildApplyProposalDecisionFixture() {
|
||||||
|
const decision = makeNode({
|
||||||
|
id: "n_active_decision",
|
||||||
|
label: "Whether relocating is commercially justified",
|
||||||
|
description:
|
||||||
|
"Need to evaluate whether relocating is commercially justified.",
|
||||||
|
kind: "unknown",
|
||||||
|
status: "unknown",
|
||||||
|
confidence: "medium",
|
||||||
|
});
|
||||||
|
|
||||||
|
return makeGraph({
|
||||||
|
centralStatement: "Relocation follow-up fixture.",
|
||||||
|
nodes: [decision],
|
||||||
|
edges: [],
|
||||||
|
activeUnknownNodeId: decision.id,
|
||||||
|
resolvedNodeIds: [],
|
||||||
|
currentSummary: "Decision apply-proposal fixture",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Tests ────────────────────────────────────────────────
|
||||||
|
|
||||||
|
describe("reasoning-context compatibility — bounded structural admission (60B.19)", () => {
|
||||||
|
it("Test 1 - Route A: newly-added decision unknown admitted via parent/ancestor chain", () => {
|
||||||
|
const decision = makeNode({
|
||||||
|
id: "n_decision_a",
|
||||||
|
label: "Investment context A",
|
||||||
|
description: "A parent context for the option under review.",
|
||||||
|
kind: "unknown",
|
||||||
|
status: "unknown",
|
||||||
|
confidence: "medium",
|
||||||
|
});
|
||||||
|
|
||||||
|
const parentOption = makeNode({
|
||||||
|
id: "opt_parent_a",
|
||||||
|
label: "Parent option A",
|
||||||
|
description: "An option for the decision.",
|
||||||
|
kind: "option",
|
||||||
|
status: "provisional",
|
||||||
|
parentId: "n_decision_a",
|
||||||
|
});
|
||||||
|
|
||||||
|
const childUnknown = makeNode({
|
||||||
|
id: "n_child_embedded",
|
||||||
|
label: "What practical issue is blocking progress",
|
||||||
|
description:
|
||||||
|
"Need to identify the specific blocking issue before continuing this line of work.",
|
||||||
|
kind: "unknown",
|
||||||
|
status: "unknown",
|
||||||
|
confidence: "medium",
|
||||||
|
parentId: "opt_parent_a",
|
||||||
|
});
|
||||||
|
|
||||||
|
const graph = makeGraph({
|
||||||
|
centralStatement: "Market entry decision.",
|
||||||
|
nodes: [decision, parentOption],
|
||||||
|
edges: [],
|
||||||
|
activeUnknownNodeId: "n_decision_a",
|
||||||
|
resolvedNodeIds: [],
|
||||||
|
currentSummary: "Route A fixture",
|
||||||
|
});
|
||||||
|
|
||||||
|
// childUnknown is not yet in graph.nodes; add it so we can build the chain
|
||||||
|
const compat = assessBoundedAdmission(
|
||||||
|
{ ...graph, nodes: [...graph.nodes, childUnknown] },
|
||||||
|
childUnknown,
|
||||||
|
{ activePattern: "decision", activeNodeId: "n_decision_a" },
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(compat.admitted).toBe(true);
|
||||||
|
expect(compat.structuralEmbedding).toBe(true);
|
||||||
|
expect(compat.routeA).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Test 2 - Route B may_cause admitted", () => {
|
||||||
|
const graph = buildDecisionContext();
|
||||||
|
addOption(graph, "opt_relocate");
|
||||||
|
|
||||||
|
const newUnknown = makeNode({
|
||||||
|
id: "n_client_retention_uncertainty",
|
||||||
|
label: "What practical risk affects this option",
|
||||||
|
description:
|
||||||
|
"Need to identify the concrete risk factor affecting this option.",
|
||||||
|
kind: "unknown",
|
||||||
|
status: "unknown",
|
||||||
|
confidence: "medium",
|
||||||
|
});
|
||||||
|
|
||||||
|
graph.nodes.push(newUnknown);
|
||||||
|
graph.edges.push({
|
||||||
|
id: "n_client_retention_uncertainty_may_cause_opt_relocate",
|
||||||
|
fromNodeId: "n_client_retention_uncertainty",
|
||||||
|
toNodeId: "opt_relocate",
|
||||||
|
relationship: "may_cause",
|
||||||
|
confidence: "medium",
|
||||||
|
description: "May cause option consequence.",
|
||||||
|
});
|
||||||
|
|
||||||
|
const compat = assessBoundedAdmission(graph, newUnknown);
|
||||||
|
|
||||||
|
expect(compat.admitted).toBe(true);
|
||||||
|
expect(compat.structuralEmbedding).toBe(true);
|
||||||
|
expect(compat.routeB).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Test 3 - Route B causes admitted", () => {
|
||||||
|
const graph = buildDecisionContext();
|
||||||
|
addOption(graph, "opt_relocate");
|
||||||
|
|
||||||
|
// Add the node and edge into the graph for traversal.
|
||||||
|
const newUnknown = makeNode({
|
||||||
|
id: "n_client_retention_uncertainty",
|
||||||
|
label: "What practical risk affects this option",
|
||||||
|
description:
|
||||||
|
"Need to identify the concrete risk factor affecting this option.",
|
||||||
|
kind: "unknown",
|
||||||
|
status: "unknown",
|
||||||
|
confidence: "medium",
|
||||||
|
});
|
||||||
|
|
||||||
|
graph.nodes.push(newUnknown);
|
||||||
|
graph.edges.push({
|
||||||
|
id: "n_client_retention_uncertainty_causes_opt_relocate",
|
||||||
|
fromNodeId: "n_client_retention_uncertainty",
|
||||||
|
toNodeId: "opt_relocate",
|
||||||
|
relationship: "causes",
|
||||||
|
confidence: "medium",
|
||||||
|
description: "Causal link.",
|
||||||
|
});
|
||||||
|
|
||||||
|
const compat = assessBoundedAdmission(graph, newUnknown);
|
||||||
|
|
||||||
|
expect(compat.admitted).toBe(true);
|
||||||
|
expect(compat.structuralEmbedding).toBe(true);
|
||||||
|
expect(compat.routeB).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Test 4 - Route B affects admitted", () => {
|
||||||
|
const graph = buildDecisionContext();
|
||||||
|
addOption(graph, "opt_relocate");
|
||||||
|
|
||||||
|
const newUnknown = makeNode({
|
||||||
|
id: "n_client_retention_uncertainty",
|
||||||
|
label: "What practical risk affects this option",
|
||||||
|
description:
|
||||||
|
"Need to identify the concrete risk factor affecting this option.",
|
||||||
|
kind: "unknown",
|
||||||
|
status: "unknown",
|
||||||
|
confidence: "medium",
|
||||||
|
});
|
||||||
|
|
||||||
|
graph.nodes.push(newUnknown);
|
||||||
|
graph.edges.push({
|
||||||
|
id: "n_client_retention_uncertainty_affects_opt_relocate",
|
||||||
|
fromNodeId: "n_client_retention_uncertainty",
|
||||||
|
toNodeId: "opt_relocate",
|
||||||
|
relationship: "affects",
|
||||||
|
confidence: "medium",
|
||||||
|
description: "Impact link.",
|
||||||
|
});
|
||||||
|
|
||||||
|
const compat = assessBoundedAdmission(graph, newUnknown);
|
||||||
|
|
||||||
|
expect(compat.admitted).toBe(true);
|
||||||
|
expect(compat.structuralEmbedding).toBe(true);
|
||||||
|
expect(compat.routeB).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Test 5 - supports does NOT qualify as structural embedding", () => {
|
||||||
|
const graph = buildDecisionContext();
|
||||||
|
addOption(graph, "opt_relocate");
|
||||||
|
|
||||||
|
const newUnknown = makeNode({
|
||||||
|
id: "n_supporting_factor",
|
||||||
|
label: "What causes the revenue discrepancy?",
|
||||||
|
description: "Need to understand root cause of divergence.",
|
||||||
|
kind: "unknown",
|
||||||
|
status: "unknown",
|
||||||
|
confidence: "medium",
|
||||||
|
});
|
||||||
|
|
||||||
|
graph.nodes.push(newUnknown);
|
||||||
|
graph.edges.push({
|
||||||
|
id: "n_supporting_factor_supports_opt_relocate",
|
||||||
|
fromNodeId: "n_supporting_factor",
|
||||||
|
toNodeId: "opt_relocate",
|
||||||
|
relationship: "supports",
|
||||||
|
confidence: "medium",
|
||||||
|
description: "Support link.",
|
||||||
|
});
|
||||||
|
|
||||||
|
const compat = assessBoundedAdmission(graph, newUnknown);
|
||||||
|
|
||||||
|
expect(compat.admitted).toBe(false);
|
||||||
|
expect(compat.structuralEmbedding).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Test 6 - measures does NOT qualify as structural embedding", () => {
|
||||||
|
const graph = buildDecisionContext();
|
||||||
|
addOption(graph, "opt_relocate");
|
||||||
|
|
||||||
|
const newUnknown = makeNode({
|
||||||
|
id: "n_measuring_node",
|
||||||
|
label: "What causes the revenue discrepancy?",
|
||||||
|
description: "Need to understand root cause of divergence.",
|
||||||
|
kind: "unknown",
|
||||||
|
status: "unknown",
|
||||||
|
confidence: "medium",
|
||||||
|
});
|
||||||
|
|
||||||
|
graph.nodes.push(newUnknown);
|
||||||
|
graph.edges.push({
|
||||||
|
id: "n_measuring_node_measures_opt_relocate",
|
||||||
|
fromNodeId: "n_measuring_node",
|
||||||
|
toNodeId: "opt_relocate",
|
||||||
|
relationship: "measures",
|
||||||
|
confidence: "medium",
|
||||||
|
description: "Measurement link.",
|
||||||
|
});
|
||||||
|
|
||||||
|
const compat = assessBoundedAdmission(graph, newUnknown);
|
||||||
|
|
||||||
|
expect(compat.admitted).toBe(false);
|
||||||
|
expect(compat.structuralEmbedding).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Test 7 - depends_on does NOT qualify as structural embedding", () => {
|
||||||
|
const graph = buildDecisionContext();
|
||||||
|
addOption(graph, "opt_relocate");
|
||||||
|
|
||||||
|
const newUnknown = makeNode({
|
||||||
|
id: "n_depends_node",
|
||||||
|
label: "What causes the revenue discrepancy?",
|
||||||
|
description: "Need to understand root cause of divergence.",
|
||||||
|
kind: "unknown",
|
||||||
|
status: "unknown",
|
||||||
|
confidence: "medium",
|
||||||
|
});
|
||||||
|
|
||||||
|
graph.nodes.push(newUnknown);
|
||||||
|
graph.edges.push({
|
||||||
|
id: "n_depends_node_depends_on_opt_relocate",
|
||||||
|
fromNodeId: "n_depends_node",
|
||||||
|
toNodeId: "opt_relocate",
|
||||||
|
relationship: "depends_on",
|
||||||
|
confidence: "medium",
|
||||||
|
description: "Dependency link.",
|
||||||
|
});
|
||||||
|
|
||||||
|
const compat = assessBoundedAdmission(graph, newUnknown);
|
||||||
|
|
||||||
|
expect(compat.admitted).toBe(false);
|
||||||
|
expect(compat.structuralEmbedding).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Test 8 - arbitrary graph connectivity does NOT produce compatibility", () => {
|
||||||
|
const graph = buildDecisionContext();
|
||||||
|
addOption(graph, "opt_relocate");
|
||||||
|
|
||||||
|
const intermediateNode = makeNode({
|
||||||
|
id: "n_intermediate",
|
||||||
|
label: "Some unrelated factor",
|
||||||
|
description: "Not relevant.",
|
||||||
|
kind: "unknown",
|
||||||
|
status: "unknown",
|
||||||
|
confidence: "medium",
|
||||||
|
});
|
||||||
|
|
||||||
|
const newUnknown = makeNode({
|
||||||
|
id: "n_arbitrary_path",
|
||||||
|
label: "What causes the revenue discrepancy?",
|
||||||
|
description: "Need to understand root cause of divergence.",
|
||||||
|
kind: "unknown",
|
||||||
|
status: "unknown",
|
||||||
|
confidence: "medium",
|
||||||
|
});
|
||||||
|
|
||||||
|
graph.nodes.push(intermediateNode, newUnknown);
|
||||||
|
graph.edges.push({
|
||||||
|
id: "n_arbitrary_path_may_cause_intermediate",
|
||||||
|
fromNodeId: "n_arbitrary_path",
|
||||||
|
toNodeId: "n_intermediate",
|
||||||
|
relationship: "may_cause",
|
||||||
|
confidence: "medium",
|
||||||
|
description: "Connects to intermediate, not an option.",
|
||||||
|
});
|
||||||
|
|
||||||
|
const compat = assessBoundedAdmission(graph, newUnknown);
|
||||||
|
|
||||||
|
expect(compat.admitted).toBe(false);
|
||||||
|
expect(compat.structuralEmbedding).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Test 9 - wrong decision context rejects", () => {
|
||||||
|
const otherDecision = makeNode({
|
||||||
|
id: "n_other_decision",
|
||||||
|
label: "Should we launch product B?",
|
||||||
|
description: "Different decision.",
|
||||||
|
kind: "unknown",
|
||||||
|
status: "unknown",
|
||||||
|
confidence: "medium",
|
||||||
|
});
|
||||||
|
|
||||||
|
const baseGraph = makeGraph({
|
||||||
|
centralStatement: "Testing wrong decision context.",
|
||||||
|
nodes: [buildDecisionContext().nodes[0], otherDecision],
|
||||||
|
edges: [],
|
||||||
|
activeUnknownNodeId: "n_relocation_decision",
|
||||||
|
resolvedNodeIds: [],
|
||||||
|
currentSummary: "Wrong context fixture",
|
||||||
|
});
|
||||||
|
|
||||||
|
const wrongOption = makeNode({
|
||||||
|
id: "opt_wrong_context",
|
||||||
|
label: "Launch product B option",
|
||||||
|
description: "Belongs to other decision.",
|
||||||
|
kind: "option",
|
||||||
|
status: "provisional",
|
||||||
|
});
|
||||||
|
|
||||||
|
baseGraph.nodes.push(wrongOption);
|
||||||
|
baseGraph.edges.push({
|
||||||
|
id: "opt_wrong_context_contained_in_other",
|
||||||
|
fromNodeId: "opt_wrong_context",
|
||||||
|
toNodeId: "n_other_decision",
|
||||||
|
relationship: "contained_in",
|
||||||
|
confidence: "high",
|
||||||
|
description: "Wrong decision membership.",
|
||||||
|
});
|
||||||
|
|
||||||
|
const newUnknown = makeNode({
|
||||||
|
id: "n_wrong_context_unknown",
|
||||||
|
label: "What practical risk affects this option",
|
||||||
|
description:
|
||||||
|
"Need to identify the concrete risk factor affecting this option.",
|
||||||
|
kind: "unknown",
|
||||||
|
status: "unknown",
|
||||||
|
confidence: "medium",
|
||||||
|
});
|
||||||
|
|
||||||
|
baseGraph.nodes.push(newUnknown);
|
||||||
|
baseGraph.edges.push({
|
||||||
|
id: "n_wrong_context_unknown_may_cause_opt",
|
||||||
|
fromNodeId: "n_wrong_context_unknown",
|
||||||
|
toNodeId: "opt_wrong_context",
|
||||||
|
relationship: "may_cause",
|
||||||
|
confidence: "medium",
|
||||||
|
description: "May cause wrong option.",
|
||||||
|
});
|
||||||
|
|
||||||
|
const compat = assessBoundedAdmission(baseGraph, newUnknown);
|
||||||
|
|
||||||
|
expect(compat.admitted).toBe(false);
|
||||||
|
expect(compat.structuralEmbedding).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Test 10 - already-compatible pattern remains unchanged", () => {
|
||||||
|
const decision = makeNode({
|
||||||
|
id: "n_decision_10",
|
||||||
|
label: "Whether proceeding is commercially justified",
|
||||||
|
description:
|
||||||
|
"Need to evaluate whether proceeding is commercially justified.",
|
||||||
|
kind: "unknown",
|
||||||
|
status: "unknown",
|
||||||
|
confidence: "medium",
|
||||||
|
});
|
||||||
|
|
||||||
|
const definitionNode = makeNode({
|
||||||
|
id: "n_definition_node",
|
||||||
|
label: "Definition of commercially justified",
|
||||||
|
description: "Define what commercially justified means. Define the term.",
|
||||||
|
kind: "unknown",
|
||||||
|
status: "unknown",
|
||||||
|
confidence: "medium",
|
||||||
|
});
|
||||||
|
|
||||||
|
const graph = makeGraph({
|
||||||
|
centralStatement: "Decision context.",
|
||||||
|
nodes: [decision, definitionNode],
|
||||||
|
edges: [],
|
||||||
|
activeUnknownNodeId: "n_decision_10",
|
||||||
|
resolvedNodeIds: [],
|
||||||
|
currentSummary: "Already compatible fixture",
|
||||||
|
});
|
||||||
|
|
||||||
|
const compat = assessBoundedAdmission(graph, definitionNode);
|
||||||
|
|
||||||
|
expect(compat.intrinsicCompatible).toBe(true);
|
||||||
|
expect(compat.admitted).toBe(false);
|
||||||
|
expect(compat.structuralEmbedding).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Test 11 - genuine incompatible child under explanation is rejected", () => {
|
||||||
|
const explanationUnknown = makeNode({
|
||||||
|
id: "n_explanation",
|
||||||
|
label: "Explanation for why revenue increased by 18%",
|
||||||
|
description:
|
||||||
|
"Need to understand what change or event could explain these observations.",
|
||||||
|
kind: "unknown",
|
||||||
|
status: "unknown",
|
||||||
|
confidence: "medium",
|
||||||
|
});
|
||||||
|
|
||||||
|
const graph = makeGraph({
|
||||||
|
centralStatement:
|
||||||
|
"Revenue increased by 18%, but cash in the bank fell over the same period.",
|
||||||
|
nodes: [
|
||||||
|
explanationUnknown,
|
||||||
|
makeNode({
|
||||||
|
id: "n_revenue_obs",
|
||||||
|
label: "Revenue increased by 18%.",
|
||||||
|
description: "Revenue increased by 18%.",
|
||||||
|
kind: "observation",
|
||||||
|
status: "supported",
|
||||||
|
confidence: "high",
|
||||||
|
}),
|
||||||
|
makeNode({
|
||||||
|
id: "n_cash_obs",
|
||||||
|
label: "Cash in the bank fell over the same period.",
|
||||||
|
description: "Cash in the bank fell.",
|
||||||
|
kind: "observation",
|
||||||
|
status: "supported",
|
||||||
|
confidence: "high",
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
edges: [],
|
||||||
|
activeUnknownNodeId: "n_explanation",
|
||||||
|
resolvedNodeIds: [],
|
||||||
|
currentSummary: "Explanation fixture",
|
||||||
|
});
|
||||||
|
|
||||||
|
const diagnosisChild = makeNode({
|
||||||
|
id: "n_diagnosis_child",
|
||||||
|
label: "What practical issue is blocking progress",
|
||||||
|
description:
|
||||||
|
"Need to identify the specific blocking issue before continuing this line of work.",
|
||||||
|
kind: "unknown",
|
||||||
|
status: "unknown",
|
||||||
|
confidence: "medium",
|
||||||
|
parentId: "n_explanation",
|
||||||
|
});
|
||||||
|
|
||||||
|
graph.nodes.push(diagnosisChild);
|
||||||
|
|
||||||
|
const compat = assessBoundedAdmission(graph, diagnosisChild);
|
||||||
|
const compatibility = assessReasoningPatternCompatibility({
|
||||||
|
node: diagnosisChild,
|
||||||
|
graph,
|
||||||
|
activePattern: resolveActivePattern(graph),
|
||||||
|
activeNodeId: graph.activeUnknownNodeId,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(compat.admitted).toBe(false);
|
||||||
|
expect(compat.nodePattern).toBe("diagnosis");
|
||||||
|
expect(compat.structuralEmbedding).toBe(false);
|
||||||
|
expect(compatibility.compatible).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Test 12 - pre-existing unknown does not enter fallback", () => {
|
||||||
|
const graph = buildApplyProposalDecisionFixture();
|
||||||
|
const preExisting = makeNode({
|
||||||
|
id: "n_preexisting_diagnosis",
|
||||||
|
label: "What practical risk affects this option",
|
||||||
|
description:
|
||||||
|
"Need to identify the concrete risk factor affecting this option.",
|
||||||
|
kind: "unknown",
|
||||||
|
status: "unknown",
|
||||||
|
confidence: "medium",
|
||||||
|
parentId: graph.activeUnknownNodeId,
|
||||||
|
});
|
||||||
|
graph.nodes.push(preExisting);
|
||||||
|
|
||||||
|
const result = applyValidatedProposal({
|
||||||
|
situationGraph: graph,
|
||||||
|
proposal: {
|
||||||
|
addedNodes: [],
|
||||||
|
updatedNodes: [
|
||||||
|
{
|
||||||
|
nodeId: graph.activeUnknownNodeId,
|
||||||
|
previousStatus: "unknown",
|
||||||
|
newStatus: "known",
|
||||||
|
previousValue: null,
|
||||||
|
newValue: "Decision anchor updated for selection.",
|
||||||
|
reason: "Makes the parent answer-derived for the test.",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
addedEdges: [],
|
||||||
|
removedEdgeIds: [],
|
||||||
|
resolvedUnknownNodeIds: [],
|
||||||
|
affectedNodeIds: [],
|
||||||
|
selectedQuestion: {
|
||||||
|
nodeId: "n_preexisting_diagnosis",
|
||||||
|
question: "What practical risk affects this option?",
|
||||||
|
reason: "Pre-existing node should not gain fallback admission.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Test 13 - admitted node survives later result validation", () => {
|
||||||
|
const graph = buildApplyProposalDecisionFixture();
|
||||||
|
|
||||||
|
const result = applyValidatedProposal({
|
||||||
|
situationGraph: graph,
|
||||||
|
answer: "We need to identify the practical risk affecting this option.",
|
||||||
|
previousQuestion: "Should we relocate?",
|
||||||
|
proposal: {
|
||||||
|
addedNodes: [
|
||||||
|
makeNode({
|
||||||
|
id: "n_added_route_a",
|
||||||
|
label: "What practical risk affects this option",
|
||||||
|
description:
|
||||||
|
"Need to identify the concrete risk factor affecting this option because it matters to the decision.",
|
||||||
|
kind: "unknown",
|
||||||
|
status: "unknown",
|
||||||
|
confidence: "medium",
|
||||||
|
parentId: graph.activeUnknownNodeId,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
updatedNodes: [
|
||||||
|
{
|
||||||
|
nodeId: graph.activeUnknownNodeId,
|
||||||
|
previousStatus: "unknown",
|
||||||
|
newStatus: "provisional",
|
||||||
|
previousValue: null,
|
||||||
|
newValue: "Decision remains open pending factor clarification.",
|
||||||
|
reason: "The answer introduces a concrete follow-up factor.",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
addedEdges: [],
|
||||||
|
removedEdgeIds: [],
|
||||||
|
resolvedUnknownNodeIds: [],
|
||||||
|
affectedNodeIds: [],
|
||||||
|
selectedQuestion: {
|
||||||
|
nodeId: "n_added_route_a",
|
||||||
|
question: "What practical risk affects this option?",
|
||||||
|
reason: "New factor introduced this turn.",
|
||||||
|
},
|
||||||
|
answerMeaning: {
|
||||||
|
userSupportedMeaning:
|
||||||
|
"We need to identify the practical risk affecting this option.",
|
||||||
|
possibleInference: null,
|
||||||
|
supportCategory: null,
|
||||||
|
resolutionGuidance: null,
|
||||||
|
},
|
||||||
|
structuralActionRequired: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
expect(result.selectedQuestion?.nodeId).toBe("n_added_route_a");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Test 14 - non-pattern validations still apply", () => {
|
||||||
|
const graph = buildApplyProposalDecisionFixture();
|
||||||
|
|
||||||
|
const result = applyValidatedProposal({
|
||||||
|
situationGraph: graph,
|
||||||
|
answer: "We need to identify the practical risk affecting this option.",
|
||||||
|
previousQuestion: "Should we relocate?",
|
||||||
|
proposal: {
|
||||||
|
addedNodes: [
|
||||||
|
makeNode({
|
||||||
|
id: "n_added_invalid",
|
||||||
|
label: "What practical risk affects this option",
|
||||||
|
description:
|
||||||
|
"Need to identify the concrete risk factor affecting this option because it matters to the decision.",
|
||||||
|
kind: "unknown",
|
||||||
|
status: "unknown",
|
||||||
|
confidence: "medium",
|
||||||
|
parentId: graph.activeUnknownNodeId,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
updatedNodes: [
|
||||||
|
{
|
||||||
|
nodeId: graph.activeUnknownNodeId,
|
||||||
|
previousStatus: "unknown",
|
||||||
|
newStatus: "provisional",
|
||||||
|
previousValue: null,
|
||||||
|
newValue: "Decision remains open pending factor clarification.",
|
||||||
|
reason: "The answer introduces a concrete follow-up factor.",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
addedEdges: [],
|
||||||
|
removedEdgeIds: [],
|
||||||
|
resolvedUnknownNodeIds: [],
|
||||||
|
affectedNodeIds: [],
|
||||||
|
selectedQuestion: {
|
||||||
|
nodeId: "n_added_invalid",
|
||||||
|
question:
|
||||||
|
"What practical risk affects this option, and how severe is it?",
|
||||||
|
reason: "Compound question should still fail.",
|
||||||
|
},
|
||||||
|
answerMeaning: {
|
||||||
|
userSupportedMeaning:
|
||||||
|
"We need to identify the practical risk affecting this option.",
|
||||||
|
possibleInference: null,
|
||||||
|
supportCategory: null,
|
||||||
|
resolutionGuidance: null,
|
||||||
|
},
|
||||||
|
structuralActionRequired: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.success).toBe(false);
|
||||||
|
expect(result.errors.join(" ")).toContain(
|
||||||
|
"selectedQuestion must be a single non-compound question",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user