feat: enforce reasoning pattern consistency
This commit is contained in:
@@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
After every successful graph update, the full deterministic question-selection pipeline must run again whenever eligible unresolved unknowns remain.
|
After every successful graph update, the full deterministic question-selection pipeline must run again whenever eligible unresolved unknowns remain.
|
||||||
|
|
||||||
|
The active reasoning pattern constrains which graph nodes may participate in reasoning.
|
||||||
|
|
||||||
That means the update path must not stop at graph mutation, child resolution, emergent unknown creation, decomposition, or upward propagation. It must continue through:
|
That means the update path must not stop at graph mutation, child resolution, emergent unknown creation, decomposition, or upward propagation. It must continue through:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
@@ -23,6 +25,22 @@ updated graph
|
|||||||
|
|
||||||
Returning no question is only valid when no eligible unresolved candidate remains, the case is complete, ambiguity cannot be safely resolved, or question formulation fails validation with an explicit deterministic reason.
|
Returning no question is only valid when no eligible unresolved candidate remains, the case is complete, ambiguity cannot be safely resolved, or question formulation fails validation with an explicit deterministic reason.
|
||||||
|
|
||||||
|
## Graph validity vs reasoning-pattern validity
|
||||||
|
|
||||||
|
These are separate requirements.
|
||||||
|
|
||||||
|
- **Graph validity** means references, IDs, node shapes, and update semantics are structurally correct.
|
||||||
|
- **Reasoning-pattern validity** means selectable investigation nodes are compatible with the current reasoning mode.
|
||||||
|
|
||||||
|
A graph can be structurally valid while still being reasoning-invalid.
|
||||||
|
|
||||||
|
Example: a decision investigation may still contain an unresolved comparison-style node such as `How the two observations were measured`. That node is structurally well-formed, but it is not allowed to participate as an active investigation target unless the reasoning pattern has actually shifted into comparison, contradiction, or explanation work.
|
||||||
|
|
||||||
|
The engine therefore needs both invariants:
|
||||||
|
|
||||||
|
1. the graph must be structurally valid
|
||||||
|
2. every selectable unknown must be compatible with the active reasoning pattern
|
||||||
|
|
||||||
# v0.7 Question Simplicity Experiment
|
# v0.7 Question Simplicity Experiment
|
||||||
|
|
||||||
## Observed failure
|
## Observed failure
|
||||||
|
|||||||
+497
-12
@@ -7,6 +7,7 @@ import {
|
|||||||
COMPARABILITY_REASONING_NODE_ID,
|
COMPARABILITY_REASONING_NODE_ID,
|
||||||
formulateQuestion,
|
formulateQuestion,
|
||||||
formulateTieResolutionQuestion,
|
formulateTieResolutionQuestion,
|
||||||
|
selectReasoningPattern,
|
||||||
} from "./question-formulator.js";
|
} from "./question-formulator.js";
|
||||||
import {
|
import {
|
||||||
graphUpdateSchema,
|
graphUpdateSchema,
|
||||||
@@ -1727,12 +1728,280 @@ function selectedQuestionBelongsToChild(graph, selectedQuestion) {
|
|||||||
return Boolean(findNodeById(graph, selectedQuestion.nodeId)?.parentId);
|
return Boolean(findNodeById(graph, selectedQuestion.nodeId)?.parentId);
|
||||||
}
|
}
|
||||||
|
|
||||||
function selectDecompositionChildCandidate(graph, parentNodeId) {
|
const ALLOWED_NODE_PATTERNS_BY_ACTIVE_PATTERN = {
|
||||||
const childCandidates = findDirectChildUnknowns(graph, parentNodeId).filter(
|
decision: ["decision", "definition"],
|
||||||
|
explanation: ["explanation", "comparison", "definition"],
|
||||||
|
contradiction: ["contradiction", "comparison", "explanation", "definition"],
|
||||||
|
definition: ["definition"],
|
||||||
|
diagnosis: ["diagnosis", "comparison", "definition"],
|
||||||
|
comparison: ["comparison", "definition"],
|
||||||
|
prioritisation: ["prioritisation", "decision", "definition"],
|
||||||
|
};
|
||||||
|
|
||||||
|
function determineActiveReasoningPattern(node, graph) {
|
||||||
|
if (!node || !graph) {
|
||||||
|
return {
|
||||||
|
pattern: null,
|
||||||
|
reason: "No active reasoning pattern could be determined.",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const nodesById = new Map((graph.nodes || []).map((item) => [item.id, item]));
|
||||||
|
let currentParentId = node.parentId;
|
||||||
|
while (currentParentId) {
|
||||||
|
const parentNode = nodesById.get(currentParentId);
|
||||||
|
if (!parentNode) break;
|
||||||
|
const parentSelection = selectReasoningPattern({ node: parentNode, graph });
|
||||||
|
if (parentSelection.pattern && parentSelection.pattern !== "definition") {
|
||||||
|
return {
|
||||||
|
pattern: parentSelection.pattern,
|
||||||
|
reason: `Inherited active reasoning pattern from parent node because ${parentSelection.reason}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
currentParentId = parentNode.parentId;
|
||||||
|
}
|
||||||
|
|
||||||
|
const selection = selectReasoningPattern({ node, graph });
|
||||||
|
return {
|
||||||
|
pattern: selection.pattern,
|
||||||
|
reason: selection.reason,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function inferIntrinsicNodePattern(node, graph) {
|
||||||
|
const text = normaliseText(`${node?.label || ""} ${node?.description || ""}`);
|
||||||
|
const observationCount = (graph.nodes || []).filter(
|
||||||
|
(candidate) =>
|
||||||
|
candidate.kind === "observation" && candidate.status === "supported",
|
||||||
|
).length;
|
||||||
|
|
||||||
|
if (
|
||||||
|
/\b(define|definition|meaning|term|terminology|boundaries)\b/.test(text)
|
||||||
|
) {
|
||||||
|
return "definition";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
/\b(contradiction|contradict|conflict|inconsistent|mismatch|opposing)\b/.test(
|
||||||
|
text,
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return "contradiction";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
/\b(two observations|measured|measurement|basis|scale|same period|different timing|comparable)\b/.test(
|
||||||
|
text,
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return "comparison";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
observationCount >= 2 &&
|
||||||
|
/\b(explain|explanation|what changed|difference between|divergence|moved differently)\b/.test(
|
||||||
|
text,
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return "explanation";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
/\b(genuine problem|who experiences|other people experience|how often this problem happens|what happens when this problem is not resolved|how people deal with this problem today|actively look for help|would pay to solve this problem|commercially justified|commercial justification|business case|value|demand|audience|customer|user|alternative|alternatives)\b/.test(
|
||||||
|
text,
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return "decision";
|
||||||
|
}
|
||||||
|
|
||||||
|
return selectReasoningPattern({ node, graph }).pattern;
|
||||||
|
}
|
||||||
|
|
||||||
|
function assessReasoningPatternCompatibility({ node, graph, activePattern }) {
|
||||||
|
if (!node || !activePattern) {
|
||||||
|
return {
|
||||||
|
compatible: true,
|
||||||
|
activePattern: activePattern ?? null,
|
||||||
|
nodePattern: null,
|
||||||
|
reason: "No active reasoning pattern constraint was applied.",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const nodePattern = inferIntrinsicNodePattern(node, graph);
|
||||||
|
const allowedPatterns = ALLOWED_NODE_PATTERNS_BY_ACTIVE_PATTERN[
|
||||||
|
activePattern
|
||||||
|
] ?? [activePattern];
|
||||||
|
const compatible = allowedPatterns.includes(nodePattern);
|
||||||
|
|
||||||
|
return {
|
||||||
|
compatible,
|
||||||
|
activePattern,
|
||||||
|
nodePattern,
|
||||||
|
allowedPatterns,
|
||||||
|
reason: compatible
|
||||||
|
? `Node remains compatible because ${nodePattern} is allowed during ${activePattern} reasoning.`
|
||||||
|
: `Node is incompatible because ${nodePattern} is not allowed during ${activePattern} reasoning.`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
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(
|
||||||
|
text,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildCompatibilityFailure(node, compatibility, reason) {
|
||||||
|
return {
|
||||||
|
nodeId: node?.id ?? null,
|
||||||
|
label: node?.label ?? null,
|
||||||
|
activePattern: compatibility?.activePattern ?? null,
|
||||||
|
nodePattern: compatibility?.nodePattern ?? null,
|
||||||
|
allowedPatterns: compatibility?.allowedPatterns ?? [],
|
||||||
|
rejectionReason: reason || compatibility?.reason || null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildRejectedSelectionDiagnostics({
|
||||||
|
node,
|
||||||
|
graph,
|
||||||
|
activePattern,
|
||||||
|
reason,
|
||||||
|
}) {
|
||||||
|
const compatibility = assessReasoningPatternCompatibility({
|
||||||
|
node,
|
||||||
|
graph,
|
||||||
|
activePattern,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
incompatibleNodeIds: node?.id ? [node.id] : [],
|
||||||
|
compatibilityFailures: [
|
||||||
|
buildCompatibilityFailure(node, compatibility, reason),
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectPatternCompatibleUnknownCandidate({
|
||||||
|
graph,
|
||||||
|
resolvedNodeIds = [],
|
||||||
|
activePattern,
|
||||||
|
excludedNodeIds = [],
|
||||||
|
}) {
|
||||||
|
if (!activePattern) {
|
||||||
|
return selectActiveUnknownCandidate(graph, resolvedNodeIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
const excluded = new Set(excludedNodeIds || []);
|
||||||
|
const incompatibleNodeIds = (graph.nodes || [])
|
||||||
|
.filter(
|
||||||
|
(node) =>
|
||||||
|
node.kind === "unknown" &&
|
||||||
|
!excluded.has(node.id) &&
|
||||||
|
!["resolved", "contradicted"].includes(node.status),
|
||||||
|
)
|
||||||
|
.filter(
|
||||||
|
(node) =>
|
||||||
|
!assessReasoningPatternCompatibility({
|
||||||
|
node,
|
||||||
|
graph,
|
||||||
|
activePattern,
|
||||||
|
}).compatible,
|
||||||
|
)
|
||||||
|
.map((node) => node.id);
|
||||||
|
|
||||||
|
return selectActiveUnknownCandidate(graph, [
|
||||||
|
...new Set([
|
||||||
|
...(resolvedNodeIds || []),
|
||||||
|
...incompatibleNodeIds,
|
||||||
|
...excludedNodeIds,
|
||||||
|
]),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function collectPatternCompatibilityDiagnostics({
|
||||||
|
graph,
|
||||||
|
activePattern,
|
||||||
|
candidateNodeIds = [],
|
||||||
|
}) {
|
||||||
|
if (!activePattern) {
|
||||||
|
return {
|
||||||
|
reasoningPatternValidation: {
|
||||||
|
activePattern: null,
|
||||||
|
valid: true,
|
||||||
|
reason: "No active reasoning pattern constraint was applied.",
|
||||||
|
},
|
||||||
|
patternCompatibleNodeCount: 0,
|
||||||
|
incompatibleNodeIds: [],
|
||||||
|
compatibilityFailures: [],
|
||||||
|
graphReasoningIntegrity: "not_applicable",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const candidateSet = new Set(candidateNodeIds || []);
|
||||||
|
const compatibilityFailures = (graph.nodes || [])
|
||||||
|
.filter(
|
||||||
|
(node) =>
|
||||||
|
node.kind === "unknown" &&
|
||||||
|
candidateSet.has(node.id) &&
|
||||||
|
!["resolved", "contradicted"].includes(node.status),
|
||||||
|
)
|
||||||
|
.map((node) => ({
|
||||||
|
node,
|
||||||
|
compatibility: assessReasoningPatternCompatibility({
|
||||||
|
node,
|
||||||
|
graph,
|
||||||
|
activePattern,
|
||||||
|
}),
|
||||||
|
}))
|
||||||
|
.filter(({ compatibility }) => !compatibility.compatible)
|
||||||
|
.map(({ node, compatibility }) =>
|
||||||
|
buildCompatibilityFailure(node, compatibility),
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
reasoningPatternValidation: {
|
||||||
|
activePattern,
|
||||||
|
valid: compatibilityFailures.length === 0,
|
||||||
|
reason:
|
||||||
|
compatibilityFailures.length === 0
|
||||||
|
? `All selectable unknowns are compatible with ${activePattern} reasoning.`
|
||||||
|
: `Some selectable unknowns are incompatible with ${activePattern} reasoning.`,
|
||||||
|
},
|
||||||
|
patternCompatibleNodeCount:
|
||||||
|
(candidateNodeIds || []).length - compatibilityFailures.length,
|
||||||
|
incompatibleNodeIds: compatibilityFailures.map((failure) => failure.nodeId),
|
||||||
|
compatibilityFailures,
|
||||||
|
graphReasoningIntegrity:
|
||||||
|
compatibilityFailures.length === 0 ? "valid" : "invalid",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectDecompositionChildCandidate(
|
||||||
|
graph,
|
||||||
|
parentNodeId,
|
||||||
|
activePattern = null,
|
||||||
|
) {
|
||||||
|
const childCandidates = findDirectChildUnknowns(graph, parentNodeId)
|
||||||
|
.filter(
|
||||||
(node) =>
|
(node) =>
|
||||||
node.kind === "unknown" &&
|
node.kind === "unknown" &&
|
||||||
!["resolved", "contradicted"].includes(node.status),
|
!["resolved", "contradicted"].includes(node.status),
|
||||||
);
|
)
|
||||||
|
.filter((node) => {
|
||||||
|
if (
|
||||||
|
activePattern === "decision" &&
|
||||||
|
isExplicitComparisonFamilyUnknown(node)
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const compatibility = assessReasoningPatternCompatibility({
|
||||||
|
node,
|
||||||
|
graph,
|
||||||
|
activePattern,
|
||||||
|
});
|
||||||
|
return compatibility.compatible;
|
||||||
|
});
|
||||||
|
|
||||||
if (childCandidates.length === 0) {
|
if (childCandidates.length === 0) {
|
||||||
return { status: "none", nodeId: null, tiedCandidateIds: [] };
|
return { status: "none", nodeId: null, tiedCandidateIds: [] };
|
||||||
@@ -1926,6 +2195,11 @@ function runDeterministicDecomposition({
|
|||||||
let decompositionTriggeredByQuestionComplexity = false;
|
let decompositionTriggeredByQuestionComplexity = false;
|
||||||
let decompositionTriggeredByAnswerability = false;
|
let decompositionTriggeredByAnswerability = false;
|
||||||
let selectedContainerUnknown = null;
|
let selectedContainerUnknown = null;
|
||||||
|
let activeReasoningPattern = null;
|
||||||
|
let activeReasoningPatternReason = null;
|
||||||
|
let incompatibleNodeIds = [];
|
||||||
|
let compatibilityFailures = [];
|
||||||
|
let replacementActions = [];
|
||||||
|
|
||||||
while (workingSelection?.status === "selected" && workingSelection?.nodeId) {
|
while (workingSelection?.status === "selected" && workingSelection?.nodeId) {
|
||||||
const selectedNode = findNodeById(workingGraph, workingSelection.nodeId);
|
const selectedNode = findNodeById(workingGraph, workingSelection.nodeId);
|
||||||
@@ -1935,6 +2209,53 @@ function runDeterministicDecomposition({
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!activeReasoningPattern) {
|
||||||
|
const activePatternSelection = determineActiveReasoningPattern(
|
||||||
|
selectedNode,
|
||||||
|
workingGraph,
|
||||||
|
);
|
||||||
|
activeReasoningPattern = activePatternSelection.pattern;
|
||||||
|
activeReasoningPatternReason = activePatternSelection.reason;
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectedNodeCompatibility = assessReasoningPatternCompatibility({
|
||||||
|
node: selectedNode,
|
||||||
|
graph: workingGraph,
|
||||||
|
activePattern: activeReasoningPattern,
|
||||||
|
});
|
||||||
|
if (!selectedNodeCompatibility.compatible) {
|
||||||
|
incompatibleNodeIds = appendUniqueValue(
|
||||||
|
incompatibleNodeIds,
|
||||||
|
selectedNode.id,
|
||||||
|
);
|
||||||
|
compatibilityFailures.push(
|
||||||
|
buildCompatibilityFailure(
|
||||||
|
selectedNode,
|
||||||
|
selectedNodeCompatibility,
|
||||||
|
"Selected unknown violated the active reasoning pattern.",
|
||||||
|
),
|
||||||
|
);
|
||||||
|
const replacementSelection = selectPatternCompatibleUnknownCandidate({
|
||||||
|
graph: workingGraph,
|
||||||
|
resolvedNodeIds: workingGraph.resolvedNodeIds,
|
||||||
|
activePattern: activeReasoningPattern,
|
||||||
|
excludedNodeIds: [selectedNode.id],
|
||||||
|
});
|
||||||
|
if (replacementSelection?.status === "selected") {
|
||||||
|
replacementActions.push({
|
||||||
|
rejectedNodeId: selectedNode.id,
|
||||||
|
replacementNodeId: replacementSelection.nodeId,
|
||||||
|
reason:
|
||||||
|
"Replaced an incompatible active unknown with the next pattern-compatible candidate.",
|
||||||
|
});
|
||||||
|
workingSelection = replacementSelection;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
decompositionStoppedReason =
|
||||||
|
"No reasoning-pattern-compatible unknown remained available for selection.";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
const atomicityAssessment = assessUnknownAtomicity({
|
const atomicityAssessment = assessUnknownAtomicity({
|
||||||
node: selectedNode,
|
node: selectedNode,
|
||||||
graph: workingGraph,
|
graph: workingGraph,
|
||||||
@@ -1979,6 +2300,7 @@ function runDeterministicDecomposition({
|
|||||||
const childSelection = selectDecompositionChildCandidate(
|
const childSelection = selectDecompositionChildCandidate(
|
||||||
workingGraph,
|
workingGraph,
|
||||||
selectedNode.id,
|
selectedNode.id,
|
||||||
|
activeReasoningPattern,
|
||||||
);
|
);
|
||||||
if (childSelection.status === "selected") {
|
if (childSelection.status === "selected") {
|
||||||
workingSelection = childSelection;
|
workingSelection = childSelection;
|
||||||
@@ -2013,6 +2335,7 @@ function runDeterministicDecomposition({
|
|||||||
selectedNode,
|
selectedNode,
|
||||||
workingGraph,
|
workingGraph,
|
||||||
decompositionDepth,
|
decompositionDepth,
|
||||||
|
activeReasoningPattern,
|
||||||
);
|
);
|
||||||
|
|
||||||
proposedChildCount = decomposition.proposedChildCount;
|
proposedChildCount = decomposition.proposedChildCount;
|
||||||
@@ -2061,6 +2384,7 @@ function runDeterministicDecomposition({
|
|||||||
workingSelection = selectDecompositionChildCandidate(
|
workingSelection = selectDecompositionChildCandidate(
|
||||||
workingGraph,
|
workingGraph,
|
||||||
selectedNode.id,
|
selectedNode.id,
|
||||||
|
activeReasoningPattern,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (workingSelection?.status !== "selected") {
|
if (workingSelection?.status !== "selected") {
|
||||||
@@ -2112,6 +2436,11 @@ function runDeterministicDecomposition({
|
|||||||
decompositionTriggeredByQuestionComplexity,
|
decompositionTriggeredByQuestionComplexity,
|
||||||
decompositionTriggeredByAnswerability,
|
decompositionTriggeredByAnswerability,
|
||||||
selectedContainerUnknown,
|
selectedContainerUnknown,
|
||||||
|
activeReasoningPattern,
|
||||||
|
activeReasoningPatternReason,
|
||||||
|
incompatibleNodeIds,
|
||||||
|
compatibilityFailures,
|
||||||
|
replacementActions,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2446,20 +2775,75 @@ export function applyValidatedProposal({
|
|||||||
const resolvedCurrentTurnNodeIds = [
|
const resolvedCurrentTurnNodeIds = [
|
||||||
...new Set(proposalSnapshot.resolvedUnknownNodeIds || []),
|
...new Set(proposalSnapshot.resolvedUnknownNodeIds || []),
|
||||||
];
|
];
|
||||||
deterministicSelection = isSelectableUnresolvedUnknown(
|
let postPropagationIncompatibleNodeIds = [];
|
||||||
|
let postPropagationCompatibilityFailures = [];
|
||||||
|
let postPropagationReplacementActions = [];
|
||||||
|
let activeUnknownIncompatibleNodeIds = [];
|
||||||
|
let activeUnknownCompatibilityFailures = [];
|
||||||
|
let activeUnknownReplacementActions = [];
|
||||||
|
const preservedSelectedChildNode = isSelectableUnresolvedUnknown(
|
||||||
updatedSituationGraph,
|
updatedSituationGraph,
|
||||||
decompositionResult.selectedChildNodeId,
|
decompositionResult.selectedChildNodeId,
|
||||||
)
|
)
|
||||||
? {
|
? findNodeById(
|
||||||
|
updatedSituationGraph,
|
||||||
|
decompositionResult.selectedChildNodeId,
|
||||||
|
)
|
||||||
|
: null;
|
||||||
|
const preservedSelectedChildCompatibility = preservedSelectedChildNode
|
||||||
|
? assessReasoningPatternCompatibility({
|
||||||
|
node: preservedSelectedChildNode,
|
||||||
|
graph: updatedSituationGraph,
|
||||||
|
activePattern: decompositionResult.activeReasoningPattern,
|
||||||
|
})
|
||||||
|
: null;
|
||||||
|
|
||||||
|
if (
|
||||||
|
preservedSelectedChildNode &&
|
||||||
|
preservedSelectedChildCompatibility?.compatible
|
||||||
|
) {
|
||||||
|
deterministicSelection = {
|
||||||
status: "selected",
|
status: "selected",
|
||||||
nodeId: decompositionResult.selectedChildNodeId,
|
nodeId: decompositionResult.selectedChildNodeId,
|
||||||
reason:
|
reason:
|
||||||
"Preserved the selected decomposition child because it remains unresolved after propagation.",
|
"Preserved the selected decomposition child because it remains unresolved and reasoning-pattern-compatible after propagation.",
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
if (preservedSelectedChildNode) {
|
||||||
|
const rejectionDiagnostics = buildRejectedSelectionDiagnostics({
|
||||||
|
node: preservedSelectedChildNode,
|
||||||
|
graph: updatedSituationGraph,
|
||||||
|
activePattern: decompositionResult.activeReasoningPattern,
|
||||||
|
reason:
|
||||||
|
"Preserved decomposition child violated the active reasoning pattern after propagation.",
|
||||||
|
});
|
||||||
|
postPropagationIncompatibleNodeIds =
|
||||||
|
rejectionDiagnostics.incompatibleNodeIds;
|
||||||
|
postPropagationCompatibilityFailures =
|
||||||
|
rejectionDiagnostics.compatibilityFailures;
|
||||||
|
}
|
||||||
|
|
||||||
|
deterministicSelection = selectPatternCompatibleUnknownCandidate({
|
||||||
|
graph: updatedSituationGraph,
|
||||||
|
resolvedNodeIds: updatedSituationGraph.resolvedNodeIds,
|
||||||
|
activePattern: decompositionResult.activeReasoningPattern,
|
||||||
|
excludedNodeIds: preservedSelectedChildNode
|
||||||
|
? [preservedSelectedChildNode.id]
|
||||||
|
: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
if (
|
||||||
|
preservedSelectedChildNode &&
|
||||||
|
deterministicSelection?.status === "selected"
|
||||||
|
) {
|
||||||
|
postPropagationReplacementActions.push({
|
||||||
|
rejectedNodeId: preservedSelectedChildNode.id,
|
||||||
|
replacementNodeId: deterministicSelection.nodeId,
|
||||||
|
reason:
|
||||||
|
"Replaced a preserved decomposition child that violated reasoning-pattern consistency.",
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
: selectActiveUnknownCandidate(
|
|
||||||
updatedSituationGraph,
|
|
||||||
updatedSituationGraph.resolvedNodeIds,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (deterministicSelection?.status === "ambiguous") {
|
if (deterministicSelection?.status === "ambiguous") {
|
||||||
const orderedSiblingSelection = selectOrderedSiblingCandidate(
|
const orderedSiblingSelection = selectOrderedSiblingCandidate(
|
||||||
@@ -2473,14 +2857,76 @@ export function applyValidatedProposal({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const carriedActiveUnknownNode = previousActiveUnknownNodeId
|
||||||
|
? findNodeById(updatedSituationGraph, previousActiveUnknownNodeId)
|
||||||
|
: null;
|
||||||
|
const carriedActiveUnknownStillUnresolved = Boolean(
|
||||||
|
carriedActiveUnknownNode &&
|
||||||
|
carriedActiveUnknownNode.kind === "unknown" &&
|
||||||
|
!["resolved", "contradicted"].includes(carriedActiveUnknownNode.status) &&
|
||||||
|
!updatedSituationGraph.resolvedNodeIds.includes(
|
||||||
|
carriedActiveUnknownNode.id,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (
|
||||||
|
carriedActiveUnknownStillUnresolved &&
|
||||||
|
decompositionResult.activeReasoningPattern
|
||||||
|
) {
|
||||||
|
const carriedActiveCompatibility = assessReasoningPatternCompatibility({
|
||||||
|
node: carriedActiveUnknownNode,
|
||||||
|
graph: updatedSituationGraph,
|
||||||
|
activePattern: decompositionResult.activeReasoningPattern,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!carriedActiveCompatibility.compatible) {
|
||||||
|
activeUnknownIncompatibleNodeIds = [carriedActiveUnknownNode.id];
|
||||||
|
activeUnknownCompatibilityFailures = [
|
||||||
|
buildCompatibilityFailure(
|
||||||
|
carriedActiveUnknownNode,
|
||||||
|
carriedActiveCompatibility,
|
||||||
|
"Carried active unknown violated the active reasoning pattern and was not retained for investigation.",
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
if (
|
||||||
|
deterministicSelection?.status === "selected" &&
|
||||||
|
deterministicSelection.nodeId !== carriedActiveUnknownNode.id
|
||||||
|
) {
|
||||||
|
activeUnknownReplacementActions = [
|
||||||
|
{
|
||||||
|
rejectedNodeId: carriedActiveUnknownNode.id,
|
||||||
|
replacementNodeId: deterministicSelection.nodeId,
|
||||||
|
reason:
|
||||||
|
"Replaced an incompatible carried active unknown with a pattern-compatible investigation target.",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const unresolvedCandidates = listUnresolvedUnknownCandidates(
|
const unresolvedCandidates = listUnresolvedUnknownCandidates(
|
||||||
updatedSituationGraph,
|
updatedSituationGraph,
|
||||||
resolvedCurrentTurnNodeIds,
|
resolvedCurrentTurnNodeIds,
|
||||||
);
|
);
|
||||||
const eligibleCandidates = listEligibleUnknownCandidates(
|
const eligibleCandidatesBeforeCompatibility = listEligibleUnknownCandidates(
|
||||||
updatedSituationGraph,
|
updatedSituationGraph,
|
||||||
resolvedCurrentTurnNodeIds,
|
resolvedCurrentTurnNodeIds,
|
||||||
);
|
);
|
||||||
|
const eligibleCandidates = eligibleCandidatesBeforeCompatibility.filter(
|
||||||
|
(node) =>
|
||||||
|
assessReasoningPatternCompatibility({
|
||||||
|
node,
|
||||||
|
graph: updatedSituationGraph,
|
||||||
|
activePattern: decompositionResult.activeReasoningPattern,
|
||||||
|
}).compatible,
|
||||||
|
);
|
||||||
|
|
||||||
|
const compatibilityDiagnostics = collectPatternCompatibilityDiagnostics({
|
||||||
|
graph: updatedSituationGraph,
|
||||||
|
activePattern: decompositionResult.activeReasoningPattern,
|
||||||
|
candidateNodeIds: eligibleCandidates.map((node) => node.id),
|
||||||
|
});
|
||||||
|
|
||||||
const atomicityAssessment = decompositionResult.atomicityAssessment;
|
const atomicityAssessment = decompositionResult.atomicityAssessment;
|
||||||
const answerabilityAssessment = decompositionResult.answerabilityAssessment;
|
const answerabilityAssessment = decompositionResult.answerabilityAssessment;
|
||||||
@@ -2603,6 +3049,14 @@ export function applyValidatedProposal({
|
|||||||
}
|
}
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
|
const finalSelectionCompatibility = finalSelectedQuestion?.nodeId
|
||||||
|
? assessReasoningPatternCompatibility({
|
||||||
|
node: findNodeById(updatedSituationGraph, finalSelectedQuestion.nodeId),
|
||||||
|
graph: updatedSituationGraph,
|
||||||
|
activePattern: decompositionResult.activeReasoningPattern,
|
||||||
|
})
|
||||||
|
: null;
|
||||||
|
|
||||||
const finalSelectedChildNodeId =
|
const finalSelectedChildNodeId =
|
||||||
selectedChildNodeId ??
|
selectedChildNodeId ??
|
||||||
(selectedQuestionBelongsToChild(
|
(selectedQuestionBelongsToChild(
|
||||||
@@ -2638,7 +3092,8 @@ export function applyValidatedProposal({
|
|||||||
!resultGraphValidation.success ||
|
!resultGraphValidation.success ||
|
||||||
!resultReferenceValidation?.valid ||
|
!resultReferenceValidation?.valid ||
|
||||||
resultDuplicateNodeIds.length > 0 ||
|
resultDuplicateNodeIds.length > 0 ||
|
||||||
resultDuplicateEdgeIds.length > 0
|
resultDuplicateEdgeIds.length > 0 ||
|
||||||
|
(finalSelectedQuestion?.nodeId && !finalSelectionCompatibility?.compatible)
|
||||||
) {
|
) {
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
@@ -2658,6 +3113,12 @@ export function applyValidatedProposal({
|
|||||||
({ edgeId, count }) =>
|
({ edgeId, count }) =>
|
||||||
`Updated graph contains duplicate edge ID: "${edgeId}" (${count} occurrences)`,
|
`Updated graph contains duplicate edge ID: "${edgeId}" (${count} occurrences)`,
|
||||||
),
|
),
|
||||||
|
...(!finalSelectionCompatibility?.compatible &&
|
||||||
|
finalSelectedQuestion?.nodeId
|
||||||
|
? [
|
||||||
|
`Active unknown violates reasoning pattern consistency: "${finalSelectedQuestion.nodeId}" is ${finalSelectionCompatibility?.nodePattern} but active pattern is ${finalSelectionCompatibility?.activePattern}`,
|
||||||
|
]
|
||||||
|
: []),
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -2737,6 +3198,30 @@ export function applyValidatedProposal({
|
|||||||
childNodeIds: decompositionChildNodeIds,
|
childNodeIds: decompositionChildNodeIds,
|
||||||
selectedContainerUnknown:
|
selectedContainerUnknown:
|
||||||
decompositionResult.selectedContainerUnknown ?? null,
|
decompositionResult.selectedContainerUnknown ?? null,
|
||||||
|
reasoningPatternValidation:
|
||||||
|
compatibilityDiagnostics.reasoningPatternValidation,
|
||||||
|
patternCompatibleNodeCount:
|
||||||
|
compatibilityDiagnostics.patternCompatibleNodeCount,
|
||||||
|
incompatibleNodeIds: [
|
||||||
|
...new Set([
|
||||||
|
...(decompositionResult.incompatibleNodeIds || []),
|
||||||
|
...postPropagationIncompatibleNodeIds,
|
||||||
|
...activeUnknownIncompatibleNodeIds,
|
||||||
|
...compatibilityDiagnostics.incompatibleNodeIds,
|
||||||
|
]),
|
||||||
|
],
|
||||||
|
compatibilityFailures: [
|
||||||
|
...(decompositionResult.compatibilityFailures || []),
|
||||||
|
...postPropagationCompatibilityFailures,
|
||||||
|
...activeUnknownCompatibilityFailures,
|
||||||
|
...compatibilityDiagnostics.compatibilityFailures,
|
||||||
|
],
|
||||||
|
replacementActions: [
|
||||||
|
...(decompositionResult.replacementActions || []),
|
||||||
|
...postPropagationReplacementActions,
|
||||||
|
...activeUnknownReplacementActions,
|
||||||
|
],
|
||||||
|
graphReasoningIntegrity: compatibilityDiagnostics.graphReasoningIntegrity,
|
||||||
selectedChildUnknown:
|
selectedChildUnknown:
|
||||||
finalSelectedChildNodeId ??
|
finalSelectedChildNodeId ??
|
||||||
(deterministicSelection?.status === "selected"
|
(deterministicSelection?.status === "selected"
|
||||||
|
|||||||
@@ -65,6 +65,12 @@ function buildDiagnostics({
|
|||||||
rejectedQuestionFamilies,
|
rejectedQuestionFamilies,
|
||||||
selectedQuestionTemplate,
|
selectedQuestionTemplate,
|
||||||
reasoningPatternReason,
|
reasoningPatternReason,
|
||||||
|
reasoningPatternValidation,
|
||||||
|
patternCompatibleNodeCount,
|
||||||
|
incompatibleNodeIds,
|
||||||
|
compatibilityFailures,
|
||||||
|
replacementActions,
|
||||||
|
graphReasoningIntegrity,
|
||||||
}) {
|
}) {
|
||||||
return {
|
return {
|
||||||
promptVersion: analysis?.promptVersion ?? null,
|
promptVersion: analysis?.promptVersion ?? null,
|
||||||
@@ -100,6 +106,12 @@ function buildDiagnostics({
|
|||||||
rejectedQuestionFamilies: rejectedQuestionFamilies ?? [],
|
rejectedQuestionFamilies: rejectedQuestionFamilies ?? [],
|
||||||
selectedQuestionTemplate: selectedQuestionTemplate ?? null,
|
selectedQuestionTemplate: selectedQuestionTemplate ?? null,
|
||||||
reasoningPatternReason: reasoningPatternReason ?? null,
|
reasoningPatternReason: reasoningPatternReason ?? null,
|
||||||
|
reasoningPatternValidation: reasoningPatternValidation ?? null,
|
||||||
|
patternCompatibleNodeCount: patternCompatibleNodeCount ?? 0,
|
||||||
|
incompatibleNodeIds: incompatibleNodeIds ?? [],
|
||||||
|
compatibilityFailures: compatibilityFailures ?? [],
|
||||||
|
replacementActions: replacementActions ?? [],
|
||||||
|
graphReasoningIntegrity: graphReasoningIntegrity ?? null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -200,6 +212,12 @@ function buildUpdateDiagnostics({
|
|||||||
candidateNodeIds,
|
candidateNodeIds,
|
||||||
resolvedCurrentTurnNodeIds,
|
resolvedCurrentTurnNodeIds,
|
||||||
noQuestionReason,
|
noQuestionReason,
|
||||||
|
reasoningPatternValidation,
|
||||||
|
patternCompatibleNodeCount,
|
||||||
|
incompatibleNodeIds,
|
||||||
|
compatibilityFailures,
|
||||||
|
replacementActions,
|
||||||
|
graphReasoningIntegrity,
|
||||||
}) {
|
}) {
|
||||||
return {
|
return {
|
||||||
promptVersion: promptVersion ?? "v0.4",
|
promptVersion: promptVersion ?? "v0.4",
|
||||||
@@ -291,6 +309,12 @@ function buildUpdateDiagnostics({
|
|||||||
candidateNodeIds: candidateNodeIds ?? [],
|
candidateNodeIds: candidateNodeIds ?? [],
|
||||||
resolvedCurrentTurnNodeIds: resolvedCurrentTurnNodeIds ?? [],
|
resolvedCurrentTurnNodeIds: resolvedCurrentTurnNodeIds ?? [],
|
||||||
noQuestionReason: noQuestionReason ?? null,
|
noQuestionReason: noQuestionReason ?? null,
|
||||||
|
reasoningPatternValidation: reasoningPatternValidation ?? null,
|
||||||
|
patternCompatibleNodeCount: patternCompatibleNodeCount ?? 0,
|
||||||
|
incompatibleNodeIds: incompatibleNodeIds ?? [],
|
||||||
|
compatibilityFailures: compatibilityFailures ?? [],
|
||||||
|
replacementActions: replacementActions ?? [],
|
||||||
|
graphReasoningIntegrity: graphReasoningIntegrity ?? null,
|
||||||
unknownSelectionExplanation: unknownSelectionExplanation ?? null,
|
unknownSelectionExplanation: unknownSelectionExplanation ?? null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -419,6 +443,16 @@ export async function startCase(body) {
|
|||||||
reasoningPatternReason:
|
reasoningPatternReason:
|
||||||
initialQuestionResult.selectedQuestion?.reasoningPatternReason ??
|
initialQuestionResult.selectedQuestion?.reasoningPatternReason ??
|
||||||
null,
|
null,
|
||||||
|
reasoningPatternValidation:
|
||||||
|
initialQuestionResult.reasoningPatternValidation ?? null,
|
||||||
|
patternCompatibleNodeCount:
|
||||||
|
initialQuestionResult.patternCompatibleNodeCount ?? 0,
|
||||||
|
incompatibleNodeIds: initialQuestionResult.incompatibleNodeIds ?? [],
|
||||||
|
compatibilityFailures:
|
||||||
|
initialQuestionResult.compatibilityFailures ?? [],
|
||||||
|
replacementActions: initialQuestionResult.replacementActions ?? [],
|
||||||
|
graphReasoningIntegrity:
|
||||||
|
initialQuestionResult.graphReasoningIntegrity ?? null,
|
||||||
}),
|
}),
|
||||||
validationErrors: graphReferenceValidation.errors,
|
validationErrors: graphReferenceValidation.errors,
|
||||||
statusCode: 500,
|
statusCode: 500,
|
||||||
@@ -475,6 +509,15 @@ export async function startCase(body) {
|
|||||||
null,
|
null,
|
||||||
reasoningPatternReason:
|
reasoningPatternReason:
|
||||||
initialQuestionResult.selectedQuestion?.reasoningPatternReason ?? null,
|
initialQuestionResult.selectedQuestion?.reasoningPatternReason ?? null,
|
||||||
|
reasoningPatternValidation:
|
||||||
|
initialQuestionResult.reasoningPatternValidation ?? null,
|
||||||
|
patternCompatibleNodeCount:
|
||||||
|
initialQuestionResult.patternCompatibleNodeCount ?? 0,
|
||||||
|
incompatibleNodeIds: initialQuestionResult.incompatibleNodeIds ?? [],
|
||||||
|
compatibilityFailures: initialQuestionResult.compatibilityFailures ?? [],
|
||||||
|
replacementActions: initialQuestionResult.replacementActions ?? [],
|
||||||
|
graphReasoningIntegrity:
|
||||||
|
initialQuestionResult.graphReasoningIntegrity ?? null,
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -675,6 +718,12 @@ async function updateCaseWithDependencies(body, dependencies = {}) {
|
|||||||
candidateNodeIds: [],
|
candidateNodeIds: [],
|
||||||
resolvedCurrentTurnNodeIds: [],
|
resolvedCurrentTurnNodeIds: [],
|
||||||
noQuestionReason: null,
|
noQuestionReason: null,
|
||||||
|
reasoningPatternValidation: null,
|
||||||
|
patternCompatibleNodeCount: 0,
|
||||||
|
incompatibleNodeIds: [],
|
||||||
|
compatibilityFailures: [],
|
||||||
|
replacementActions: [],
|
||||||
|
graphReasoningIntegrity: null,
|
||||||
plainLanguageNormalisations: [],
|
plainLanguageNormalisations: [],
|
||||||
unknownSelectionExplanation: explainUnknownSelection(
|
unknownSelectionExplanation: explainUnknownSelection(
|
||||||
situationGraph,
|
situationGraph,
|
||||||
@@ -780,6 +829,14 @@ async function updateCaseWithDependencies(body, dependencies = {}) {
|
|||||||
resolvedCurrentTurnNodeIds:
|
resolvedCurrentTurnNodeIds:
|
||||||
applicationResult.resolvedCurrentTurnNodeIds,
|
applicationResult.resolvedCurrentTurnNodeIds,
|
||||||
noQuestionReason: applicationResult.noQuestionReason,
|
noQuestionReason: applicationResult.noQuestionReason,
|
||||||
|
reasoningPatternValidation:
|
||||||
|
applicationResult.reasoningPatternValidation,
|
||||||
|
patternCompatibleNodeCount:
|
||||||
|
applicationResult.patternCompatibleNodeCount,
|
||||||
|
incompatibleNodeIds: applicationResult.incompatibleNodeIds,
|
||||||
|
compatibilityFailures: applicationResult.compatibilityFailures,
|
||||||
|
replacementActions: applicationResult.replacementActions,
|
||||||
|
graphReasoningIntegrity: applicationResult.graphReasoningIntegrity,
|
||||||
plainLanguageNormalisations:
|
plainLanguageNormalisations:
|
||||||
applicationResult.plainLanguageNormalisations,
|
applicationResult.plainLanguageNormalisations,
|
||||||
reasoningPattern:
|
reasoningPattern:
|
||||||
@@ -876,6 +933,12 @@ async function updateCaseWithDependencies(body, dependencies = {}) {
|
|||||||
candidateNodeIds: [],
|
candidateNodeIds: [],
|
||||||
resolvedCurrentTurnNodeIds: [],
|
resolvedCurrentTurnNodeIds: [],
|
||||||
noQuestionReason: null,
|
noQuestionReason: null,
|
||||||
|
reasoningPatternValidation: null,
|
||||||
|
patternCompatibleNodeCount: 0,
|
||||||
|
incompatibleNodeIds: [],
|
||||||
|
compatibilityFailures: [],
|
||||||
|
replacementActions: [],
|
||||||
|
graphReasoningIntegrity: null,
|
||||||
plainLanguageNormalisations: [],
|
plainLanguageNormalisations: [],
|
||||||
reasoningPattern: null,
|
reasoningPattern: null,
|
||||||
questionFamily: null,
|
questionFamily: null,
|
||||||
|
|||||||
@@ -1434,5 +1434,65 @@ describe("applyValidatedProposal", () => {
|
|||||||
expect(secondResult.selectedQuestion?.question.toLowerCase()).not.toMatch(
|
expect(secondResult.selectedQuestion?.question.toLowerCase()).not.toMatch(
|
||||||
/price|budget|market size|pilot metrics|benchmark|technical differentiation/,
|
/price|budget|market size|pilot metrics|benchmark|technical differentiation/,
|
||||||
);
|
);
|
||||||
|
expect(secondResult.reasoningPatternValidation).toMatchObject({
|
||||||
|
activePattern: "decision",
|
||||||
|
valid: true,
|
||||||
|
});
|
||||||
|
expect(secondResult.graphReasoningIntegrity).toBe("valid");
|
||||||
|
expect(secondResult.incompatibleNodeIds).toEqual([]);
|
||||||
|
expect(secondResult.compatibilityFailures).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not allow a decision-mode active unknown to remain a comparison child", () => {
|
||||||
|
const graph = makeCommercialUpdateFixture();
|
||||||
|
graph.nodes.push(
|
||||||
|
makeNode({
|
||||||
|
id: "n-commercial-comparison-child",
|
||||||
|
label: "How the two observations were measured",
|
||||||
|
description:
|
||||||
|
"Need evidence about the measure used for each observation before comparing them.",
|
||||||
|
kind: "unknown",
|
||||||
|
status: "unknown",
|
||||||
|
confidence: "medium",
|
||||||
|
parentId: "n-commercial-parent",
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
graph.activeUnknownNodeId = "n-commercial-comparison-child";
|
||||||
|
|
||||||
|
const result = applyValidatedProposal({
|
||||||
|
situationGraph: graph,
|
||||||
|
proposal: makeMeaningfulNoOpProposal(),
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
expect(result.reasoningPatternValidation).toMatchObject({
|
||||||
|
activePattern: "decision",
|
||||||
|
valid: true,
|
||||||
|
});
|
||||||
|
expect(result.graphReasoningIntegrity).toBe("valid");
|
||||||
|
expect(result.incompatibleNodeIds).toContain(
|
||||||
|
"n-commercial-comparison-child",
|
||||||
|
);
|
||||||
|
expect(result.compatibilityFailures).toEqual(
|
||||||
|
expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
nodeId: "n-commercial-comparison-child",
|
||||||
|
activePattern: "decision",
|
||||||
|
nodePattern: "comparison",
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
expect(result.replacementActions).toEqual(
|
||||||
|
expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
rejectedNodeId: "n-commercial-comparison-child",
|
||||||
|
replacementNodeId: result.selectedQuestion?.nodeId,
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
expect(result.selectedQuestion?.nodeId).not.toBe(
|
||||||
|
"n-commercial-comparison-child",
|
||||||
|
);
|
||||||
|
expect(result.selectedQuestion?.reasoningPattern).toBe("decision");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1274,6 +1274,12 @@ describe("lib/graph/orchestrator startCase", () => {
|
|||||||
resolvedFirstChildNodeId,
|
resolvedFirstChildNodeId,
|
||||||
);
|
);
|
||||||
expect(initial.diagnostics.noQuestionReason).toBeNull();
|
expect(initial.diagnostics.noQuestionReason).toBeNull();
|
||||||
|
expect(initial.diagnostics.reasoningPatternValidation).toMatchObject({
|
||||||
|
activePattern: "decision",
|
||||||
|
valid: true,
|
||||||
|
});
|
||||||
|
expect(initial.diagnostics.graphReasoningIntegrity).toBe("valid");
|
||||||
|
expect(initial.diagnostics.incompatibleNodeIds).toEqual([]);
|
||||||
expect(initial.selectedQuestion?.question.toLowerCase()).not.toMatch(
|
expect(initial.selectedQuestion?.question.toLowerCase()).not.toMatch(
|
||||||
/price|budget|market size|pilot metrics|benchmark|technical differentiation/,
|
/price|budget|market size|pilot metrics|benchmark|technical differentiation/,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,282 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import { applyValidatedProposal } from "@/lib/graph/apply-proposal.js";
|
||||||
|
import {
|
||||||
|
classifyObservationRelationship,
|
||||||
|
formulateQuestion,
|
||||||
|
selectReasoningPattern,
|
||||||
|
} from "@/lib/graph/question-formulator.js";
|
||||||
|
import { makeGraph, makeNode } from "@/lib/graph/schema.js";
|
||||||
|
|
||||||
|
const COMMERCIAL_SCENARIO =
|
||||||
|
"I have developed a new reasoning method that aims to help people determine whether they have enough justified confidence to make a decision. I believe it could become a commercial product, but I do not yet know whether it solves a genuine problem, whether people would value it enough to pay for it, or whether it is fundamentally different from existing AI tools. Before investing significant time and money into building it further, I want to determine whether continuing development is commercially justified.";
|
||||||
|
|
||||||
|
function makeCommercialGraph() {
|
||||||
|
const parent = makeNode({
|
||||||
|
id: "n-commercial-parent",
|
||||||
|
label:
|
||||||
|
"Commercial justification for whether continuing development is commercially justified",
|
||||||
|
description:
|
||||||
|
"Need to know whether this solves a genuine problem, whether people would value it enough to pay for it, and whether it is commercially justified before continuing development.",
|
||||||
|
kind: "unknown",
|
||||||
|
status: "unknown",
|
||||||
|
confidence: "medium",
|
||||||
|
});
|
||||||
|
|
||||||
|
return makeGraph({
|
||||||
|
centralStatement: COMMERCIAL_SCENARIO,
|
||||||
|
nodes: [parent],
|
||||||
|
edges: [],
|
||||||
|
activeUnknownNodeId: parent.id,
|
||||||
|
resolvedNodeIds: [],
|
||||||
|
currentSummary: "Commercial pattern fixture",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeSeedProposal() {
|
||||||
|
return {
|
||||||
|
addedNodes: [
|
||||||
|
makeNode({
|
||||||
|
id: "n-anchor",
|
||||||
|
label: "Update anchor",
|
||||||
|
description:
|
||||||
|
"Anchor state introduced by the answer because the update must contain a meaningful change.",
|
||||||
|
kind: "state",
|
||||||
|
status: "known",
|
||||||
|
confidence: "low",
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
updatedNodes: [],
|
||||||
|
addedEdges: [],
|
||||||
|
removedEdgeIds: [],
|
||||||
|
resolvedUnknownNodeIds: [],
|
||||||
|
affectedNodeIds: [],
|
||||||
|
selectedQuestion: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("reasoning-pattern validation", () => {
|
||||||
|
it("keeps the commercial-method scenario in decision mode without comparability-style unknowns", () => {
|
||||||
|
const seeded = applyValidatedProposal({
|
||||||
|
situationGraph: makeCommercialGraph(),
|
||||||
|
proposal: makeSeedProposal(),
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(seeded.success).toBe(true);
|
||||||
|
expect(seeded.selectedQuestion?.reasoningPattern).toBe("decision");
|
||||||
|
expect(
|
||||||
|
seeded.updatedSituationGraph.nodes.some((node) =>
|
||||||
|
/two observations|measured|different timing/i.test(node.label),
|
||||||
|
),
|
||||||
|
).toBe(false);
|
||||||
|
|
||||||
|
const followUp = applyValidatedProposal({
|
||||||
|
situationGraph: seeded.updatedSituationGraph,
|
||||||
|
proposal: {
|
||||||
|
addedNodes: [],
|
||||||
|
updatedNodes: [
|
||||||
|
{
|
||||||
|
nodeId: seeded.selectedQuestion.nodeId,
|
||||||
|
previousStatus: "unknown",
|
||||||
|
newStatus: "resolved",
|
||||||
|
previousValue: null,
|
||||||
|
newValue:
|
||||||
|
"I experience it myself when I am trying to decide whether a project, idea or investment is justified, but I do not yet know how common that problem is for other people.",
|
||||||
|
reason: "Answered by the user.",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
addedEdges: [],
|
||||||
|
removedEdgeIds: [],
|
||||||
|
resolvedUnknownNodeIds: [seeded.selectedQuestion.nodeId],
|
||||||
|
affectedNodeIds: [],
|
||||||
|
selectedQuestion: null,
|
||||||
|
},
|
||||||
|
previousQuestion: seeded.selectedQuestion.question,
|
||||||
|
answer:
|
||||||
|
"I experience it myself when I am trying to decide whether a project, idea or investment is justified, but I do not yet know how common that problem is for other people.",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(followUp.success).toBe(true);
|
||||||
|
expect(followUp.selectedQuestion?.reasoningPattern).toBe("decision");
|
||||||
|
expect(followUp.reasoningPatternValidation).toMatchObject({
|
||||||
|
activePattern: "decision",
|
||||||
|
valid: true,
|
||||||
|
});
|
||||||
|
expect(followUp.graphReasoningIntegrity).toBe("valid");
|
||||||
|
expect(followUp.incompatibleNodeIds).toEqual([]);
|
||||||
|
expect(followUp.selectedQuestion?.question.toLowerCase()).not.toMatch(
|
||||||
|
/two observations|measured|same basis|same scale|different timing/,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("allows comparability-style reasoning in an explanation scenario", () => {
|
||||||
|
const unknown = makeNode({
|
||||||
|
id: "n-explanation",
|
||||||
|
label:
|
||||||
|
"Explanation for why revenue increased by 18%, but cash in the bank fell over the same period",
|
||||||
|
description:
|
||||||
|
"Need to understand what change or event could explain why these observations differ.",
|
||||||
|
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: [
|
||||||
|
unknown,
|
||||||
|
makeNode({
|
||||||
|
id: "n-revenue",
|
||||||
|
label: "Revenue increased by 18%.",
|
||||||
|
description: "Revenue increased by 18%.",
|
||||||
|
kind: "observation",
|
||||||
|
status: "supported",
|
||||||
|
confidence: "high",
|
||||||
|
}),
|
||||||
|
makeNode({
|
||||||
|
id: "n-cash",
|
||||||
|
label: "Cash in the bank fell over the same period.",
|
||||||
|
description: "Cash in the bank fell over the same period.",
|
||||||
|
kind: "observation",
|
||||||
|
status: "supported",
|
||||||
|
confidence: "high",
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
edges: [],
|
||||||
|
activeUnknownNodeId: unknown.id,
|
||||||
|
resolvedNodeIds: [],
|
||||||
|
currentSummary: "Explanation fixture",
|
||||||
|
});
|
||||||
|
|
||||||
|
const pattern = selectReasoningPattern({ node: unknown, graph });
|
||||||
|
const question = formulateQuestion({ node: unknown, graph });
|
||||||
|
const relationship = classifyObservationRelationship(graph);
|
||||||
|
|
||||||
|
expect(pattern.pattern).toBe("explanation");
|
||||||
|
expect(question.reasoningPattern).toBe("explanation");
|
||||||
|
expect(relationship.questionRequired).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects explanation-family tie resolution for duplicate observations", () => {
|
||||||
|
const graph = makeGraph({
|
||||||
|
centralStatement: "Sales doubled. Sales doubled.",
|
||||||
|
nodes: [
|
||||||
|
makeNode({
|
||||||
|
id: "n-sales-1",
|
||||||
|
label: "Sales doubled.",
|
||||||
|
description: "Sales doubled.",
|
||||||
|
kind: "observation",
|
||||||
|
status: "supported",
|
||||||
|
confidence: "high",
|
||||||
|
}),
|
||||||
|
makeNode({
|
||||||
|
id: "n-sales-2",
|
||||||
|
label: "Sales doubled.",
|
||||||
|
description: "Sales doubled.",
|
||||||
|
kind: "observation",
|
||||||
|
status: "supported",
|
||||||
|
confidence: "high",
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
edges: [],
|
||||||
|
activeUnknownNodeId: null,
|
||||||
|
resolvedNodeIds: [],
|
||||||
|
currentSummary: "Duplicate observation fixture",
|
||||||
|
});
|
||||||
|
|
||||||
|
const relationship = classifyObservationRelationship(graph);
|
||||||
|
|
||||||
|
expect(relationship.relationshipStatus).toBe("duplicate");
|
||||||
|
expect(relationship.questionRequired).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps definition scenarios inside compatible node families", () => {
|
||||||
|
const unknown = makeNode({
|
||||||
|
id: "n-definition",
|
||||||
|
label: "Definition of justified confidence",
|
||||||
|
description: "The term needs clearer boundaries.",
|
||||||
|
kind: "unknown",
|
||||||
|
status: "unknown",
|
||||||
|
confidence: "medium",
|
||||||
|
});
|
||||||
|
const graph = makeGraph({
|
||||||
|
centralStatement: "The team uses justified confidence inconsistently.",
|
||||||
|
nodes: [unknown],
|
||||||
|
edges: [],
|
||||||
|
activeUnknownNodeId: unknown.id,
|
||||||
|
resolvedNodeIds: [],
|
||||||
|
currentSummary: "Definition fixture",
|
||||||
|
});
|
||||||
|
|
||||||
|
const pattern = selectReasoningPattern({ node: unknown, graph });
|
||||||
|
const result = formulateQuestion({ node: unknown, graph });
|
||||||
|
|
||||||
|
expect(pattern.pattern).toBe("definition");
|
||||||
|
expect(result.reasoningPattern).toBe("definition");
|
||||||
|
expect(result.allowedQuestionFamilies).toEqual(["definition"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("replaces an incompatible decision-mode active unknown with a pattern-compatible candidate", () => {
|
||||||
|
const graph = makeGraph({
|
||||||
|
centralStatement:
|
||||||
|
"Before investing more, we need to know whether continuing development is commercially justified.",
|
||||||
|
nodes: [
|
||||||
|
makeNode({
|
||||||
|
id: "n-commercial-parent",
|
||||||
|
label:
|
||||||
|
"Commercial justification for whether continuing development is commercially justified",
|
||||||
|
description:
|
||||||
|
"Need to know whether this solves a genuine problem before continuing development.",
|
||||||
|
kind: "unknown",
|
||||||
|
status: "unknown",
|
||||||
|
confidence: "medium",
|
||||||
|
}),
|
||||||
|
makeNode({
|
||||||
|
id: "n-incompatible-child",
|
||||||
|
label: "How the two observations were measured",
|
||||||
|
description:
|
||||||
|
"Need evidence about the measure used for each observation before comparing them.",
|
||||||
|
kind: "unknown",
|
||||||
|
status: "unknown",
|
||||||
|
confidence: "medium",
|
||||||
|
parentId: "n-commercial-parent",
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
edges: [],
|
||||||
|
activeUnknownNodeId: "n-incompatible-child",
|
||||||
|
resolvedNodeIds: [],
|
||||||
|
currentSummary: "Incompatible active unknown fixture",
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = applyValidatedProposal({
|
||||||
|
situationGraph: graph,
|
||||||
|
proposal: makeSeedProposal(),
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
expect(result.reasoningPatternValidation).toMatchObject({
|
||||||
|
activePattern: "decision",
|
||||||
|
valid: true,
|
||||||
|
});
|
||||||
|
expect(result.graphReasoningIntegrity).toBe("valid");
|
||||||
|
expect(result.incompatibleNodeIds).toContain("n-incompatible-child");
|
||||||
|
expect(result.compatibilityFailures).toEqual(
|
||||||
|
expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
nodeId: "n-incompatible-child",
|
||||||
|
activePattern: "decision",
|
||||||
|
nodePattern: "comparison",
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
expect(result.replacementActions).toEqual(
|
||||||
|
expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
rejectedNodeId: "n-incompatible-child",
|
||||||
|
replacementNodeId: result.selectedQuestion?.nodeId,
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
expect(result.selectedQuestion?.nodeId).not.toBe("n-incompatible-child");
|
||||||
|
expect(result.selectedQuestion?.reasoningPattern).toBe("decision");
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user