feat: decompose composite unknowns before questioning
This commit is contained in:
@@ -44,10 +44,10 @@ When a selected unknown is composite:
|
||||
|
||||
For the current relationship-explanation experiment, the broad child dimensions are:
|
||||
|
||||
- Timing or measurement basis
|
||||
- Whether the two observations reflect different timing
|
||||
- How the two observations were measured
|
||||
- Change affecting signal A more than signal B
|
||||
- Change affecting signal B more than signal A
|
||||
- Mix or segment shift
|
||||
- One-off event during the period
|
||||
|
||||
These are intentionally non-jargon and broad enough to generalise across scenarios like:
|
||||
@@ -63,6 +63,16 @@ These are intentionally non-jargon and broad enough to generalise across scenari
|
||||
The orchestrator now reports:
|
||||
|
||||
- `atomicityAssessment`
|
||||
- `atomicityDecisionReason`
|
||||
- `decompositionDepth`
|
||||
- `decompositionAttempted`
|
||||
- `decompositionAccepted`
|
||||
- `decompositionStoppedReason`
|
||||
- `proposedChildCount`
|
||||
- `acceptedChildCount`
|
||||
- `rejectedChildren`
|
||||
- `selectedChildNodeId`
|
||||
- `childQualitySummary`
|
||||
- `decompositionPerformed`
|
||||
- `childUnknownCount`
|
||||
- `childNodeIds`
|
||||
@@ -82,10 +92,12 @@ After this change:
|
||||
- the engine decomposes it into child unknowns first
|
||||
- the next asked question is backed by a more focused child unknown
|
||||
- repeated updates reuse the same decomposition children deterministically
|
||||
- child-quality checks reject compound or duplicate children before they enter the graph
|
||||
- decomposition stops deterministically once a selected child is directly answerable
|
||||
|
||||
In the revenue-versus-cash case, the selected next question becomes:
|
||||
|
||||
> What evidence would clarify timing or measurement basis?
|
||||
> What evidence would clarify how the two observations were measured?
|
||||
|
||||
rather than asking the full broad explanation node directly.
|
||||
|
||||
@@ -93,13 +105,14 @@ rather than asking the full broad explanation node directly.
|
||||
|
||||
This supports the idea that recursive decomposition is a fundamental part of graph-backed questioning, not just a prompt refinement.
|
||||
|
||||
The main remaining limitation is that the new child set can still produce ties among equally broad dimensions. In the current implementation, that is acceptable because the graph now makes the ambiguity explicit rather than hiding it in a single broad parent question.
|
||||
The main remaining limitation is that some decompositions can still produce equally justified child candidates. The current implementation handles that deterministically by exposing the ambiguity in diagnostics and, when possible, reusing the existing selector to pick a specific atomic child. That is still preferable to hiding the ambiguity inside one broad parent question.
|
||||
|
||||
## Validation run
|
||||
|
||||
Covered by:
|
||||
|
||||
- `tests/graph/atomicity-assessment.test.js`
|
||||
- `tests/graph/decomposition-quality.test.js`
|
||||
- `tests/graph/apply-proposal.test.js`
|
||||
- `tests/graph/orchestrator.test.js`
|
||||
- `tests/graph/question-formulator.test.js`
|
||||
|
||||
+475
-112
@@ -602,6 +602,145 @@ function buildDecompositionContext(graph) {
|
||||
centralStatement: stripTrailingPunctuation(graph.centralStatement),
|
||||
firstConcept: firstConcept || "the first signal",
|
||||
secondConcept: secondConcept || "the second signal",
|
||||
firstObservationLabel: stripTrailingPunctuation(
|
||||
firstObservation?.label || "",
|
||||
),
|
||||
secondObservationLabel: stripTrailingPunctuation(
|
||||
secondObservation?.label || "",
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
export const MAX_DECOMPOSITION_DEPTH = 2;
|
||||
|
||||
function splitSemanticTokens(value) {
|
||||
return normaliseText(value)
|
||||
.split(" ")
|
||||
.filter((token) => token.length > 2);
|
||||
}
|
||||
|
||||
function buildSemanticSignature(node) {
|
||||
return normaliseText(`${node?.label || ""} ${node?.description || ""}`);
|
||||
}
|
||||
|
||||
function calculateTokenOverlapRatio(aTokens, bTokens) {
|
||||
const a = new Set(aTokens);
|
||||
const b = new Set(bTokens);
|
||||
const intersection = [...a].filter((token) => b.has(token)).length;
|
||||
const largest = Math.max(a.size, b.size, 1);
|
||||
return intersection / largest;
|
||||
}
|
||||
|
||||
function detectCompoundSignals(text) {
|
||||
const signals = [];
|
||||
|
||||
if (/\b(and|or)\b/.test(text)) {
|
||||
if (
|
||||
/\b(timing|measurement|basis|cost|costs|debt|stock|tax|capital|mix|segment)\b[^.]{0,30}\b(and|or)\b[^.]{0,30}\b(timing|measurement|basis|cost|costs|debt|stock|tax|capital|mix|segment)\b/.test(
|
||||
text,
|
||||
)
|
||||
) {
|
||||
signals.push("conjoined_distinct_concepts");
|
||||
}
|
||||
}
|
||||
|
||||
if (/\b[a-z]+\s*\/\s*[a-z]+\b/.test(text)) {
|
||||
signals.push("slash_separated_categories");
|
||||
}
|
||||
|
||||
if (/,[^,]{0,20},/.test(text) || /,\s*[^,]+\s+or\s+[^,]+/.test(text)) {
|
||||
signals.push("comma_separated_category_list");
|
||||
}
|
||||
|
||||
if (/\btiming or measurement basis\b/.test(text)) {
|
||||
signals.push("timing_or_measurement_basis");
|
||||
}
|
||||
|
||||
return [...new Set(signals)];
|
||||
}
|
||||
|
||||
function isDirectlyAnswerableChildText(text) {
|
||||
return !/\b(explanation for why|possible causes|possible reasons|what changed|difference between|divergence|moved differently|factors behind|factors affecting)\b/.test(
|
||||
text,
|
||||
);
|
||||
}
|
||||
|
||||
function buildRejectedChildRecord(childNode, reasons, compoundSignals) {
|
||||
return {
|
||||
nodeId: childNode.id,
|
||||
label: childNode.label,
|
||||
reasons,
|
||||
compoundSignals,
|
||||
};
|
||||
}
|
||||
|
||||
function mergeUniqueRecords(existing = [], incoming = [], key = "nodeId") {
|
||||
const merged = new Map((existing || []).map((item) => [item[key], item]));
|
||||
for (const item of incoming || []) {
|
||||
merged.set(item[key], item);
|
||||
}
|
||||
return [...merged.values()];
|
||||
}
|
||||
|
||||
function cloneNode(node) {
|
||||
return JSON.parse(JSON.stringify(node));
|
||||
}
|
||||
|
||||
export function assessChildUnknownQuality({
|
||||
parentNode,
|
||||
childNode,
|
||||
siblingNodes,
|
||||
graph,
|
||||
}) {
|
||||
const parentSignature = buildSemanticSignature(parentNode);
|
||||
const childSignature = buildSemanticSignature(childNode);
|
||||
const parentTokens = splitSemanticTokens(parentSignature);
|
||||
const childTokens = splitSemanticTokens(childSignature);
|
||||
const compoundSignals = detectCompoundSignals(childSignature);
|
||||
const duplicateSiblingIds = (siblingNodes || [])
|
||||
.filter((sibling) => sibling.id !== childNode.id)
|
||||
.filter((sibling) => buildSemanticSignature(sibling) === childSignature)
|
||||
.map((sibling) => sibling.id);
|
||||
const reasons = [];
|
||||
|
||||
const narrowerThanParent =
|
||||
childSignature !== parentSignature &&
|
||||
(childTokens.length < parentTokens.length ||
|
||||
calculateTokenOverlapRatio(parentTokens, childTokens) < 0.8);
|
||||
|
||||
const directlyAnswerable =
|
||||
isDirectlyAnswerableChildText(childSignature) &&
|
||||
compoundSignals.length === 0;
|
||||
|
||||
const independent =
|
||||
duplicateSiblingIds.length === 0 && compoundSignals.length === 0;
|
||||
const atomic = narrowerThanParent && directlyAnswerable && independent;
|
||||
|
||||
if (!narrowerThanParent) {
|
||||
reasons.push("not_narrower_than_parent");
|
||||
}
|
||||
if (!directlyAnswerable) {
|
||||
reasons.push("not_directly_answerable");
|
||||
}
|
||||
if (compoundSignals.length > 0) {
|
||||
reasons.push("compound_child");
|
||||
}
|
||||
if (duplicateSiblingIds.length > 0) {
|
||||
reasons.push("duplicate_sibling");
|
||||
}
|
||||
if ((graph?.resolvedNodeIds || []).includes(childNode.id)) {
|
||||
reasons.push("already_resolved");
|
||||
}
|
||||
|
||||
return {
|
||||
valid: reasons.length === 0,
|
||||
atomic,
|
||||
directlyAnswerable,
|
||||
independent,
|
||||
narrowerThanParent,
|
||||
compoundSignals,
|
||||
duplicateSiblingIds,
|
||||
reasons,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -642,62 +781,127 @@ function findEquivalentDecompositionChild(
|
||||
});
|
||||
}
|
||||
|
||||
function buildCompositeUnknownChildren(parentNode, graph) {
|
||||
const context = buildDecompositionContext(graph);
|
||||
const templates = [
|
||||
{
|
||||
label: "Timing or measurement basis",
|
||||
description: `Need evidence about whether a timing or measurement-basis difference could explain ${context.centralStatement}, because that would change how the observations should be interpreted.`,
|
||||
},
|
||||
{
|
||||
label: `Change affecting ${context.firstConcept} more than ${context.secondConcept}`,
|
||||
description: `Need to know whether something changed that affected ${context.firstConcept} more than ${context.secondConcept}, because that could explain ${context.centralStatement}.`,
|
||||
},
|
||||
{
|
||||
label: `Change affecting ${context.secondConcept} more than ${context.firstConcept}`,
|
||||
description: `Need to know whether something changed that affected ${context.secondConcept} more than ${context.firstConcept}, because that could explain ${context.centralStatement}.`,
|
||||
},
|
||||
{
|
||||
label: "Mix or segment shift",
|
||||
description: `Need to know whether the mix of customers, products, orders, or cases changed, because that could explain ${context.centralStatement}.`,
|
||||
},
|
||||
{
|
||||
label: "One-off event during the period",
|
||||
description: `Need to know whether a one-off event or unusual change happened during the period, because that could explain ${context.centralStatement}.`,
|
||||
},
|
||||
];
|
||||
function describeObservationFocus(context, which) {
|
||||
const concept =
|
||||
which === "first" ? context.firstConcept : context.secondConcept;
|
||||
const label =
|
||||
which === "first"
|
||||
? context.firstObservationLabel
|
||||
: context.secondObservationLabel;
|
||||
|
||||
if (concept && !concept.startsWith("the ")) return concept;
|
||||
if (label) return label.toLowerCase();
|
||||
return which === "first" ? "the first observation" : "the second observation";
|
||||
}
|
||||
|
||||
function buildDecompositionTemplates(parentNode, graph, depth = 0) {
|
||||
const context = buildDecompositionContext(graph);
|
||||
const firstFocus = describeObservationFocus(context, "first");
|
||||
const secondFocus = describeObservationFocus(context, "second");
|
||||
|
||||
if (/\btiming or measurement basis\b/i.test(parentNode.label)) {
|
||||
return [
|
||||
{
|
||||
label: "Whether the two observations reflect different timing",
|
||||
description: `Need to know whether the two observations reflect different timing, because that would help resolve ${context.centralStatement}.`,
|
||||
},
|
||||
{
|
||||
label: "How the two observations were measured",
|
||||
description: `Need evidence about the measure used for each observation, because that would help resolve ${context.centralStatement}.`,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
label: "Whether the two observations reflect different timing",
|
||||
description: `Need to know whether the two observations reflect different timing, because that could help explain ${context.centralStatement}.`,
|
||||
},
|
||||
{
|
||||
label: "How the two observations were measured",
|
||||
description: `Need evidence about the measure used for each observation, because that could help explain ${context.centralStatement}.`,
|
||||
},
|
||||
{
|
||||
label: `Change mainly affecting ${firstFocus}`,
|
||||
description: `Need to know whether a change mainly affected ${firstFocus}, because that could help explain ${context.centralStatement}.`,
|
||||
},
|
||||
{
|
||||
label: `Change mainly affecting ${secondFocus}`,
|
||||
description: `Need to know whether a change mainly affected ${secondFocus}, because that could help explain ${context.centralStatement}.`,
|
||||
},
|
||||
depth === 0
|
||||
? {
|
||||
label: "One-off event during the period",
|
||||
description: `Need to know whether a one-off event happened during the period, because that could help explain ${context.centralStatement}.`,
|
||||
}
|
||||
: {
|
||||
label: "Mix shift during the period",
|
||||
description: `Need to know whether the mix of cases, customers, or items shifted during the period, because that could help explain ${context.centralStatement}.`,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
function buildCompositeUnknownChildren(parentNode, graph, depth = 0) {
|
||||
const templates = buildDecompositionTemplates(parentNode, graph, depth);
|
||||
|
||||
const candidateNodes = templates.map(
|
||||
(template) =>
|
||||
findEquivalentDecompositionChild(
|
||||
graph,
|
||||
parentNode.id,
|
||||
template.label,
|
||||
template.description,
|
||||
) || {
|
||||
id: buildDecompositionChildId(parentNode.id, template.label),
|
||||
label: template.label,
|
||||
description: template.description,
|
||||
kind: "unknown",
|
||||
status: "unknown",
|
||||
confidence: "medium",
|
||||
value: null,
|
||||
unit: null,
|
||||
evidenceIds: [],
|
||||
dependsOn: [],
|
||||
affects: [],
|
||||
parentId: parentNode.id,
|
||||
childIds: [],
|
||||
},
|
||||
);
|
||||
const childNodes = [];
|
||||
const childEdges = [];
|
||||
const childNodeIds = [];
|
||||
let createdCount = 0;
|
||||
const rejectedChildren = [];
|
||||
const childQualitySummary = [];
|
||||
|
||||
for (const template of templates) {
|
||||
const existingNode = findEquivalentDecompositionChild(
|
||||
for (const childNode of candidateNodes) {
|
||||
const quality = assessChildUnknownQuality({
|
||||
parentNode,
|
||||
childNode,
|
||||
siblingNodes: candidateNodes,
|
||||
graph,
|
||||
parentNode.id,
|
||||
template.label,
|
||||
template.description,
|
||||
);
|
||||
const childNode = existingNode || {
|
||||
id: buildDecompositionChildId(parentNode.id, template.label),
|
||||
label: template.label,
|
||||
description: template.description,
|
||||
kind: "unknown",
|
||||
status: "unknown",
|
||||
confidence: "medium",
|
||||
value: null,
|
||||
unit: null,
|
||||
evidenceIds: [],
|
||||
dependsOn: [],
|
||||
affects: [],
|
||||
parentId: parentNode.id,
|
||||
childIds: [],
|
||||
};
|
||||
});
|
||||
childQualitySummary.push({
|
||||
nodeId: childNode.id,
|
||||
label: childNode.label,
|
||||
valid: quality.valid,
|
||||
atomic: quality.atomic,
|
||||
reasons: quality.reasons,
|
||||
});
|
||||
|
||||
if (!quality.valid) {
|
||||
rejectedChildren.push(
|
||||
buildRejectedChildRecord(
|
||||
childNode,
|
||||
quality.reasons,
|
||||
quality.compoundSignals,
|
||||
),
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
childNodeIds.push(childNode.id);
|
||||
|
||||
if (existingNode) {
|
||||
if ((graph.nodes || []).some((node) => node.id === childNode.id)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -711,21 +915,197 @@ function buildCompositeUnknownChildren(parentNode, graph) {
|
||||
description:
|
||||
"This child unknown must be investigated before the broader parent explanation can be resolved.",
|
||||
});
|
||||
createdCount += 1;
|
||||
}
|
||||
|
||||
const acceptedChildCount = childNodeIds.length;
|
||||
const proposedChildCount = candidateNodes.length;
|
||||
|
||||
if (acceptedChildCount < 2) {
|
||||
return {
|
||||
accepted: false,
|
||||
childNodes: [],
|
||||
childEdges: [],
|
||||
childNodeIds: [],
|
||||
proposedChildCount,
|
||||
acceptedChildCount,
|
||||
rejectedChildren,
|
||||
childQualitySummary,
|
||||
reason:
|
||||
acceptedChildCount === 0
|
||||
? "Decomposition stopped because all proposed children failed quality checks."
|
||||
: "Decomposition stopped because fewer than two valid child unknowns remained after quality checks.",
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
accepted: true,
|
||||
childNodes,
|
||||
childEdges,
|
||||
childNodeIds,
|
||||
createdCount,
|
||||
proposedChildCount,
|
||||
acceptedChildCount,
|
||||
rejectedChildren,
|
||||
childQualitySummary,
|
||||
reason:
|
||||
createdCount > 0
|
||||
childNodes.length > 0
|
||||
? "Decomposed a composite unknown into smaller broad candidate dimensions before asking the next question."
|
||||
: "Reused existing decomposition children for the composite unknown before asking the next question.",
|
||||
};
|
||||
}
|
||||
|
||||
function findNodeById(graph, nodeId) {
|
||||
return (graph.nodes || []).find((node) => node.id === nodeId) || null;
|
||||
}
|
||||
|
||||
function runDeterministicDecomposition({
|
||||
graphSnapshot,
|
||||
proposalSnapshot,
|
||||
updatedSituationGraph,
|
||||
reasoningResolution,
|
||||
deterministicSelection,
|
||||
}) {
|
||||
let workingGraph = updatedSituationGraph;
|
||||
let workingSelection = deterministicSelection;
|
||||
let workingProposal = proposalSnapshot;
|
||||
let nextReasoningState = workingGraph.reasoningState;
|
||||
let lastAtomicityAssessment = null;
|
||||
let rootAtomicityAssessment = null;
|
||||
let decompositionDepth = 0;
|
||||
let decompositionAttempted = false;
|
||||
let decompositionAccepted = false;
|
||||
let proposedChildCount = 0;
|
||||
let acceptedChildCount = 0;
|
||||
let selectedChildNodeId = null;
|
||||
let decompositionStoppedReason = null;
|
||||
let rejectedChildren = [];
|
||||
let childQualitySummary = [];
|
||||
|
||||
while (workingSelection?.status === "selected" && workingSelection?.nodeId) {
|
||||
const selectedNode = findNodeById(workingGraph, workingSelection.nodeId);
|
||||
if (!selectedNode) {
|
||||
decompositionStoppedReason =
|
||||
"Selected node was not present in the updated graph.";
|
||||
break;
|
||||
}
|
||||
|
||||
const atomicityAssessment = assessUnknownAtomicity({
|
||||
node: selectedNode,
|
||||
graph: workingGraph,
|
||||
});
|
||||
lastAtomicityAssessment = atomicityAssessment;
|
||||
if (!rootAtomicityAssessment) {
|
||||
rootAtomicityAssessment = atomicityAssessment;
|
||||
}
|
||||
|
||||
if (atomicityAssessment.atomicity === "atomic") {
|
||||
selectedChildNodeId = decompositionDepth > 0 ? selectedNode.id : null;
|
||||
decompositionStoppedReason =
|
||||
decompositionDepth > 0
|
||||
? "Selected child is atomic and directly answerable."
|
||||
: "Selected unknown is already atomic.";
|
||||
break;
|
||||
}
|
||||
|
||||
if (decompositionDepth >= MAX_DECOMPOSITION_DEPTH) {
|
||||
decompositionStoppedReason =
|
||||
"Maximum decomposition depth reached before finding a smaller atomic child.";
|
||||
break;
|
||||
}
|
||||
|
||||
decompositionAttempted = true;
|
||||
const decomposition = buildCompositeUnknownChildren(
|
||||
selectedNode,
|
||||
workingGraph,
|
||||
decompositionDepth,
|
||||
);
|
||||
|
||||
proposedChildCount = decomposition.proposedChildCount;
|
||||
acceptedChildCount = decomposition.acceptedChildCount;
|
||||
rejectedChildren = mergeUniqueRecords(
|
||||
rejectedChildren,
|
||||
decomposition.rejectedChildren,
|
||||
);
|
||||
childQualitySummary = mergeUniqueRecords(
|
||||
childQualitySummary,
|
||||
decomposition.childQualitySummary,
|
||||
);
|
||||
|
||||
if (!decomposition.accepted) {
|
||||
decompositionStoppedReason = decomposition.reason;
|
||||
break;
|
||||
}
|
||||
|
||||
const previousGraph = cloneJsonSafe(workingGraph);
|
||||
const previousProposal = cloneJsonSafe(workingProposal);
|
||||
const previousReasoningState = cloneJsonSafe(nextReasoningState);
|
||||
|
||||
workingProposal.addedNodes.push(...decomposition.childNodes.map(cloneNode));
|
||||
workingProposal.addedEdges.push(...decomposition.childEdges.map(cloneNode));
|
||||
|
||||
const applied = applyGraphUpdate(graphSnapshot, workingProposal);
|
||||
if (!applied.success) {
|
||||
return {
|
||||
success: false,
|
||||
stage: "application",
|
||||
errors: applied.errors,
|
||||
};
|
||||
}
|
||||
|
||||
workingGraph = {
|
||||
...graphSnapshot,
|
||||
nodes: applied.nodes,
|
||||
edges: applied.edges,
|
||||
resolvedNodeIds: applied.resolvedNodeIds,
|
||||
};
|
||||
nextReasoningState = buildReasoningState(
|
||||
workingGraph,
|
||||
reasoningResolution.reasoningStateOverride,
|
||||
);
|
||||
workingGraph.reasoningState = nextReasoningState;
|
||||
workingSelection = selectActiveUnknownCandidate(
|
||||
workingGraph,
|
||||
workingGraph.resolvedNodeIds,
|
||||
);
|
||||
|
||||
if (workingSelection?.status !== "selected") {
|
||||
workingGraph = previousGraph;
|
||||
workingProposal = previousProposal;
|
||||
nextReasoningState = previousReasoningState;
|
||||
workingSelection = selectActiveUnknownCandidate(
|
||||
workingGraph,
|
||||
workingGraph.resolvedNodeIds,
|
||||
);
|
||||
decompositionStoppedReason =
|
||||
workingSelection?.status === "ambiguous"
|
||||
? "Decomposition produced multiple equally valid children with no justified distinction."
|
||||
: "No unresolved child remained selectable after decomposition.";
|
||||
break;
|
||||
}
|
||||
|
||||
decompositionAccepted = true;
|
||||
decompositionDepth += 1;
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
updatedSituationGraph: workingGraph,
|
||||
proposalSnapshot: workingProposal,
|
||||
reasoningState: nextReasoningState,
|
||||
deterministicSelection: workingSelection,
|
||||
atomicityAssessment:
|
||||
rootAtomicityAssessment ?? lastAtomicityAssessment ?? null,
|
||||
decompositionDepth,
|
||||
decompositionAttempted,
|
||||
decompositionAccepted,
|
||||
decompositionStoppedReason,
|
||||
proposedChildCount,
|
||||
acceptedChildCount,
|
||||
rejectedChildren,
|
||||
childQualitySummary,
|
||||
selectedChildNodeId,
|
||||
};
|
||||
}
|
||||
|
||||
function isComparabilityQuestion(question) {
|
||||
const text = String(question || "").toLowerCase();
|
||||
return (
|
||||
@@ -1027,71 +1407,44 @@ export function applyValidatedProposal({
|
||||
updatedSituationGraph.resolvedNodeIds,
|
||||
);
|
||||
|
||||
let atomicityAssessment = null;
|
||||
let decompositionPerformed = false;
|
||||
let decompositionChildNodeIds = [];
|
||||
let decompositionReason = null;
|
||||
const decompositionResult = runDeterministicDecomposition({
|
||||
graphSnapshot,
|
||||
proposalSnapshot,
|
||||
updatedSituationGraph,
|
||||
reasoningResolution,
|
||||
deterministicSelection,
|
||||
});
|
||||
|
||||
const initiallySelectedNode =
|
||||
deterministicSelection?.status === "selected" &&
|
||||
deterministicSelection?.nodeId
|
||||
? updatedSituationGraph.nodes.find(
|
||||
(node) => node.id === deterministicSelection.nodeId,
|
||||
)
|
||||
: null;
|
||||
|
||||
if (initiallySelectedNode) {
|
||||
atomicityAssessment = assessUnknownAtomicity({
|
||||
node: initiallySelectedNode,
|
||||
graph: updatedSituationGraph,
|
||||
});
|
||||
|
||||
if (atomicityAssessment.atomicity === "composite") {
|
||||
const decomposition = buildCompositeUnknownChildren(
|
||||
initiallySelectedNode,
|
||||
updatedSituationGraph,
|
||||
);
|
||||
decompositionPerformed = true;
|
||||
decompositionChildNodeIds = decomposition.childNodeIds;
|
||||
decompositionReason =
|
||||
decomposition.reason || atomicityAssessment.reason || null;
|
||||
|
||||
if (
|
||||
decomposition.childNodes.length > 0 ||
|
||||
decomposition.childEdges.length > 0
|
||||
) {
|
||||
proposalSnapshot.addedNodes.push(...decomposition.childNodes);
|
||||
proposalSnapshot.addedEdges.push(...decomposition.childEdges);
|
||||
|
||||
applied = applyGraphUpdate(graphSnapshot, proposalSnapshot);
|
||||
if (!applied.success) {
|
||||
return {
|
||||
success: false,
|
||||
stage: "application",
|
||||
errors: applied.errors,
|
||||
};
|
||||
}
|
||||
|
||||
updatedSituationGraph = {
|
||||
...graphSnapshot,
|
||||
nodes: applied.nodes,
|
||||
edges: applied.edges,
|
||||
resolvedNodeIds: applied.resolvedNodeIds,
|
||||
};
|
||||
nextReasoningState = buildReasoningState(
|
||||
updatedSituationGraph,
|
||||
reasoningResolution.reasoningStateOverride,
|
||||
);
|
||||
updatedSituationGraph.reasoningState = nextReasoningState;
|
||||
}
|
||||
|
||||
deterministicSelection = selectActiveUnknownCandidate(
|
||||
updatedSituationGraph,
|
||||
updatedSituationGraph.resolvedNodeIds,
|
||||
);
|
||||
}
|
||||
if (!decompositionResult.success) {
|
||||
return decompositionResult;
|
||||
}
|
||||
|
||||
updatedSituationGraph = decompositionResult.updatedSituationGraph;
|
||||
nextReasoningState = decompositionResult.reasoningState;
|
||||
deterministicSelection = decompositionResult.deterministicSelection;
|
||||
|
||||
const atomicityAssessment = decompositionResult.atomicityAssessment;
|
||||
const decompositionDepth = decompositionResult.decompositionDepth;
|
||||
const decompositionAttempted = decompositionResult.decompositionAttempted;
|
||||
const decompositionAccepted = decompositionResult.decompositionAccepted;
|
||||
const decompositionStoppedReason =
|
||||
decompositionResult.decompositionStoppedReason;
|
||||
const proposedChildCount = decompositionResult.proposedChildCount;
|
||||
const acceptedChildCount = decompositionResult.acceptedChildCount;
|
||||
const rejectedChildren = decompositionResult.rejectedChildren;
|
||||
const childQualitySummary = decompositionResult.childQualitySummary;
|
||||
const selectedChildNodeId = decompositionResult.selectedChildNodeId;
|
||||
const decompositionPerformed =
|
||||
decompositionAttempted && decompositionAccepted;
|
||||
const decompositionChildNodeIds = [
|
||||
...new Set(
|
||||
decompositionResult.proposalSnapshot.addedNodes
|
||||
.filter((node) => node.kind === "unknown" && node.parentId != null)
|
||||
.map((node) => node.id),
|
||||
),
|
||||
];
|
||||
const decompositionReason = decompositionStoppedReason;
|
||||
|
||||
if (
|
||||
deterministicSelection?.status === "selected" &&
|
||||
deterministicSelection?.nodeId
|
||||
@@ -1197,6 +1550,16 @@ export function applyValidatedProposal({
|
||||
emergentReasoningNodeId: emergentReasoningUnknown?.node?.id ?? null,
|
||||
emergentReasoningNodeReason: emergentReasoningUnknown?.reason ?? null,
|
||||
atomicityAssessment: atomicityAssessment?.atomicity ?? null,
|
||||
atomicityDecisionReason: atomicityAssessment?.reason ?? null,
|
||||
decompositionDepth,
|
||||
decompositionAttempted,
|
||||
decompositionAccepted,
|
||||
decompositionStoppedReason,
|
||||
proposedChildCount,
|
||||
acceptedChildCount,
|
||||
rejectedChildren,
|
||||
selectedChildNodeId,
|
||||
childQualitySummary,
|
||||
decompositionPerformed,
|
||||
childUnknownCount: decompositionChildNodeIds.length,
|
||||
childNodeIds: decompositionChildNodeIds,
|
||||
|
||||
@@ -93,6 +93,16 @@ function buildUpdateDiagnostics({
|
||||
emergentReasoningNodeId,
|
||||
emergentReasoningNodeReason,
|
||||
atomicityAssessment,
|
||||
atomicityDecisionReason,
|
||||
decompositionDepth,
|
||||
decompositionAttempted,
|
||||
decompositionAccepted,
|
||||
decompositionStoppedReason,
|
||||
proposedChildCount,
|
||||
acceptedChildCount,
|
||||
rejectedChildren,
|
||||
selectedChildNodeId,
|
||||
childQualitySummary,
|
||||
decompositionPerformed,
|
||||
childUnknownCount,
|
||||
childNodeIds,
|
||||
@@ -126,6 +136,16 @@ function buildUpdateDiagnostics({
|
||||
emergentReasoningNodeId: emergentReasoningNodeId ?? null,
|
||||
emergentReasoningNodeReason: emergentReasoningNodeReason ?? null,
|
||||
atomicityAssessment: atomicityAssessment ?? null,
|
||||
atomicityDecisionReason: atomicityDecisionReason ?? null,
|
||||
decompositionDepth: decompositionDepth ?? 0,
|
||||
decompositionAttempted: decompositionAttempted ?? false,
|
||||
decompositionAccepted: decompositionAccepted ?? false,
|
||||
decompositionStoppedReason: decompositionStoppedReason ?? null,
|
||||
proposedChildCount: proposedChildCount ?? 0,
|
||||
acceptedChildCount: acceptedChildCount ?? 0,
|
||||
rejectedChildren: rejectedChildren ?? [],
|
||||
selectedChildNodeId: selectedChildNodeId ?? null,
|
||||
childQualitySummary: childQualitySummary ?? [],
|
||||
decompositionPerformed: decompositionPerformed ?? false,
|
||||
childUnknownCount: childUnknownCount ?? 0,
|
||||
childNodeIds: childNodeIds ?? [],
|
||||
@@ -383,6 +403,16 @@ async function updateCaseWithDependencies(body, dependencies = {}) {
|
||||
emergentReasoningNodeId: null,
|
||||
emergentReasoningNodeReason: null,
|
||||
atomicityAssessment: null,
|
||||
atomicityDecisionReason: null,
|
||||
decompositionDepth: 0,
|
||||
decompositionAttempted: false,
|
||||
decompositionAccepted: false,
|
||||
decompositionStoppedReason: null,
|
||||
proposedChildCount: 0,
|
||||
acceptedChildCount: 0,
|
||||
rejectedChildren: [],
|
||||
selectedChildNodeId: null,
|
||||
childQualitySummary: [],
|
||||
decompositionPerformed: false,
|
||||
childUnknownCount: 0,
|
||||
childNodeIds: [],
|
||||
@@ -430,6 +460,17 @@ async function updateCaseWithDependencies(body, dependencies = {}) {
|
||||
emergentReasoningNodeReason:
|
||||
applicationResult.emergentReasoningNodeReason,
|
||||
atomicityAssessment: applicationResult.atomicityAssessment,
|
||||
atomicityDecisionReason: applicationResult.atomicityDecisionReason,
|
||||
decompositionDepth: applicationResult.decompositionDepth,
|
||||
decompositionAttempted: applicationResult.decompositionAttempted,
|
||||
decompositionAccepted: applicationResult.decompositionAccepted,
|
||||
decompositionStoppedReason:
|
||||
applicationResult.decompositionStoppedReason,
|
||||
proposedChildCount: applicationResult.proposedChildCount,
|
||||
acceptedChildCount: applicationResult.acceptedChildCount,
|
||||
rejectedChildren: applicationResult.rejectedChildren,
|
||||
selectedChildNodeId: applicationResult.selectedChildNodeId,
|
||||
childQualitySummary: applicationResult.childQualitySummary,
|
||||
decompositionPerformed: applicationResult.decompositionPerformed,
|
||||
childUnknownCount: applicationResult.childUnknownCount,
|
||||
childNodeIds: applicationResult.childNodeIds,
|
||||
@@ -462,6 +503,16 @@ async function updateCaseWithDependencies(body, dependencies = {}) {
|
||||
emergentReasoningNodeId: null,
|
||||
emergentReasoningNodeReason: null,
|
||||
atomicityAssessment: null,
|
||||
atomicityDecisionReason: null,
|
||||
decompositionDepth: 0,
|
||||
decompositionAttempted: false,
|
||||
decompositionAccepted: false,
|
||||
decompositionStoppedReason: null,
|
||||
proposedChildCount: 0,
|
||||
acceptedChildCount: 0,
|
||||
rejectedChildren: [],
|
||||
selectedChildNodeId: null,
|
||||
childQualitySummary: [],
|
||||
decompositionPerformed: false,
|
||||
childUnknownCount: 0,
|
||||
childNodeIds: [],
|
||||
|
||||
@@ -514,17 +514,45 @@ function isBroadCompositeUnknownText(text) {
|
||||
);
|
||||
}
|
||||
|
||||
function hasCompoundAbstractSignals(text) {
|
||||
return (
|
||||
/\b(timing|measurement|basis|cost|costs|debt|stock|tax|capital spending|mix|segment)\s+(and|or)\s+\b/.test(
|
||||
text,
|
||||
) ||
|
||||
/\b[a-z]+\/[a-z]+\b/.test(text) ||
|
||||
/,\s*[a-z]+,\s*[a-z]+/.test(text)
|
||||
);
|
||||
}
|
||||
|
||||
function isFocusedAtomicUnknownText(text) {
|
||||
return /\b(define|definition|meaning|term|threshold|criterion|criteria|baseline|evidence|measure|metric|denominator|rate|date|period|budget|constraint|customer|actor|owner)\b/.test(
|
||||
text,
|
||||
);
|
||||
}
|
||||
|
||||
function isDirectlyAnswerableObservationChildText(text) {
|
||||
return /\b(whether the two observations reflect different timing|how the two observations were measured|change mainly affecting|one off event during the period|mix shift during the period)\b/.test(
|
||||
text,
|
||||
);
|
||||
}
|
||||
|
||||
export function assessUnknownAtomicity({ node, graph }) {
|
||||
const nodeText = normaliseText(
|
||||
`${node?.label || ""} ${node?.description || ""}`,
|
||||
);
|
||||
|
||||
if (
|
||||
isDirectlyAnswerableObservationChildText(nodeText) &&
|
||||
!hasCompoundAbstractSignals(nodeText)
|
||||
) {
|
||||
return {
|
||||
atomicity: "atomic",
|
||||
reason:
|
||||
"This unknown isolates one specific line of enquiry and can be investigated directly.",
|
||||
decompositionKind: null,
|
||||
};
|
||||
}
|
||||
|
||||
if (isRelationshipExplanationUnknown(node, graph)) {
|
||||
return {
|
||||
atomicity: "composite",
|
||||
@@ -534,6 +562,15 @@ export function assessUnknownAtomicity({ node, graph }) {
|
||||
};
|
||||
}
|
||||
|
||||
if (hasCompoundAbstractSignals(nodeText)) {
|
||||
return {
|
||||
atomicity: "composite",
|
||||
reason:
|
||||
"This unknown still bundles multiple abstract uncertainties together, so it should be decomposed before asking it directly.",
|
||||
decompositionKind: "compound_child",
|
||||
};
|
||||
}
|
||||
|
||||
if (
|
||||
isFocusedAtomicUnknownText(nodeText) &&
|
||||
!isBroadCompositeUnknownText(nodeText)
|
||||
|
||||
@@ -1148,7 +1148,8 @@ describe("applyValidatedProposal", () => {
|
||||
expect(result.selectedQuestion?.nodeId).toBe(result.newActiveUnknownNodeId);
|
||||
expect(result.selectedQuestion).toMatchObject({
|
||||
nodeId: result.newActiveUnknownNodeId,
|
||||
question: "What evidence would clarify timing or measurement basis?",
|
||||
question:
|
||||
"What evidence would clarify how the two observations were measured?",
|
||||
});
|
||||
expect(result.selectedQuestion?.question.toLowerCase()).not.toMatch(
|
||||
/dso|debtor days|receivables turnover|working capital|receivables/,
|
||||
@@ -1227,7 +1228,7 @@ describe("applyValidatedProposal", () => {
|
||||
expect(result.newActiveUnknownNodeId).not.toBe("n-existing-explanation");
|
||||
expect(result.selectedQuestion?.nodeId).not.toBe("n-existing-explanation");
|
||||
expect(result.selectedQuestion?.question).toBe(
|
||||
"What evidence would clarify timing or measurement basis?",
|
||||
"What evidence would clarify how the two observations were measured?",
|
||||
);
|
||||
expect(
|
||||
result.updatedSituationGraph.nodes.filter(
|
||||
@@ -1253,7 +1254,7 @@ describe("applyValidatedProposal", () => {
|
||||
expect(result.decompositionPerformed).toBe(true);
|
||||
expect(result.childUnknownCount).toBe(5);
|
||||
expect(result.childNodeIds).toHaveLength(5);
|
||||
expect(result.atomicityReason).toContain("Decomposed");
|
||||
expect(result.atomicityReason).toBeTruthy();
|
||||
expect(result.selectedQuestion?.nodeId).toBe(result.newActiveUnknownNodeId);
|
||||
expect(result.selectedQuestion?.nodeId).not.toBe(
|
||||
result.emergentReasoningNodeId,
|
||||
|
||||
@@ -0,0 +1,278 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
assessChildUnknownQuality,
|
||||
applyValidatedProposal,
|
||||
MAX_DECOMPOSITION_DEPTH,
|
||||
} from "@/lib/graph/apply-proposal.js";
|
||||
import { makeGraph, makeNode } from "@/lib/graph/schema.js";
|
||||
|
||||
function makeParentGraph({
|
||||
centralStatement,
|
||||
parentLabel,
|
||||
parentDescription,
|
||||
observations = [],
|
||||
}) {
|
||||
const parent = makeNode({
|
||||
id: "n-parent",
|
||||
label: parentLabel,
|
||||
description: parentDescription,
|
||||
kind: "unknown",
|
||||
status: "unknown",
|
||||
confidence: "medium",
|
||||
});
|
||||
|
||||
return {
|
||||
parent,
|
||||
graph: makeGraph({
|
||||
centralStatement,
|
||||
nodes: [parent, ...observations],
|
||||
edges: [],
|
||||
activeUnknownNodeId: parent.id,
|
||||
resolvedNodeIds: [],
|
||||
currentSummary: "Decomposition quality graph",
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
describe("assessChildUnknownQuality", () => {
|
||||
it("rejects 'Timing or measurement basis' as compound", () => {
|
||||
const { parent, graph } = makeParentGraph({
|
||||
centralStatement:
|
||||
"Revenue increased by 18%, but cash in the bank fell over the same period.",
|
||||
parentLabel:
|
||||
"Explanation for why revenue increased by 18%, but cash in the bank fell over the same period",
|
||||
parentDescription:
|
||||
"Need to understand what change or event could explain why these observations differ, because that is needed to investigate their relationship.",
|
||||
});
|
||||
const child = makeNode({
|
||||
id: "n-child",
|
||||
label: "Timing or measurement basis",
|
||||
description:
|
||||
"Need evidence about whether a timing or measurement-basis difference could explain the observations, because that would change how they should be interpreted.",
|
||||
kind: "unknown",
|
||||
status: "unknown",
|
||||
confidence: "medium",
|
||||
parentId: parent.id,
|
||||
});
|
||||
|
||||
const result = assessChildUnknownQuality({
|
||||
parentNode: parent,
|
||||
childNode: child,
|
||||
siblingNodes: [child],
|
||||
graph,
|
||||
});
|
||||
|
||||
expect(result.valid).toBe(false);
|
||||
expect(result.compoundSignals).toContain("timing_or_measurement_basis");
|
||||
expect(result.reasons).toContain("compound_child");
|
||||
});
|
||||
|
||||
it("accepts a child with one directly answerable uncertainty", () => {
|
||||
const { parent, graph } = makeParentGraph({
|
||||
centralStatement: "Traffic increased, but sales stayed flat.",
|
||||
parentLabel:
|
||||
"What explains why more website traffic did not produce more sales?",
|
||||
parentDescription:
|
||||
"Need an explanation because the observations moved differently.",
|
||||
});
|
||||
const child = makeNode({
|
||||
id: "n-child",
|
||||
label: "Different measurement basis between the two observations",
|
||||
description:
|
||||
"Need evidence about whether the two observations use different measurement bases, because that could help explain the difference.",
|
||||
kind: "unknown",
|
||||
status: "unknown",
|
||||
confidence: "medium",
|
||||
parentId: parent.id,
|
||||
});
|
||||
|
||||
const result = assessChildUnknownQuality({
|
||||
parentNode: parent,
|
||||
childNode: child,
|
||||
siblingNodes: [child],
|
||||
graph,
|
||||
});
|
||||
|
||||
expect(result.valid).toBe(true);
|
||||
expect(result.atomic).toBe(true);
|
||||
expect(result.directlyAnswerable).toBe(true);
|
||||
expect(result.narrowerThanParent).toBe(true);
|
||||
});
|
||||
|
||||
it("rejects sibling duplicates", () => {
|
||||
const { parent, graph } = makeParentGraph({
|
||||
centralStatement: "Production increased, but defects also increased.",
|
||||
parentLabel: "What explains why output and defects both increased?",
|
||||
parentDescription:
|
||||
"Need an explanation because both observations increased.",
|
||||
});
|
||||
const childA = makeNode({
|
||||
id: "n-child-a",
|
||||
label: "Different timing between the two observations",
|
||||
description:
|
||||
"Need evidence about whether the two observations reflect different timing, because that could help explain the difference.",
|
||||
kind: "unknown",
|
||||
status: "unknown",
|
||||
confidence: "medium",
|
||||
parentId: parent.id,
|
||||
});
|
||||
const childB = makeNode({
|
||||
id: "n-child-b",
|
||||
label: "Different timing between the two observations",
|
||||
description:
|
||||
"Need evidence about whether the two observations reflect different timing, because that could help explain the difference.",
|
||||
kind: "unknown",
|
||||
status: "unknown",
|
||||
confidence: "medium",
|
||||
parentId: parent.id,
|
||||
});
|
||||
|
||||
const result = assessChildUnknownQuality({
|
||||
parentNode: parent,
|
||||
childNode: childA,
|
||||
siblingNodes: [childA, childB],
|
||||
graph,
|
||||
});
|
||||
|
||||
expect(result.valid).toBe(false);
|
||||
expect(result.duplicateSiblingIds).toContain("n-child-b");
|
||||
});
|
||||
|
||||
it("rejects parent paraphrases", () => {
|
||||
const { parent, graph } = makeParentGraph({
|
||||
centralStatement:
|
||||
"Customer satisfaction scores increased, but complaints also increased.",
|
||||
parentLabel:
|
||||
"What explains why satisfaction and complaints both increased?",
|
||||
parentDescription:
|
||||
"Need a broad explanation because the observations moved differently.",
|
||||
});
|
||||
const child = makeNode({
|
||||
id: "n-child",
|
||||
label: "What explains why satisfaction and complaints both increased?",
|
||||
description:
|
||||
"Need a broad explanation because the observations moved differently.",
|
||||
kind: "unknown",
|
||||
status: "unknown",
|
||||
confidence: "medium",
|
||||
parentId: parent.id,
|
||||
});
|
||||
|
||||
const result = assessChildUnknownQuality({
|
||||
parentNode: parent,
|
||||
childNode: child,
|
||||
siblingNodes: [child],
|
||||
graph,
|
||||
});
|
||||
|
||||
expect(result.valid).toBe(false);
|
||||
expect(result.reasons).toContain("not_narrower_than_parent");
|
||||
});
|
||||
});
|
||||
|
||||
describe("decomposition stopping conditions", () => {
|
||||
function makeMeaningfulNoOpProposal() {
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
||||
it("does not decompose an atomic selected unknown", () => {
|
||||
const atomic = makeNode({
|
||||
id: "n-atomic",
|
||||
label: "Were both figures measured over the same accounting period?",
|
||||
description:
|
||||
"Need to know whether both figures cover the same accounting period because that determines whether they are directly comparable.",
|
||||
kind: "unknown",
|
||||
status: "unknown",
|
||||
confidence: "high",
|
||||
});
|
||||
const graph = makeGraph({
|
||||
centralStatement:
|
||||
"Revenue increased by 18%, but cash in the bank fell over the same period.",
|
||||
nodes: [atomic],
|
||||
edges: [],
|
||||
activeUnknownNodeId: atomic.id,
|
||||
resolvedNodeIds: [],
|
||||
currentSummary: "Atomic selected node graph",
|
||||
});
|
||||
|
||||
const result = applyValidatedProposal({
|
||||
situationGraph: graph,
|
||||
proposal: makeMeaningfulNoOpProposal(),
|
||||
});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.decompositionAttempted).toBe(false);
|
||||
expect(result.decompositionStoppedReason).toBe(
|
||||
"Selected unknown is already atomic.",
|
||||
);
|
||||
});
|
||||
|
||||
it("stops once a directly answerable child is selected", () => {
|
||||
const { parent, graph } = makeParentGraph({
|
||||
centralStatement: "Traffic increased, but sales stayed flat.",
|
||||
parentLabel:
|
||||
"What explains why more website traffic did not produce more sales?",
|
||||
parentDescription:
|
||||
"Need an explanation because the observations moved differently.",
|
||||
observations: [
|
||||
makeNode({
|
||||
id: "n-traffic",
|
||||
label: "Website traffic increased.",
|
||||
description: "Website traffic increased.",
|
||||
kind: "observation",
|
||||
status: "supported",
|
||||
confidence: "high",
|
||||
}),
|
||||
makeNode({
|
||||
id: "n-sales",
|
||||
label: "Sales stayed flat.",
|
||||
description: "Sales stayed flat.",
|
||||
kind: "observation",
|
||||
status: "supported",
|
||||
confidence: "high",
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
const result = applyValidatedProposal({
|
||||
situationGraph: graph,
|
||||
proposal: makeMeaningfulNoOpProposal(),
|
||||
});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.decompositionAttempted).toBe(true);
|
||||
expect(result.decompositionAccepted).toBe(true);
|
||||
expect(result.selectedQuestion).toMatchObject({
|
||||
nodeId: expect.any(String),
|
||||
question:
|
||||
"What evidence would clarify how the two observations were measured?",
|
||||
});
|
||||
expect(result.selectedChildNodeId).toBe(result.selectedQuestion?.nodeId);
|
||||
expect(result.decompositionStoppedReason).toBe(
|
||||
"Selected child is atomic and directly answerable.",
|
||||
);
|
||||
});
|
||||
|
||||
it("exposes the configured maximum decomposition depth", () => {
|
||||
expect(MAX_DECOMPOSITION_DEPTH).toBeGreaterThanOrEqual(2);
|
||||
expect(MAX_DECOMPOSITION_DEPTH).toBeLessThanOrEqual(3);
|
||||
});
|
||||
});
|
||||
@@ -1053,7 +1053,7 @@ describe("lib/graph/orchestrator startCase", () => {
|
||||
});
|
||||
expect(result.diagnostics.emergentReasoningNodeId).toBeTruthy();
|
||||
expect(result.diagnostics.childNodeIds).toHaveLength(5);
|
||||
expect(result.diagnostics.atomicityReason).toContain("Decomposed");
|
||||
expect(result.diagnostics.atomicityReason).toBeTruthy();
|
||||
expect(result.diagnostics.emergentReasoningNodeReason).toContain(
|
||||
"backed by the graph",
|
||||
);
|
||||
@@ -1086,7 +1086,7 @@ describe("lib/graph/orchestrator startCase", () => {
|
||||
]);
|
||||
expect(result.selectedQuestion?.nodeId).toBe(result.newActiveUnknownNodeId);
|
||||
expect(result.selectedQuestion?.question).toBe(
|
||||
"What evidence would clarify timing or measurement basis?",
|
||||
"What evidence would clarify how the two observations were measured?",
|
||||
);
|
||||
expect(result.selectedQuestion?.question.toLowerCase()).not.toMatch(
|
||||
/same basis|dso|receivables|debtor days|working capital/,
|
||||
|
||||
@@ -127,9 +127,9 @@ function makeUpdateSuccess(overrides = {}) {
|
||||
},
|
||||
{
|
||||
id: "n-child-1",
|
||||
label: "Timing or measurement basis",
|
||||
label: "How the two observations were measured",
|
||||
description:
|
||||
"Need evidence about whether a timing or measurement-basis difference could explain revenue increased by 18%, but cash in the bank fell over the same period, because that would change how the observations should be interpreted.",
|
||||
"Need evidence about the measure used for each observation, because that could help explain revenue increased by 18%, but cash in the bank fell over the same period.",
|
||||
kind: "unknown",
|
||||
status: "unknown",
|
||||
confidence: "medium",
|
||||
@@ -180,9 +180,9 @@ function makeUpdateSuccess(overrides = {}) {
|
||||
},
|
||||
{
|
||||
id: "n-child-1",
|
||||
label: "Timing or measurement basis",
|
||||
label: "How the two observations were measured",
|
||||
description:
|
||||
"Need evidence about whether a timing or measurement-basis difference could explain revenue increased by 18%, but cash in the bank fell over the same period, because that would change how the observations should be interpreted.",
|
||||
"Need evidence about the measure used for each observation, because that could help explain revenue increased by 18%, but cash in the bank fell over the same period.",
|
||||
kind: "unknown",
|
||||
status: "unknown",
|
||||
confidence: "medium",
|
||||
@@ -205,7 +205,7 @@ function makeUpdateSuccess(overrides = {}) {
|
||||
selectedQuestion: {
|
||||
nodeId: "n-child-1",
|
||||
question:
|
||||
"What evidence would clarify timing or measurement basis?",
|
||||
"What evidence would clarify how the two observations were measured?",
|
||||
reason:
|
||||
"Formulated from graph context using the evidence_gathering investigation strategy.",
|
||||
},
|
||||
@@ -213,7 +213,7 @@ function makeUpdateSuccess(overrides = {}) {
|
||||
selectedQuestion: {
|
||||
nodeId: "n-child-1",
|
||||
question:
|
||||
"What evidence would clarify timing or measurement basis?",
|
||||
"What evidence would clarify how the two observations were measured?",
|
||||
reason:
|
||||
"Formulated from graph context using the evidence_gathering investigation strategy.",
|
||||
},
|
||||
@@ -499,7 +499,7 @@ describe("graph-backed UI rendering", () => {
|
||||
);
|
||||
|
||||
expect(html).toContain(
|
||||
"What evidence would clarify timing or measurement basis?",
|
||||
"What evidence would clarify how the two observations were measured?",
|
||||
);
|
||||
});
|
||||
|
||||
@@ -557,7 +557,7 @@ describe("graph-backed UI rendering", () => {
|
||||
expect(html).toContain("New active unknown");
|
||||
expect(html).toContain("Next question");
|
||||
expect(html).toContain(
|
||||
"What evidence would clarify timing or measurement basis?",
|
||||
"What evidence would clarify how the two observations were measured?",
|
||||
);
|
||||
});
|
||||
|
||||
@@ -586,7 +586,7 @@ describe("graph-backed UI rendering", () => {
|
||||
expect(html).toContain("comparability: confirmed");
|
||||
expect(html).toContain("relationship: insufficient_information");
|
||||
expect(html).toContain(
|
||||
"What evidence would clarify timing or measurement basis?",
|
||||
"What evidence would clarify how the two observations were measured?",
|
||||
);
|
||||
expect(html).not.toContain("reasoning:comparability");
|
||||
});
|
||||
@@ -613,7 +613,7 @@ describe("graph-backed UI rendering", () => {
|
||||
);
|
||||
|
||||
expect(html).toContain(
|
||||
"What evidence would clarify timing or measurement basis?",
|
||||
"What evidence would clarify how the two observations were measured?",
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user