feat: enforce reasoning pattern consistency

This commit is contained in:
2026-08-03 12:10:57 +01:00
parent 3e2edd2edc
commit c27320984c
6 changed files with 931 additions and 17 deletions
+502 -17
View File
@@ -7,6 +7,7 @@ import {
COMPARABILITY_REASONING_NODE_ID,
formulateQuestion,
formulateTieResolutionQuestion,
selectReasoningPattern,
} from "./question-formulator.js";
import {
graphUpdateSchema,
@@ -1727,12 +1728,280 @@ function selectedQuestionBelongsToChild(graph, selectedQuestion) {
return Boolean(findNodeById(graph, selectedQuestion.nodeId)?.parentId);
}
function selectDecompositionChildCandidate(graph, parentNodeId) {
const childCandidates = findDirectChildUnknowns(graph, parentNodeId).filter(
(node) =>
node.kind === "unknown" &&
!["resolved", "contradicted"].includes(node.status),
const ALLOWED_NODE_PATTERNS_BY_ACTIVE_PATTERN = {
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.kind === "unknown" &&
!["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) {
return { status: "none", nodeId: null, tiedCandidateIds: [] };
@@ -1926,6 +2195,11 @@ function runDeterministicDecomposition({
let decompositionTriggeredByQuestionComplexity = false;
let decompositionTriggeredByAnswerability = false;
let selectedContainerUnknown = null;
let activeReasoningPattern = null;
let activeReasoningPatternReason = null;
let incompatibleNodeIds = [];
let compatibilityFailures = [];
let replacementActions = [];
while (workingSelection?.status === "selected" && workingSelection?.nodeId) {
const selectedNode = findNodeById(workingGraph, workingSelection.nodeId);
@@ -1935,6 +2209,53 @@ function runDeterministicDecomposition({
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({
node: selectedNode,
graph: workingGraph,
@@ -1979,6 +2300,7 @@ function runDeterministicDecomposition({
const childSelection = selectDecompositionChildCandidate(
workingGraph,
selectedNode.id,
activeReasoningPattern,
);
if (childSelection.status === "selected") {
workingSelection = childSelection;
@@ -2013,6 +2335,7 @@ function runDeterministicDecomposition({
selectedNode,
workingGraph,
decompositionDepth,
activeReasoningPattern,
);
proposedChildCount = decomposition.proposedChildCount;
@@ -2061,6 +2384,7 @@ function runDeterministicDecomposition({
workingSelection = selectDecompositionChildCandidate(
workingGraph,
selectedNode.id,
activeReasoningPattern,
);
if (workingSelection?.status !== "selected") {
@@ -2112,6 +2436,11 @@ function runDeterministicDecomposition({
decompositionTriggeredByQuestionComplexity,
decompositionTriggeredByAnswerability,
selectedContainerUnknown,
activeReasoningPattern,
activeReasoningPatternReason,
incompatibleNodeIds,
compatibilityFailures,
replacementActions,
};
}
@@ -2446,20 +2775,75 @@ export function applyValidatedProposal({
const resolvedCurrentTurnNodeIds = [
...new Set(proposalSnapshot.resolvedUnknownNodeIds || []),
];
deterministicSelection = isSelectableUnresolvedUnknown(
let postPropagationIncompatibleNodeIds = [];
let postPropagationCompatibilityFailures = [];
let postPropagationReplacementActions = [];
let activeUnknownIncompatibleNodeIds = [];
let activeUnknownCompatibilityFailures = [];
let activeUnknownReplacementActions = [];
const preservedSelectedChildNode = isSelectableUnresolvedUnknown(
updatedSituationGraph,
decompositionResult.selectedChildNodeId,
)
? {
status: "selected",
nodeId: decompositionResult.selectedChildNodeId,
reason:
"Preserved the selected decomposition child because it remains unresolved after propagation.",
}
: selectActiveUnknownCandidate(
? findNodeById(
updatedSituationGraph,
updatedSituationGraph.resolvedNodeIds,
);
decompositionResult.selectedChildNodeId,
)
: null;
const preservedSelectedChildCompatibility = preservedSelectedChildNode
? assessReasoningPatternCompatibility({
node: preservedSelectedChildNode,
graph: updatedSituationGraph,
activePattern: decompositionResult.activeReasoningPattern,
})
: null;
if (
preservedSelectedChildNode &&
preservedSelectedChildCompatibility?.compatible
) {
deterministicSelection = {
status: "selected",
nodeId: decompositionResult.selectedChildNodeId,
reason:
"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.",
});
}
}
if (deterministicSelection?.status === "ambiguous") {
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(
updatedSituationGraph,
resolvedCurrentTurnNodeIds,
);
const eligibleCandidates = listEligibleUnknownCandidates(
const eligibleCandidatesBeforeCompatibility = listEligibleUnknownCandidates(
updatedSituationGraph,
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 answerabilityAssessment = decompositionResult.answerabilityAssessment;
@@ -2603,6 +3049,14 @@ export function applyValidatedProposal({
}
: null;
const finalSelectionCompatibility = finalSelectedQuestion?.nodeId
? assessReasoningPatternCompatibility({
node: findNodeById(updatedSituationGraph, finalSelectedQuestion.nodeId),
graph: updatedSituationGraph,
activePattern: decompositionResult.activeReasoningPattern,
})
: null;
const finalSelectedChildNodeId =
selectedChildNodeId ??
(selectedQuestionBelongsToChild(
@@ -2638,7 +3092,8 @@ export function applyValidatedProposal({
!resultGraphValidation.success ||
!resultReferenceValidation?.valid ||
resultDuplicateNodeIds.length > 0 ||
resultDuplicateEdgeIds.length > 0
resultDuplicateEdgeIds.length > 0 ||
(finalSelectedQuestion?.nodeId && !finalSelectionCompatibility?.compatible)
) {
return {
success: false,
@@ -2658,6 +3113,12 @@ export function applyValidatedProposal({
({ edgeId, count }) =>
`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,
selectedContainerUnknown:
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:
finalSelectedChildNodeId ??
(deterministicSelection?.status === "selected"
+63
View File
@@ -65,6 +65,12 @@ function buildDiagnostics({
rejectedQuestionFamilies,
selectedQuestionTemplate,
reasoningPatternReason,
reasoningPatternValidation,
patternCompatibleNodeCount,
incompatibleNodeIds,
compatibilityFailures,
replacementActions,
graphReasoningIntegrity,
}) {
return {
promptVersion: analysis?.promptVersion ?? null,
@@ -100,6 +106,12 @@ function buildDiagnostics({
rejectedQuestionFamilies: rejectedQuestionFamilies ?? [],
selectedQuestionTemplate: selectedQuestionTemplate ?? 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,
resolvedCurrentTurnNodeIds,
noQuestionReason,
reasoningPatternValidation,
patternCompatibleNodeCount,
incompatibleNodeIds,
compatibilityFailures,
replacementActions,
graphReasoningIntegrity,
}) {
return {
promptVersion: promptVersion ?? "v0.4",
@@ -291,6 +309,12 @@ function buildUpdateDiagnostics({
candidateNodeIds: candidateNodeIds ?? [],
resolvedCurrentTurnNodeIds: resolvedCurrentTurnNodeIds ?? [],
noQuestionReason: noQuestionReason ?? null,
reasoningPatternValidation: reasoningPatternValidation ?? null,
patternCompatibleNodeCount: patternCompatibleNodeCount ?? 0,
incompatibleNodeIds: incompatibleNodeIds ?? [],
compatibilityFailures: compatibilityFailures ?? [],
replacementActions: replacementActions ?? [],
graphReasoningIntegrity: graphReasoningIntegrity ?? null,
unknownSelectionExplanation: unknownSelectionExplanation ?? null,
};
}
@@ -419,6 +443,16 @@ export async function startCase(body) {
reasoningPatternReason:
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,
}),
validationErrors: graphReferenceValidation.errors,
statusCode: 500,
@@ -475,6 +509,15 @@ export async function startCase(body) {
null,
reasoningPatternReason:
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: [],
resolvedCurrentTurnNodeIds: [],
noQuestionReason: null,
reasoningPatternValidation: null,
patternCompatibleNodeCount: 0,
incompatibleNodeIds: [],
compatibilityFailures: [],
replacementActions: [],
graphReasoningIntegrity: null,
plainLanguageNormalisations: [],
unknownSelectionExplanation: explainUnknownSelection(
situationGraph,
@@ -780,6 +829,14 @@ async function updateCaseWithDependencies(body, dependencies = {}) {
resolvedCurrentTurnNodeIds:
applicationResult.resolvedCurrentTurnNodeIds,
noQuestionReason: applicationResult.noQuestionReason,
reasoningPatternValidation:
applicationResult.reasoningPatternValidation,
patternCompatibleNodeCount:
applicationResult.patternCompatibleNodeCount,
incompatibleNodeIds: applicationResult.incompatibleNodeIds,
compatibilityFailures: applicationResult.compatibilityFailures,
replacementActions: applicationResult.replacementActions,
graphReasoningIntegrity: applicationResult.graphReasoningIntegrity,
plainLanguageNormalisations:
applicationResult.plainLanguageNormalisations,
reasoningPattern:
@@ -876,6 +933,12 @@ async function updateCaseWithDependencies(body, dependencies = {}) {
candidateNodeIds: [],
resolvedCurrentTurnNodeIds: [],
noQuestionReason: null,
reasoningPatternValidation: null,
patternCompatibleNodeCount: 0,
incompatibleNodeIds: [],
compatibilityFailures: [],
replacementActions: [],
graphReasoningIntegrity: null,
plainLanguageNormalisations: [],
reasoningPattern: null,
questionFamily: null,