feat: decompose non-answerable unknowns
This commit is contained in:
@@ -25,6 +25,8 @@ The engine should ask one question about one primary concept at a time.
|
||||
|
||||
**The reconstruction model may suggest a question, but only the graph-backed deterministic pipeline may select the user-facing question.**
|
||||
|
||||
**A node is only questionable if it is independently answerable.**
|
||||
|
||||
## One-question / one-concept rule
|
||||
|
||||
Every user-facing question should:
|
||||
@@ -72,6 +74,26 @@ For the current scenario, this meant creating child unknowns such as:
|
||||
|
||||
The selector then reaches the first foundational child through prerequisite ordering encoded in the decomposition graph rather than through global scoring changes.
|
||||
|
||||
## Atomicity vs answerability
|
||||
|
||||
These are different reasoning properties.
|
||||
|
||||
- **Atomicity** asks: does this node describe one investigation or several bundled investigations?
|
||||
- **Answerability** asks: even if the wording looks singular, can this node be answered directly without first resolving multiple prerequisite dimensions?
|
||||
|
||||
A node can appear atomic in wording but still fail answerability.
|
||||
|
||||
Examples include broad evaluation containers such as product validation, customer value, business case, technical feasibility, or commercial justification. These often compress several prerequisite investigations into one conclusion-shaped unknown.
|
||||
|
||||
That means atomicity alone is not enough.
|
||||
|
||||
The engine now decomposes whenever either of these is true:
|
||||
|
||||
- the unknown is not atomic
|
||||
- the unknown is not independently answerable
|
||||
|
||||
This prevents a broad container node from becoming the selected question target even when its wording looks grammatically singular.
|
||||
|
||||
## UI result
|
||||
|
||||
The long compound question no longer survives as the first follow-up in the tested path.
|
||||
|
||||
+184
-8
@@ -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,7 +1714,17 @@ export function determineGraphBackedQuestion({ situationGraph }) {
|
||||
updatedSituationGraph.reasoningState = buildReasoningState(
|
||||
updatedSituationGraph,
|
||||
);
|
||||
deterministicSelection = selectActiveUnknownCandidate(
|
||||
deterministicSelection = isSelectableUnresolvedUnknown(
|
||||
updatedSituationGraph,
|
||||
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 || [],
|
||||
);
|
||||
@@ -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 composite parent already has decomposition children, so they should be reused instead of regenerated.";
|
||||
"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 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,
|
||||
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 || ""}`,
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
assessUnknownAnswerability,
|
||||
assessUnknownAtomicity,
|
||||
} from "@/lib/graph/question-formulator.js";
|
||||
import { applyValidatedProposal } from "@/lib/graph/apply-proposal.js";
|
||||
import { makeGraph, makeNode } from "@/lib/graph/schema.js";
|
||||
|
||||
const COMMERCIAL_SCENARIO =
|
||||
"I have developed a new reasoning method that aims to help people determine whether they have enough justified confidence to make a decision. I believe it could become a commercial product, but I do not yet know whether it solves a genuine problem, whether people would value it enough to pay for it, or whether it is fundamentally different from existing AI tools. Before investing significant time and money into building it further, I want to determine whether continuing development is commercially justified.";
|
||||
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
||||
function makeCommercialContainerGraph() {
|
||||
const parent = makeNode({
|
||||
id: "n-commercial-parent",
|
||||
label:
|
||||
"Commercial justification for whether continuing development is commercially justified",
|
||||
description:
|
||||
"Need to know whether this solves a genuine problem, whether people would value it enough to pay for it, and whether it is commercially justified before continuing development.",
|
||||
kind: "unknown",
|
||||
status: "unknown",
|
||||
confidence: "medium",
|
||||
});
|
||||
|
||||
return makeGraph({
|
||||
centralStatement: COMMERCIAL_SCENARIO,
|
||||
nodes: [parent],
|
||||
edges: [],
|
||||
activeUnknownNodeId: parent.id,
|
||||
resolvedNodeIds: [],
|
||||
currentSummary: "Commercial answerability fixture",
|
||||
});
|
||||
}
|
||||
|
||||
describe("assessUnknownAnswerability", () => {
|
||||
it("flags commercial-validation container unknowns as non-answerable", () => {
|
||||
const graph = makeCommercialContainerGraph();
|
||||
const unknown = graph.nodes[0];
|
||||
|
||||
const atomicity = assessUnknownAtomicity({ node: unknown, graph });
|
||||
const answerability = assessUnknownAnswerability({ node: unknown, graph });
|
||||
|
||||
expect(atomicity.atomicity).toBe("composite");
|
||||
expect(answerability.independentlyAnswerable).toBe(false);
|
||||
expect(answerability.decompositionRequired).toBe(true);
|
||||
expect(answerability.prerequisiteConceptCount).toBeGreaterThan(1);
|
||||
});
|
||||
|
||||
it("keeps one-concept denominator unknowns independently answerable", () => {
|
||||
const unknown = makeNode({
|
||||
id: "n-denominator",
|
||||
label: "Complaint rate denominator",
|
||||
description:
|
||||
"Need the denominator because it directly determines the complaint rate.",
|
||||
kind: "unknown",
|
||||
status: "unknown",
|
||||
confidence: "high",
|
||||
});
|
||||
const graph = makeGraph({
|
||||
centralStatement: "Production increased while complaints increased.",
|
||||
nodes: [unknown],
|
||||
edges: [],
|
||||
activeUnknownNodeId: unknown.id,
|
||||
resolvedNodeIds: [],
|
||||
currentSummary: "Denominator answerability fixture",
|
||||
});
|
||||
|
||||
const result = assessUnknownAnswerability({ node: unknown, graph });
|
||||
|
||||
expect(result.independentlyAnswerable).toBe(true);
|
||||
expect(result.decompositionRequired).toBe(false);
|
||||
expect(result.prerequisiteConceptCount).toBeLessThanOrEqual(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("answerability-triggered decomposition", () => {
|
||||
it("decomposes a non-answerable parent into independently answerable child investigations", () => {
|
||||
const graph = makeCommercialContainerGraph();
|
||||
|
||||
const result = applyValidatedProposal({
|
||||
situationGraph: graph,
|
||||
proposal: makeMeaningfulNoOpProposal(),
|
||||
});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.decompositionPerformed).toBe(true);
|
||||
expect(result.decompositionTriggeredByAnswerability).toBe(true);
|
||||
expect(result.selectedContainerUnknown).toBe("n-commercial-parent");
|
||||
expect(result.selectedChildUnknown).toBe(result.selectedUnknownAfter);
|
||||
expect(result.independentlyAnswerable).toBe(false);
|
||||
expect(result.prerequisiteConceptCount).toBeGreaterThan(1);
|
||||
expect(result.selectedUnknownAfter).not.toBe("n-commercial-parent");
|
||||
expect(result.selectedQuestion.question).toBe(
|
||||
"Who experiences this problem?",
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps the parent unresolved while selecting a child unknown", () => {
|
||||
const graph = makeCommercialContainerGraph();
|
||||
|
||||
const result = applyValidatedProposal({
|
||||
situationGraph: graph,
|
||||
proposal: makeMeaningfulNoOpProposal(),
|
||||
});
|
||||
|
||||
const parentNode = result.updatedSituationGraph.nodes.find(
|
||||
(node) => node.id === "n-commercial-parent",
|
||||
);
|
||||
const selectedChild = result.updatedSituationGraph.nodes.find(
|
||||
(node) => node.id === result.selectedUnknownAfter,
|
||||
);
|
||||
|
||||
expect(parentNode.status).toBe("unknown");
|
||||
expect(selectedChild.parentId).toBe(parentNode.id);
|
||||
expect(selectedChild.status).toBe("unknown");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user