feat: decompose composite unknowns before questioning
This commit is contained in:
+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)
|
||||
|
||||
Reference in New Issue
Block a user