fix(reasoning): scope structural context admission
This commit is contained in:
+261
-1
@@ -1805,6 +1805,7 @@ function determineActiveReasoningPattern(node, graph) {
|
||||
if (!node || !graph) {
|
||||
return {
|
||||
pattern: null,
|
||||
sourceNodeId: null,
|
||||
reason: "No active reasoning pattern could be determined.",
|
||||
};
|
||||
}
|
||||
@@ -1818,6 +1819,7 @@ function determineActiveReasoningPattern(node, graph) {
|
||||
if (parentSelection.pattern && parentSelection.pattern !== "definition") {
|
||||
return {
|
||||
pattern: parentSelection.pattern,
|
||||
sourceNodeId: parentNode.id,
|
||||
reason: `Inherited active reasoning pattern from parent node because ${parentSelection.reason}`,
|
||||
};
|
||||
}
|
||||
@@ -1827,6 +1829,7 @@ function determineActiveReasoningPattern(node, graph) {
|
||||
const selection = selectReasoningPattern({ node, graph });
|
||||
return {
|
||||
pattern: selection.pattern,
|
||||
sourceNodeId: node.id,
|
||||
reason: selection.reason,
|
||||
};
|
||||
}
|
||||
@@ -1880,7 +1883,101 @@ function inferIntrinsicNodePattern(node, graph) {
|
||||
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) {
|
||||
return {
|
||||
compatible: true,
|
||||
@@ -1895,18 +1992,139 @@ function assessReasoningPatternCompatibility({ node, graph, activePattern }) {
|
||||
activePattern
|
||||
] ?? [activePattern];
|
||||
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 {
|
||||
compatible,
|
||||
activePattern,
|
||||
nodePattern,
|
||||
allowedPatterns,
|
||||
structuralEmbedding: false,
|
||||
reason: compatible
|
||||
? `Node remains compatible because ${nodePattern} is 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) {
|
||||
const text = normaliseText(`${node?.label || ""} ${node?.description || ""}`);
|
||||
return /\b(two observations|measured|measurement|basis|scale|same period|different timing|comparable)\b/.test(
|
||||
@@ -1930,11 +2148,14 @@ function buildRejectedSelectionDiagnostics({
|
||||
graph,
|
||||
activePattern,
|
||||
reason,
|
||||
structurallyAdmittedNodeIds,
|
||||
}) {
|
||||
const compatibility = assessReasoningPatternCompatibility({
|
||||
node,
|
||||
graph,
|
||||
activePattern,
|
||||
activeNodeId: graph.activeUnknownNodeId ?? null,
|
||||
structurallyAdmittedNodeIds,
|
||||
});
|
||||
|
||||
return {
|
||||
@@ -1950,6 +2171,7 @@ function selectPatternCompatibleUnknownCandidate({
|
||||
resolvedNodeIds = [],
|
||||
activePattern,
|
||||
excludedNodeIds = [],
|
||||
structurallyAdmittedNodeIds,
|
||||
}) {
|
||||
if (!activePattern) {
|
||||
return selectActiveUnknownCandidate(graph, resolvedNodeIds);
|
||||
@@ -1969,6 +2191,8 @@ function selectPatternCompatibleUnknownCandidate({
|
||||
node,
|
||||
graph,
|
||||
activePattern,
|
||||
activeNodeId: graph.activeUnknownNodeId ?? null,
|
||||
structurallyAdmittedNodeIds,
|
||||
}).compatible,
|
||||
)
|
||||
.map((node) => node.id);
|
||||
@@ -2021,6 +2245,7 @@ function collectPatternCompatibilityDiagnostics({
|
||||
graph,
|
||||
activePattern,
|
||||
candidateNodeIds = [],
|
||||
structurallyAdmittedNodeIds,
|
||||
}) {
|
||||
if (!activePattern) {
|
||||
return {
|
||||
@@ -2050,6 +2275,8 @@ function collectPatternCompatibilityDiagnostics({
|
||||
node,
|
||||
graph,
|
||||
activePattern,
|
||||
activeNodeId: graph.activeUnknownNodeId ?? null,
|
||||
structurallyAdmittedNodeIds,
|
||||
}),
|
||||
}))
|
||||
.filter(({ compatibility }) => !compatibility.compatible)
|
||||
@@ -2079,6 +2306,7 @@ function selectDecompositionChildCandidate(
|
||||
graph,
|
||||
parentNodeId,
|
||||
activePattern = null,
|
||||
structurallyAdmittedNodeIds,
|
||||
) {
|
||||
const childCandidates = findDirectChildUnknowns(graph, parentNodeId)
|
||||
.filter(
|
||||
@@ -2097,6 +2325,8 @@ function selectDecompositionChildCandidate(
|
||||
node,
|
||||
graph,
|
||||
activePattern,
|
||||
activeNodeId: graph.activeUnknownNodeId ?? null,
|
||||
structurallyAdmittedNodeIds,
|
||||
});
|
||||
return compatibility.compatible;
|
||||
});
|
||||
@@ -2272,6 +2502,7 @@ function reseatSelectionAfterQuestionRejection({
|
||||
deterministicSelection,
|
||||
activePattern = null,
|
||||
excludedNodeIds = [],
|
||||
structurallyAdmittedNodeIds,
|
||||
}) {
|
||||
const nextSelection = activePattern
|
||||
? selectPatternCompatibleUnknownCandidate({
|
||||
@@ -2279,6 +2510,7 @@ function reseatSelectionAfterQuestionRejection({
|
||||
resolvedNodeIds: graph.resolvedNodeIds || [],
|
||||
activePattern,
|
||||
excludedNodeIds,
|
||||
structurallyAdmittedNodeIds,
|
||||
})
|
||||
: selectActiveUnknownCandidate(graph, [
|
||||
...(graph.resolvedNodeIds || []),
|
||||
@@ -2439,6 +2671,7 @@ function runDeterministicDecomposition({
|
||||
updatedSituationGraph,
|
||||
reasoningResolution,
|
||||
deterministicSelection,
|
||||
structurallyAdmittedNodeIds = new Set(),
|
||||
}) {
|
||||
let workingGraph = updatedSituationGraph;
|
||||
let workingSelection = deterministicSelection;
|
||||
@@ -2466,6 +2699,7 @@ function runDeterministicDecomposition({
|
||||
let selectedContainerUnknown = null;
|
||||
let activeReasoningPattern = null;
|
||||
let activeReasoningPatternReason = null;
|
||||
let activeReasoningContextNodeId = null;
|
||||
let incompatibleNodeIds = [];
|
||||
let compatibilityFailures = [];
|
||||
let replacementActions = [];
|
||||
@@ -2485,12 +2719,15 @@ function runDeterministicDecomposition({
|
||||
);
|
||||
activeReasoningPattern = activePatternSelection.pattern;
|
||||
activeReasoningPatternReason = activePatternSelection.reason;
|
||||
activeReasoningContextNodeId = activePatternSelection.sourceNodeId;
|
||||
}
|
||||
|
||||
const selectedNodeCompatibility = assessReasoningPatternCompatibility({
|
||||
node: selectedNode,
|
||||
graph: workingGraph,
|
||||
activePattern: activeReasoningPattern,
|
||||
activeNodeId: activeReasoningContextNodeId,
|
||||
structurallyAdmittedNodeIds,
|
||||
});
|
||||
if (!selectedNodeCompatibility.compatible) {
|
||||
incompatibleNodeIds = appendUniqueValue(
|
||||
@@ -2509,6 +2746,7 @@ function runDeterministicDecomposition({
|
||||
resolvedNodeIds: workingGraph.resolvedNodeIds,
|
||||
activePattern: activeReasoningPattern,
|
||||
excludedNodeIds: [selectedNode.id],
|
||||
structurallyAdmittedNodeIds,
|
||||
});
|
||||
if (replacementSelection?.status === "selected") {
|
||||
replacementActions.push({
|
||||
@@ -2570,6 +2808,7 @@ function runDeterministicDecomposition({
|
||||
workingGraph,
|
||||
selectedNode.id,
|
||||
activeReasoningPattern,
|
||||
structurallyAdmittedNodeIds,
|
||||
);
|
||||
if (childSelection.status === "selected") {
|
||||
workingSelection = childSelection;
|
||||
@@ -2654,6 +2893,7 @@ function runDeterministicDecomposition({
|
||||
workingGraph,
|
||||
selectedNode.id,
|
||||
activeReasoningPattern,
|
||||
structurallyAdmittedNodeIds,
|
||||
);
|
||||
|
||||
if (workingSelection?.status !== "selected") {
|
||||
@@ -2707,6 +2947,7 @@ function runDeterministicDecomposition({
|
||||
selectedContainerUnknown,
|
||||
activeReasoningPattern,
|
||||
activeReasoningPatternReason,
|
||||
activeReasoningContextNodeId,
|
||||
incompatibleNodeIds,
|
||||
compatibilityFailures,
|
||||
replacementActions,
|
||||
@@ -3376,6 +3617,13 @@ export function applyValidatedProposal({
|
||||
};
|
||||
}
|
||||
|
||||
const structurallyAdmittedNodeIds = collectStructurallyAdmittedUnknownNodeIds(
|
||||
{
|
||||
graph: situationGraph,
|
||||
proposal: validatedProposal,
|
||||
},
|
||||
);
|
||||
|
||||
const graphSnapshot = cloneJsonSafe(situationGraph);
|
||||
const proposalSnapshot = cloneJsonSafe(validatedProposal);
|
||||
const previousActiveUnknownNodeId = graphSnapshot.activeUnknownNodeId ?? null;
|
||||
@@ -3482,6 +3730,7 @@ export function applyValidatedProposal({
|
||||
updatedSituationGraph,
|
||||
reasoningResolution,
|
||||
deterministicSelection,
|
||||
structurallyAdmittedNodeIds,
|
||||
});
|
||||
|
||||
if (!decompositionResult.success) {
|
||||
@@ -3526,6 +3775,8 @@ export function applyValidatedProposal({
|
||||
node: preservedSelectedChildNode,
|
||||
graph: updatedSituationGraph,
|
||||
activePattern: decompositionResult.activeReasoningPattern,
|
||||
activeNodeId: decompositionResult.activeReasoningContextNodeId ?? null,
|
||||
structurallyAdmittedNodeIds,
|
||||
})
|
||||
: null;
|
||||
|
||||
@@ -3547,6 +3798,7 @@ export function applyValidatedProposal({
|
||||
activePattern: decompositionResult.activeReasoningPattern,
|
||||
reason:
|
||||
"Preserved decomposition child violated the active reasoning pattern after propagation.",
|
||||
structurallyAdmittedNodeIds,
|
||||
});
|
||||
postPropagationIncompatibleNodeIds =
|
||||
rejectionDiagnostics.incompatibleNodeIds;
|
||||
@@ -3561,6 +3813,7 @@ export function applyValidatedProposal({
|
||||
excludedNodeIds: preservedSelectedChildNode
|
||||
? [preservedSelectedChildNode.id]
|
||||
: [],
|
||||
structurallyAdmittedNodeIds,
|
||||
});
|
||||
|
||||
if (
|
||||
@@ -3608,6 +3861,8 @@ export function applyValidatedProposal({
|
||||
node: carriedActiveUnknownNode,
|
||||
graph: updatedSituationGraph,
|
||||
activePattern: decompositionResult.activeReasoningPattern,
|
||||
activeNodeId: decompositionResult.activeReasoningContextNodeId ?? null,
|
||||
structurallyAdmittedNodeIds,
|
||||
});
|
||||
|
||||
if (!carriedActiveCompatibility.compatible) {
|
||||
@@ -3650,6 +3905,8 @@ export function applyValidatedProposal({
|
||||
node,
|
||||
graph: updatedSituationGraph,
|
||||
activePattern: decompositionResult.activeReasoningPattern,
|
||||
activeNodeId: decompositionResult.activeReasoningContextNodeId ?? null,
|
||||
structurallyAdmittedNodeIds,
|
||||
}).compatible,
|
||||
);
|
||||
|
||||
@@ -3844,6 +4101,7 @@ export function applyValidatedProposal({
|
||||
deterministicSelection,
|
||||
activePattern: decompositionResult.activeReasoningPattern,
|
||||
excludedNodeIds: [deterministicSelection.nodeId],
|
||||
structurallyAdmittedNodeIds,
|
||||
});
|
||||
|
||||
if (
|
||||
@@ -3932,6 +4190,8 @@ export function applyValidatedProposal({
|
||||
),
|
||||
graph: updatedSituationGraph,
|
||||
activePattern: decompositionResult.activeReasoningPattern,
|
||||
activeNodeId: decompositionResult.activeReasoningContextNodeId ?? null,
|
||||
structurallyAdmittedNodeIds,
|
||||
})
|
||||
: null;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user