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 ||
|
||||
|
||||
@@ -52,6 +52,13 @@ function buildDiagnostics({
|
||||
selectedUnknownNodeId,
|
||||
decompositionApplied,
|
||||
questionComplexityAssessment,
|
||||
answerabilityAssessment,
|
||||
independentlyAnswerable,
|
||||
prerequisiteConceptCount,
|
||||
decompositionTriggeredByAnswerability,
|
||||
decompositionReason,
|
||||
selectedContainerUnknown,
|
||||
selectedChildUnknown,
|
||||
}) {
|
||||
return {
|
||||
promptVersion: analysis?.promptVersion ?? null,
|
||||
@@ -73,6 +80,14 @@ function buildDiagnostics({
|
||||
selectedUnknownNodeId: selectedUnknownNodeId ?? null,
|
||||
decompositionApplied: decompositionApplied ?? false,
|
||||
questionComplexityAssessment: questionComplexityAssessment ?? null,
|
||||
answerabilityAssessment: answerabilityAssessment ?? null,
|
||||
independentlyAnswerable: independentlyAnswerable ?? null,
|
||||
prerequisiteConceptCount: prerequisiteConceptCount ?? null,
|
||||
decompositionTriggeredByAnswerability:
|
||||
decompositionTriggeredByAnswerability ?? false,
|
||||
decompositionReason: decompositionReason ?? null,
|
||||
selectedContainerUnknown: selectedContainerUnknown ?? null,
|
||||
selectedChildUnknown: selectedChildUnknown ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -341,6 +356,20 @@ export async function startCase(body) {
|
||||
initialQuestionResult.decompositionPerformed ?? false,
|
||||
questionComplexityAssessment:
|
||||
initialQuestionResult.questionComplexityAssessment ?? null,
|
||||
answerabilityAssessment:
|
||||
initialQuestionResult.answerabilityAssessment ?? null,
|
||||
independentlyAnswerable:
|
||||
initialQuestionResult.independentlyAnswerable ?? null,
|
||||
prerequisiteConceptCount:
|
||||
initialQuestionResult.prerequisiteConceptCount ?? null,
|
||||
decompositionTriggeredByAnswerability:
|
||||
initialQuestionResult.decompositionTriggeredByAnswerability ?? false,
|
||||
decompositionReason:
|
||||
initialQuestionResult.selectedQuestion?.reason ?? null,
|
||||
selectedContainerUnknown:
|
||||
initialQuestionResult.selectedContainerUnknown ?? null,
|
||||
selectedChildUnknown:
|
||||
initialQuestionResult.selectedChildUnknown ?? null,
|
||||
}),
|
||||
validationErrors: graphReferenceValidation.errors,
|
||||
statusCode: 500,
|
||||
@@ -371,6 +400,19 @@ export async function startCase(body) {
|
||||
initialQuestionResult.decompositionPerformed ?? false,
|
||||
questionComplexityAssessment:
|
||||
initialQuestionResult.questionComplexityAssessment ?? null,
|
||||
answerabilityAssessment:
|
||||
initialQuestionResult.answerabilityAssessment ?? null,
|
||||
independentlyAnswerable:
|
||||
initialQuestionResult.independentlyAnswerable ?? null,
|
||||
prerequisiteConceptCount:
|
||||
initialQuestionResult.prerequisiteConceptCount ?? null,
|
||||
decompositionTriggeredByAnswerability:
|
||||
initialQuestionResult.decompositionTriggeredByAnswerability ?? false,
|
||||
decompositionReason:
|
||||
initialQuestionResult.selectedQuestion?.reason ?? null,
|
||||
selectedContainerUnknown:
|
||||
initialQuestionResult.selectedContainerUnknown ?? null,
|
||||
selectedChildUnknown: initialQuestionResult.selectedChildUnknown ?? null,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -569,6 +569,93 @@ function isDirectlyAnswerableObservationChildText(text) {
|
||||
);
|
||||
}
|
||||
|
||||
function countPrerequisiteConceptSignals(text) {
|
||||
let count = 0;
|
||||
|
||||
if (/\bproblem\b/.test(text)) count += 1;
|
||||
if (/\b(audience|customer|user|buyer|stakeholder|recipient)\b/.test(text))
|
||||
count += 1;
|
||||
if (/\b(demand|seek help|actively look for help)\b/.test(text)) count += 1;
|
||||
if (/\b(pay|willingness to pay|price|pricing)\b/.test(text)) count += 1;
|
||||
if (
|
||||
/\b(compare|comparison|different from|alternatives|alternative|existing alternatives|existing tools|better than)\b/.test(
|
||||
text,
|
||||
)
|
||||
)
|
||||
count += 1;
|
||||
if (/\b(value|viability|justified|business case|commercial)\b/.test(text))
|
||||
count += 1;
|
||||
if (/\b(feasibility|technical)\b/.test(text)) count += 1;
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
function countIndependentAnswerDimensions(node, graph) {
|
||||
const text = normaliseText(`${node?.label || ""} ${node?.description || ""}`);
|
||||
const relatedNodes = collectRelatedNodes(node, graph);
|
||||
const unresolvedDependencies = relatedNodes.filter(
|
||||
(relatedNode) =>
|
||||
relatedNode.kind === "unknown" && relatedNode.status !== "resolved",
|
||||
).length;
|
||||
const conjunctionCount = (text.match(/\b(and|or)\b/g) || []).length;
|
||||
const prerequisiteConceptCount = countPrerequisiteConceptSignals(text);
|
||||
const implicitConclusion =
|
||||
/\b(commercially justified|commercial justification|commercial viability|business case|customer value|market demand|product validation|technical feasibility)\b/.test(
|
||||
text,
|
||||
) ||
|
||||
(/\bevidence that\b/.test(text) &&
|
||||
/\b(problem|need|demand|audience|customer|user|alternatives|better than)\b/.test(
|
||||
text,
|
||||
)) ||
|
||||
(/\bwhether\b/.test(text) &&
|
||||
/\b(addresses|solve|solves|justifies|supports|demonstrates)\b/.test(
|
||||
text,
|
||||
) &&
|
||||
/\b(problem|value|need|demand|audience|customer|user)\b/.test(text));
|
||||
|
||||
return {
|
||||
prerequisiteConceptCount,
|
||||
unresolvedDependencies,
|
||||
conjunctionCount,
|
||||
implicitConclusion,
|
||||
multipleEvidenceDimensions:
|
||||
prerequisiteConceptCount >= 2 || conjunctionCount >= 2,
|
||||
};
|
||||
}
|
||||
|
||||
export function assessUnknownAnswerability({ node, graph }) {
|
||||
const text = normaliseText(`${node?.label || ""} ${node?.description || ""}`);
|
||||
const dimensionSummary = countIndependentAnswerDimensions(node, graph);
|
||||
const independentlyAnswerable =
|
||||
!dimensionSummary.implicitConclusion &&
|
||||
!dimensionSummary.multipleEvidenceDimensions &&
|
||||
dimensionSummary.unresolvedDependencies === 0 &&
|
||||
dimensionSummary.prerequisiteConceptCount <= 1;
|
||||
|
||||
if (independentlyAnswerable) {
|
||||
return {
|
||||
independentlyAnswerable: true,
|
||||
reason:
|
||||
"This unknown can be answered directly without first resolving several prerequisite investigations.",
|
||||
prerequisiteConceptCount: dimensionSummary.prerequisiteConceptCount,
|
||||
decompositionRequired: false,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
independentlyAnswerable: false,
|
||||
reason: dimensionSummary.implicitConclusion
|
||||
? "This unknown asks for a higher-level conclusion that depends on several smaller investigations."
|
||||
: "This unknown still bundles multiple prerequisite evidence dimensions, so it should be decomposed before it becomes the selected question.",
|
||||
prerequisiteConceptCount: Math.max(
|
||||
dimensionSummary.prerequisiteConceptCount,
|
||||
dimensionSummary.unresolvedDependencies,
|
||||
dimensionSummary.conjunctionCount + 1,
|
||||
),
|
||||
decompositionRequired: true,
|
||||
};
|
||||
}
|
||||
|
||||
export function assessUnknownAtomicity({ node, graph }) {
|
||||
const nodeText = normaliseText(
|
||||
`${node?.label || ""} ${node?.description || ""}`,
|
||||
|
||||
Reference in New Issue
Block a user