feat: decompose non-answerable unknowns
This commit is contained in:
+188
-12
@@ -1,5 +1,6 @@
|
||||
import { describeGraph } from "./builder.js";
|
||||
import {
|
||||
assessUnknownAnswerability,
|
||||
assessUnknownAtomicity,
|
||||
buildReasoningState,
|
||||
classifyObservationRelationship,
|
||||
@@ -1624,6 +1625,63 @@ function findNodeById(graph, nodeId) {
|
||||
return (graph.nodes || []).find((node) => node.id === nodeId) || null;
|
||||
}
|
||||
|
||||
function isSelectableUnresolvedUnknown(graph, nodeId) {
|
||||
const node = findNodeById(graph, nodeId);
|
||||
return Boolean(
|
||||
node &&
|
||||
node.kind === "unknown" &&
|
||||
!["resolved", "contradicted"].includes(node.status) &&
|
||||
!(graph.resolvedNodeIds || []).includes(node.id),
|
||||
);
|
||||
}
|
||||
|
||||
function selectedQuestionBelongsToChild(graph, selectedQuestion) {
|
||||
if (!selectedQuestion?.nodeId) return false;
|
||||
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),
|
||||
);
|
||||
|
||||
if (childCandidates.length === 0) {
|
||||
return { status: "none", nodeId: null, tiedCandidateIds: [] };
|
||||
}
|
||||
|
||||
const scored = childCandidates.map((node) => ({
|
||||
node,
|
||||
score:
|
||||
scoreUnknownCandidate(graph, node, graph.resolvedNodeIds || []).score ??
|
||||
Number.NEGATIVE_INFINITY,
|
||||
}));
|
||||
const topScore = Math.max(...scored.map((item) => item.score));
|
||||
const top = scored.filter((item) => item.score === topScore);
|
||||
|
||||
if (top.length === 0) {
|
||||
return { status: "none", nodeId: null, tiedCandidateIds: [] };
|
||||
}
|
||||
|
||||
if (top.length > 1) {
|
||||
return {
|
||||
status: "ambiguous",
|
||||
nodeId: null,
|
||||
tiedCandidateIds: top.map((item) => item.node.id),
|
||||
reason:
|
||||
"Multiple decomposition children remain equally good next investigations.",
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
status: "selected",
|
||||
nodeId: top[0].node.id,
|
||||
reason:
|
||||
"Selected the strongest direct child investigation for a non-answerable parent unknown.",
|
||||
};
|
||||
}
|
||||
|
||||
export function determineGraphBackedQuestion({ situationGraph }) {
|
||||
const graphSnapshot = cloneJsonSafe(situationGraph);
|
||||
let updatedSituationGraph = cloneJsonSafe(situationGraph);
|
||||
@@ -1656,10 +1714,20 @@ export function determineGraphBackedQuestion({ situationGraph }) {
|
||||
updatedSituationGraph.reasoningState = buildReasoningState(
|
||||
updatedSituationGraph,
|
||||
);
|
||||
deterministicSelection = selectActiveUnknownCandidate(
|
||||
deterministicSelection = isSelectableUnresolvedUnknown(
|
||||
updatedSituationGraph,
|
||||
updatedSituationGraph.resolvedNodeIds || [],
|
||||
);
|
||||
decompositionResult.selectedChildNodeId,
|
||||
)
|
||||
? {
|
||||
status: "selected",
|
||||
nodeId: decompositionResult.selectedChildNodeId,
|
||||
reason:
|
||||
"Selected the preserved decomposition child because it remains the strongest independently answerable investigation.",
|
||||
}
|
||||
: selectActiveUnknownCandidate(
|
||||
updatedSituationGraph,
|
||||
updatedSituationGraph.resolvedNodeIds || [],
|
||||
);
|
||||
updatedSituationGraph.activeUnknownNodeId =
|
||||
deterministicSelection?.status === "selected"
|
||||
? deterministicSelection.nodeId
|
||||
@@ -1704,10 +1772,26 @@ export function determineGraphBackedQuestion({ situationGraph }) {
|
||||
}
|
||||
: null,
|
||||
atomicityAssessment: decompositionResult.atomicityAssessment,
|
||||
answerabilityAssessment: decompositionResult.answerabilityAssessment,
|
||||
independentlyAnswerable:
|
||||
decompositionResult.answerabilityAssessment?.independentlyAnswerable ??
|
||||
null,
|
||||
prerequisiteConceptCount:
|
||||
decompositionResult.answerabilityAssessment?.prerequisiteConceptCount ??
|
||||
null,
|
||||
decompositionPerformed:
|
||||
decompositionResult.decompositionAttempted &&
|
||||
decompositionResult.decompositionAccepted,
|
||||
decompositionAttempted: decompositionResult.decompositionAttempted,
|
||||
decompositionTriggeredByAnswerability:
|
||||
decompositionResult.decompositionTriggeredByAnswerability ?? false,
|
||||
selectedContainerUnknown:
|
||||
decompositionResult.selectedContainerUnknown ?? null,
|
||||
selectedChildUnknown:
|
||||
decompositionResult.selectedChildNodeId ??
|
||||
(deterministicSelection?.status === "selected"
|
||||
? deterministicSelection.nodeId
|
||||
: null),
|
||||
selectedUnknownBefore: decompositionResult.selectedUnknownBefore,
|
||||
selectedUnknownAfter: deterministicSelection?.nodeId ?? null,
|
||||
questionComplexityAssessment:
|
||||
@@ -1727,7 +1811,9 @@ function runDeterministicDecomposition({
|
||||
let workingProposal = proposalSnapshot;
|
||||
let nextReasoningState = workingGraph.reasoningState;
|
||||
let lastAtomicityAssessment = null;
|
||||
let lastAnswerabilityAssessment = null;
|
||||
let rootAtomicityAssessment = null;
|
||||
let rootAnswerabilityAssessment = null;
|
||||
let decompositionDepth = 0;
|
||||
let decompositionAttempted = false;
|
||||
let decompositionAccepted = false;
|
||||
@@ -1742,6 +1828,8 @@ function runDeterministicDecomposition({
|
||||
let rejectedChildren = [];
|
||||
let childQualitySummary = [];
|
||||
let decompositionTriggeredByQuestionComplexity = false;
|
||||
let decompositionTriggeredByAnswerability = false;
|
||||
let selectedContainerUnknown = null;
|
||||
|
||||
while (workingSelection?.status === "selected" && workingSelection?.nodeId) {
|
||||
const selectedNode = findNodeById(workingGraph, workingSelection.nodeId);
|
||||
@@ -1755,13 +1843,28 @@ function runDeterministicDecomposition({
|
||||
node: selectedNode,
|
||||
graph: workingGraph,
|
||||
});
|
||||
const answerabilityAssessment = assessUnknownAnswerability({
|
||||
node: selectedNode,
|
||||
graph: workingGraph,
|
||||
});
|
||||
lastAtomicityAssessment = atomicityAssessment;
|
||||
lastAnswerabilityAssessment = answerabilityAssessment;
|
||||
if (!rootAtomicityAssessment) {
|
||||
rootAtomicityAssessment = atomicityAssessment;
|
||||
}
|
||||
if (!rootAnswerabilityAssessment) {
|
||||
rootAnswerabilityAssessment = answerabilityAssessment;
|
||||
}
|
||||
|
||||
if (atomicityAssessment.atomicity === "atomic") {
|
||||
selectedChildNodeId = decompositionDepth > 0 ? selectedNode.id : null;
|
||||
const decompositionRequired =
|
||||
atomicityAssessment.atomicity !== "atomic" ||
|
||||
!answerabilityAssessment.independentlyAnswerable;
|
||||
|
||||
if (!decompositionRequired) {
|
||||
selectedChildNodeId =
|
||||
decompositionDepth > 0 || selectedNode.parentId
|
||||
? selectedNode.id
|
||||
: null;
|
||||
decompositionStoppedReason =
|
||||
decompositionDepth > 0
|
||||
? "Selected child is atomic and directly answerable."
|
||||
@@ -1769,9 +1872,37 @@ function runDeterministicDecomposition({
|
||||
break;
|
||||
}
|
||||
|
||||
if (!selectedContainerUnknown) {
|
||||
selectedContainerUnknown = selectedNode.id;
|
||||
}
|
||||
if (!answerabilityAssessment.independentlyAnswerable) {
|
||||
decompositionTriggeredByAnswerability = true;
|
||||
}
|
||||
|
||||
if (hasExistingDecompositionChildren(workingGraph, selectedNode.id)) {
|
||||
const childSelection = selectDecompositionChildCandidate(
|
||||
workingGraph,
|
||||
selectedNode.id,
|
||||
);
|
||||
if (childSelection.status === "selected") {
|
||||
workingSelection = childSelection;
|
||||
decompositionStoppedReason =
|
||||
"Selected child is atomic and directly answerable.";
|
||||
selectedChildNodeId =
|
||||
findNodeById(workingGraph, childSelection.nodeId)?.parentId ===
|
||||
selectedNode.id
|
||||
? childSelection.nodeId
|
||||
: null;
|
||||
break;
|
||||
}
|
||||
if (childSelection.status === "ambiguous") {
|
||||
workingSelection = childSelection;
|
||||
decompositionStoppedReason =
|
||||
"Selected parent is not independently answerable and its existing child investigations are tied.";
|
||||
break;
|
||||
}
|
||||
decompositionStoppedReason =
|
||||
"Selected composite parent already has decomposition children, so they should be reused instead of regenerated.";
|
||||
"Selected parent is not independently answerable, but no unresolved child investigation remained available.";
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -1831,9 +1962,9 @@ function runDeterministicDecomposition({
|
||||
reasoningResolution.reasoningStateOverride,
|
||||
);
|
||||
workingGraph.reasoningState = nextReasoningState;
|
||||
workingSelection = selectActiveUnknownCandidate(
|
||||
workingSelection = selectDecompositionChildCandidate(
|
||||
workingGraph,
|
||||
workingGraph.resolvedNodeIds,
|
||||
selectedNode.id,
|
||||
);
|
||||
|
||||
if (workingSelection?.status !== "selected") {
|
||||
@@ -1851,6 +1982,13 @@ function runDeterministicDecomposition({
|
||||
break;
|
||||
}
|
||||
|
||||
if (
|
||||
findNodeById(workingGraph, workingSelection.nodeId)?.parentId ===
|
||||
selectedNode.id
|
||||
) {
|
||||
selectedChildNodeId = workingSelection.nodeId;
|
||||
}
|
||||
|
||||
decompositionAccepted = true;
|
||||
decompositionDepth += 1;
|
||||
}
|
||||
@@ -1863,6 +2001,8 @@ function runDeterministicDecomposition({
|
||||
deterministicSelection: workingSelection,
|
||||
atomicityAssessment:
|
||||
rootAtomicityAssessment ?? lastAtomicityAssessment ?? null,
|
||||
answerabilityAssessment:
|
||||
rootAnswerabilityAssessment ?? lastAnswerabilityAssessment ?? null,
|
||||
decompositionDepth,
|
||||
decompositionAttempted,
|
||||
decompositionAccepted,
|
||||
@@ -1874,6 +2014,8 @@ function runDeterministicDecomposition({
|
||||
selectedChildNodeId,
|
||||
selectedUnknownBefore,
|
||||
decompositionTriggeredByQuestionComplexity,
|
||||
decompositionTriggeredByAnswerability,
|
||||
selectedContainerUnknown,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -2205,12 +2347,23 @@ export function applyValidatedProposal({
|
||||
reasoningResolution.reasoningStateOverride,
|
||||
);
|
||||
updatedSituationGraph.reasoningState = nextReasoningState;
|
||||
deterministicSelection = selectActiveUnknownCandidate(
|
||||
deterministicSelection = isSelectableUnresolvedUnknown(
|
||||
updatedSituationGraph,
|
||||
updatedSituationGraph.resolvedNodeIds,
|
||||
);
|
||||
decompositionResult.selectedChildNodeId,
|
||||
)
|
||||
? {
|
||||
status: "selected",
|
||||
nodeId: decompositionResult.selectedChildNodeId,
|
||||
reason:
|
||||
"Preserved the selected decomposition child because it remains unresolved after propagation.",
|
||||
}
|
||||
: selectActiveUnknownCandidate(
|
||||
updatedSituationGraph,
|
||||
updatedSituationGraph.resolvedNodeIds,
|
||||
);
|
||||
|
||||
const atomicityAssessment = decompositionResult.atomicityAssessment;
|
||||
const answerabilityAssessment = decompositionResult.answerabilityAssessment;
|
||||
const decompositionDepth = decompositionResult.decompositionDepth;
|
||||
const decompositionAttempted = decompositionResult.decompositionAttempted;
|
||||
const decompositionAccepted = decompositionResult.decompositionAccepted;
|
||||
@@ -2321,6 +2474,15 @@ export function applyValidatedProposal({
|
||||
}
|
||||
: null;
|
||||
|
||||
const finalSelectedChildNodeId =
|
||||
selectedChildNodeId ??
|
||||
(selectedQuestionBelongsToChild(
|
||||
updatedSituationGraph,
|
||||
finalSelectedQuestion,
|
||||
)
|
||||
? finalSelectedQuestion?.nodeId
|
||||
: null);
|
||||
|
||||
const resultGraphValidation = situationGraphSchema.safeParse(
|
||||
updatedSituationGraph,
|
||||
);
|
||||
@@ -2374,6 +2536,11 @@ export function applyValidatedProposal({
|
||||
emergentReasoningNodeReason: emergentReasoningUnknown?.reason ?? null,
|
||||
atomicityAssessment: atomicityAssessment?.atomicity ?? null,
|
||||
atomicityDecisionReason: atomicityAssessment?.reason ?? null,
|
||||
answerabilityAssessment,
|
||||
independentlyAnswerable:
|
||||
answerabilityAssessment?.independentlyAnswerable ?? null,
|
||||
prerequisiteConceptCount:
|
||||
answerabilityAssessment?.prerequisiteConceptCount ?? null,
|
||||
decompositionDepth,
|
||||
decompositionAttempted,
|
||||
decompositionAccepted,
|
||||
@@ -2381,7 +2548,7 @@ export function applyValidatedProposal({
|
||||
proposedChildCount,
|
||||
acceptedChildCount,
|
||||
rejectedChildren,
|
||||
selectedChildNodeId,
|
||||
selectedChildNodeId: finalSelectedChildNodeId,
|
||||
childQualitySummary,
|
||||
selectedUnknownBefore: decompositionResult.selectedUnknownBefore,
|
||||
selectedUnknownAfter: deterministicSelection?.nodeId ?? null,
|
||||
@@ -2391,6 +2558,8 @@ export function applyValidatedProposal({
|
||||
complexityReasons: questionComplexity?.reasons ?? [],
|
||||
decompositionTriggeredByQuestionComplexity:
|
||||
decompositionResult.decompositionTriggeredByQuestionComplexity ?? false,
|
||||
decompositionTriggeredByAnswerability:
|
||||
decompositionResult.decompositionTriggeredByAnswerability ?? false,
|
||||
previousQuestion,
|
||||
finalQuestion: finalSelectedQuestion?.question ?? null,
|
||||
plainLanguageNormalisations,
|
||||
@@ -2423,6 +2592,13 @@ export function applyValidatedProposal({
|
||||
decompositionPerformed,
|
||||
childUnknownCount: decompositionChildNodeIds.length,
|
||||
childNodeIds: decompositionChildNodeIds,
|
||||
selectedContainerUnknown:
|
||||
decompositionResult.selectedContainerUnknown ?? null,
|
||||
selectedChildUnknown:
|
||||
finalSelectedChildNodeId ??
|
||||
(deterministicSelection?.status === "selected"
|
||||
? deterministicSelection.nodeId
|
||||
: null),
|
||||
atomicityReason:
|
||||
propagationReason ||
|
||||
decompositionReason ||
|
||||
|
||||
Reference in New Issue
Block a user