feat: enforce one-concept questions

This commit is contained in:
2026-08-03 08:40:39 +01:00
parent 449cf996dc
commit 3c0f7f5a45
5 changed files with 634 additions and 11 deletions
@@ -0,0 +1,92 @@
# v0.7 Question Simplicity Experiment
## Observed failure
The first v0.7 UI scenario exposed a reasoning failure where the selected unknown could still be directionally correct while the resulting question was too large to answer in one coherent response.
Example failure:
> Have you measured the current financial or operational cost to users who lack justified confidence, and what baseline budget do they currently allocate for comparable decision-support methods?
This question bundled multiple investigations:
- cost
- user impact
- existing alternatives
- current budget
That violated the intended one-step reasoning discipline.
## Principle
**A correct unknown paired with an unanswerably broad question is still a reasoning failure.**
The engine should ask one question about one primary concept at a time.
## One-question / one-concept rule
Every user-facing question should:
- contain one question mark
- target one unresolved graph node
- ask for one primary concept
- request one coherent answer
- avoid joined investigations
- minimise cognitive effort while still reducing meaningful uncertainty
## Deterministic cognitive-load rules
The new deterministic question-complexity assessment marks a question as too broad when it shows signals such as:
- multiple requested answers joined by `and`
- distinct measures combined in one prompt, such as cost plus budget
- comma-list phrasing that expands the request into several sub-questions
- more than one primary concept
- abstract noun chains that make the question hard to parse on first reading
- very long question length
The assessment returns:
- `acceptable`
- `primaryConceptCount`
- `compoundQuestionSignals`
- `abstractTermCount`
- `cognitiveLoad`
- `reasons`
## Decomposition-before-rewording rule
The engine now treats broad commercial-validation unknowns as composite.
If the selected unknown still spans multiple validation dimensions, the system should not simply shorten the sentence. It should first decompose the unknown into smaller child unknowns and then select one foundational child.
For the current scenario, this meant creating child unknowns such as:
- who experiences the problem
- what happens when it is not resolved
- how often it happens
- how people deal with it today
- whether people actively look for help
The selector then reaches the first foundational child through prerequisite ordering encoded in the decomposition graph rather than through global scoring changes.
## UI result
The long compound question no longer survives as the first follow-up in the tested path.
The new first-step question is:
> Who experiences this problem?
This question:
- asks one thing
- is understandable immediately
- stays graph-backed
- avoids pricing or budget before problem existence is established
## Remaining limitations
- question-complexity assessment is still conservative and pattern-based rather than semantic in a richer linguistic sense
- plain-language simplification currently uses a small deterministic replacement set
- broader prerequisite ordering is strongest for decomposition structures that explicitly encode those dependencies
+111 -11
View File
@@ -1385,10 +1385,74 @@ function describeObservationFocus(context, which) {
} }
function buildDecompositionTemplates(parentNode, graph, depth = 0) { function buildDecompositionTemplates(parentNode, graph, depth = 0) {
const parentText = normaliseText(
`${parentNode?.label || ""} ${parentNode?.description || ""}`,
);
const context = buildDecompositionContext(graph); const context = buildDecompositionContext(graph);
const firstFocus = describeObservationFocus(context, "first"); const firstFocus = describeObservationFocus(context, "first");
const secondFocus = describeObservationFocus(context, "second"); const secondFocus = describeObservationFocus(context, "second");
if (
/\b(genuine problem|commercially justified|commercial justification|people would value|pay for it|justified confidence|decision support methods|willingness to pay|seek help)\b/.test(
parentText,
)
) {
return [
{
label: "Who experiences this problem",
description:
"Need to know who experiences this problem, because that must be clear before deciding whether it is commercially justified.",
dependsOnLabels: [],
},
{
label: "What happens when this problem is not resolved",
description:
"Need to know what happens when this problem is not resolved, because that is needed before judging whether the problem matters.",
dependsOnLabels: ["Who experiences this problem"],
},
{
label: "How often this problem happens",
description:
"Need to know how often this problem happens, because that helps judge whether it is a real recurring problem.",
dependsOnLabels: ["Who experiences this problem"],
},
{
label: "How people deal with this problem today",
description:
"Need to know how people deal with this problem today, because that is needed before comparing alternatives or value.",
dependsOnLabels: [
"Who experiences this problem",
"What happens when this problem is not resolved",
"How often this problem happens",
],
},
depth === 0
? {
label: "Whether people actively look for help with this problem",
description:
"Need to know whether people actively look for help with this problem, because that is needed before judging demand or willingness to pay.",
dependsOnLabels: [
"Who experiences this problem",
"What happens when this problem is not resolved",
"How often this problem happens",
"How people deal with this problem today",
],
}
: {
label: "Whether people would pay to solve this problem",
description:
"Need to know whether people would pay to solve this problem, because that can only be judged after the problem itself is established.",
dependsOnLabels: [
"Who experiences this problem",
"What happens when this problem is not resolved",
"How often this problem happens",
"How people deal with this problem today",
"Whether people actively look for help with this problem",
],
},
];
}
if (/\btiming or measurement basis\b/i.test(parentNode.label)) { if (/\btiming or measurement basis\b/i.test(parentNode.label)) {
return [ return [
{ {
@@ -1434,14 +1498,25 @@ function buildDecompositionTemplates(parentNode, graph, depth = 0) {
function buildCompositeUnknownChildren(parentNode, graph, depth = 0) { function buildCompositeUnknownChildren(parentNode, graph, depth = 0) {
const templates = buildDecompositionTemplates(parentNode, graph, depth); const templates = buildDecompositionTemplates(parentNode, graph, depth);
const candidateNodes = templates.map( const labelToId = new Map(
(template) => templates.map((template) => [
findEquivalentDecompositionChild( template.label,
graph, buildDecompositionChildId(parentNode.id, template.label),
parentNode.id, ]),
template.label, );
template.description, const candidateNodes = templates.map((template) => {
) || { const existing = findEquivalentDecompositionChild(
graph,
parentNode.id,
template.label,
template.description,
);
const dependsOn = (template.dependsOnLabels || [])
.map((label) => labelToId.get(label))
.filter(Boolean);
return (
existing || {
id: buildDecompositionChildId(parentNode.id, template.label), id: buildDecompositionChildId(parentNode.id, template.label),
label: template.label, label: template.label,
description: template.description, description: template.description,
@@ -1451,12 +1526,13 @@ function buildCompositeUnknownChildren(parentNode, graph, depth = 0) {
value: null, value: null,
unit: null, unit: null,
evidenceIds: [], evidenceIds: [],
dependsOn: [], dependsOn,
affects: [], affects: [],
parentId: parentNode.id, parentId: parentNode.id,
childIds: [], childIds: [],
}, }
); );
});
const childNodes = []; const childNodes = [];
const childEdges = []; const childEdges = [];
const childNodeIds = []; const childNodeIds = [];
@@ -1566,9 +1642,14 @@ function runDeterministicDecomposition({
let proposedChildCount = 0; let proposedChildCount = 0;
let acceptedChildCount = 0; let acceptedChildCount = 0;
let selectedChildNodeId = null; let selectedChildNodeId = null;
let selectedUnknownBefore =
deterministicSelection?.status === "selected"
? deterministicSelection.nodeId
: null;
let decompositionStoppedReason = null; let decompositionStoppedReason = null;
let rejectedChildren = []; let rejectedChildren = [];
let childQualitySummary = []; let childQualitySummary = [];
let decompositionTriggeredByQuestionComplexity = false;
while (workingSelection?.status === "selected" && workingSelection?.nodeId) { while (workingSelection?.status === "selected" && workingSelection?.nodeId) {
const selectedNode = findNodeById(workingGraph, workingSelection.nodeId); const selectedNode = findNodeById(workingGraph, workingSelection.nodeId);
@@ -1699,6 +1780,8 @@ function runDeterministicDecomposition({
rejectedChildren, rejectedChildren,
childQualitySummary, childQualitySummary,
selectedChildNodeId, selectedChildNodeId,
selectedUnknownBefore,
decompositionTriggeredByQuestionComplexity,
}; };
} }
@@ -2121,6 +2204,10 @@ export function applyValidatedProposal({
}) })
: null; : null;
const questionComplexity = formulatedQuestion?.questionComplexity ?? null;
const plainLanguageNormalisations =
formulatedQuestion?.plainLanguageNormalisations ?? [];
const finalSelectedQuestion = const finalSelectedQuestion =
deterministicSelection?.status === "ambiguous" deterministicSelection?.status === "ambiguous"
? { ? {
@@ -2137,6 +2224,8 @@ export function applyValidatedProposal({
reason: formulatedQuestion?.reason || deterministicSelection.reason, reason: formulatedQuestion?.reason || deterministicSelection.reason,
strategy: formulatedQuestion?.strategy, strategy: formulatedQuestion?.strategy,
investigationStrategy: formulatedQuestion?.investigationStrategy, investigationStrategy: formulatedQuestion?.investigationStrategy,
questionComplexity,
plainLanguageNormalisations,
} }
: null; : null;
@@ -2202,6 +2291,17 @@ export function applyValidatedProposal({
rejectedChildren, rejectedChildren,
selectedChildNodeId, selectedChildNodeId,
childQualitySummary, childQualitySummary,
selectedUnknownBefore: decompositionResult.selectedUnknownBefore,
selectedUnknownAfter: deterministicSelection?.nodeId ?? null,
questionComplexityAccepted: questionComplexity?.acceptable ?? null,
primaryConceptCount: questionComplexity?.primaryConceptCount ?? null,
cognitiveLoad: questionComplexity?.cognitiveLoad ?? null,
complexityReasons: questionComplexity?.reasons ?? [],
decompositionTriggeredByQuestionComplexity:
decompositionResult.decompositionTriggeredByQuestionComplexity ?? false,
previousQuestion,
finalQuestion: finalSelectedQuestion?.question ?? null,
plainLanguageNormalisations,
propagationPerformed, propagationPerformed,
resolvedChildNodeId, resolvedChildNodeId,
parentNodeId, parentNodeId,
+54
View File
@@ -133,6 +133,16 @@ function buildUpdateDiagnostics({
childUnknownCount, childUnknownCount,
childNodeIds, childNodeIds,
atomicityReason, atomicityReason,
questionComplexityAccepted,
primaryConceptCount,
cognitiveLoad,
complexityReasons,
decompositionTriggeredByQuestionComplexity,
previousQuestion,
finalQuestion,
selectedUnknownBefore,
selectedUnknownAfter,
plainLanguageNormalisations,
}) { }) {
return { return {
promptVersion: promptVersion ?? "v0.4", promptVersion: promptVersion ?? "v0.4",
@@ -202,6 +212,17 @@ function buildUpdateDiagnostics({
childUnknownCount: childUnknownCount ?? 0, childUnknownCount: childUnknownCount ?? 0,
childNodeIds: childNodeIds ?? [], childNodeIds: childNodeIds ?? [],
atomicityReason: atomicityReason ?? null, atomicityReason: atomicityReason ?? null,
questionComplexityAccepted: questionComplexityAccepted ?? null,
primaryConceptCount: primaryConceptCount ?? null,
cognitiveLoad: cognitiveLoad ?? null,
complexityReasons: complexityReasons ?? [],
decompositionTriggeredByQuestionComplexity:
decompositionTriggeredByQuestionComplexity ?? false,
previousQuestion: previousQuestion ?? null,
finalQuestion: finalQuestion ?? null,
selectedUnknownBefore: selectedUnknownBefore ?? null,
selectedUnknownAfter: selectedUnknownAfter ?? null,
plainLanguageNormalisations: plainLanguageNormalisations ?? [],
unknownSelectionExplanation: unknownSelectionExplanation ?? null, unknownSelectionExplanation: unknownSelectionExplanation ?? null,
}; };
} }
@@ -495,6 +516,16 @@ async function updateCaseWithDependencies(body, dependencies = {}) {
childUnknownCount: 0, childUnknownCount: 0,
childNodeIds: [], childNodeIds: [],
atomicityReason: null, atomicityReason: null,
questionComplexityAccepted: null,
primaryConceptCount: null,
cognitiveLoad: null,
complexityReasons: [],
decompositionTriggeredByQuestionComplexity: false,
previousQuestion,
finalQuestion: null,
selectedUnknownBefore: null,
selectedUnknownAfter: null,
plainLanguageNormalisations: [],
unknownSelectionExplanation: explainUnknownSelection( unknownSelectionExplanation: explainUnknownSelection(
situationGraph, situationGraph,
situationGraph.resolvedNodeIds || [], situationGraph.resolvedNodeIds || [],
@@ -582,6 +613,19 @@ async function updateCaseWithDependencies(body, dependencies = {}) {
childUnknownCount: applicationResult.childUnknownCount, childUnknownCount: applicationResult.childUnknownCount,
childNodeIds: applicationResult.childNodeIds, childNodeIds: applicationResult.childNodeIds,
atomicityReason: applicationResult.atomicityReason, atomicityReason: applicationResult.atomicityReason,
questionComplexityAccepted:
applicationResult.questionComplexityAccepted,
primaryConceptCount: applicationResult.primaryConceptCount,
cognitiveLoad: applicationResult.cognitiveLoad,
complexityReasons: applicationResult.complexityReasons,
decompositionTriggeredByQuestionComplexity:
applicationResult.decompositionTriggeredByQuestionComplexity,
previousQuestion: applicationResult.previousQuestion,
finalQuestion: applicationResult.finalQuestion,
selectedUnknownBefore: applicationResult.selectedUnknownBefore,
selectedUnknownAfter: applicationResult.selectedUnknownAfter,
plainLanguageNormalisations:
applicationResult.plainLanguageNormalisations,
unknownSelectionExplanation: buildUnknownSelectionDiagnostics( unknownSelectionExplanation: buildUnknownSelectionDiagnostics(
applicationResult.updatedSituationGraph, applicationResult.updatedSituationGraph,
applicationResult.updatedSituationGraph.resolvedNodeIds || [], applicationResult.updatedSituationGraph.resolvedNodeIds || [],
@@ -650,6 +694,16 @@ async function updateCaseWithDependencies(body, dependencies = {}) {
childUnknownCount: 0, childUnknownCount: 0,
childNodeIds: [], childNodeIds: [],
atomicityReason: null, atomicityReason: null,
questionComplexityAccepted: null,
primaryConceptCount: null,
cognitiveLoad: null,
complexityReasons: [],
decompositionTriggeredByQuestionComplexity: false,
previousQuestion,
finalQuestion: null,
selectedUnknownBefore: null,
selectedUnknownAfter: null,
plainLanguageNormalisations: [],
unknownSelectionExplanation: buildUnknownSelectionDiagnostics( unknownSelectionExplanation: buildUnknownSelectionDiagnostics(
situationGraph, situationGraph,
situationGraph.resolvedNodeIds || [], situationGraph.resolvedNodeIds || [],
+174
View File
@@ -498,6 +498,33 @@ function buildBroadInvestigationQuestion(graph) {
return `What changed during that period that could help explain why ${central}?`; return `What changed during that period that could help explain why ${central}?`;
} }
function buildFoundationalDirectQuestion(node) {
const label = stripTrailingPunctuation(node?.label || "");
if (/^who experiences this problem$/i.test(label)) {
return "Who experiences this problem?";
}
if (/^what happens when this problem is not resolved$/i.test(label)) {
return "What happens when this problem is not resolved?";
}
if (/^how often this problem happens$/i.test(label)) {
return "How often does this problem happen?";
}
if (/^how people deal with this problem today$/i.test(label)) {
return "How do people deal with this problem today?";
}
if (
/^whether people actively look for help with this problem$/i.test(label)
) {
return "Do people actively look for help with this problem?";
}
if (/^whether people would pay to solve this problem$/i.test(label)) {
return "Would people pay to solve this problem?";
}
return null;
}
function isRelationshipExplanationUnknown(node, graph) { function isRelationshipExplanationUnknown(node, graph) {
const text = normaliseText(`${node?.label || ""} ${node?.description || ""}`); const text = normaliseText(`${node?.label || ""} ${node?.description || ""}`);
return ( return (
@@ -514,6 +541,12 @@ function isBroadCompositeUnknownText(text) {
); );
} }
function isCommercialValidationUnknownText(text) {
return /\b(genuine problem|people would value|pay for it|commercially justified|commercial justification|commercial value|justified confidence|decision support methods|decision support|budget do they currently allocate|willingness to pay|problem existence|seek help)\b/.test(
text,
);
}
function hasCompoundAbstractSignals(text) { function hasCompoundAbstractSignals(text) {
return ( return (
/\b(timing|measurement|basis|cost|costs|debt|stock|tax|capital spending|mix|segment)\s+(and|or)\s+\b/.test( /\b(timing|measurement|basis|cost|costs|debt|stock|tax|capital spending|mix|segment)\s+(and|or)\s+\b/.test(
@@ -592,6 +625,15 @@ export function assessUnknownAtomicity({ node, graph }) {
}; };
} }
if (isCommercialValidationUnknownText(nodeText)) {
return {
atomicity: "composite",
reason:
"This unknown combines multiple problem-validation or commercial-validation dimensions, so it should be decomposed before asking a direct question.",
decompositionKind: "commercial_validation",
};
}
return { return {
atomicity: "atomic", atomicity: "atomic",
reason: reason:
@@ -998,11 +1040,133 @@ function validateFormulatedQuestion(question, meaning) {
return true; return true;
} }
function countPrimaryConcepts(question) {
let concepts = 1;
if (/\band what\b/i.test(question)) concepts += 1;
if (/\bhow often\b.*\bwhat\b/i.test(question)) concepts += 1;
if (/\bcost\b.*\bbudget\b|\bbudget\b.*\bcost\b/i.test(question)) {
concepts += 1;
}
if (/\bwho\b.*\bwhat\b|\bwhat\b.*\bwho\b/i.test(question)) concepts += 1;
return concepts;
}
export function assessQuestionComplexity({ question, selectedUnknown, graph }) {
const text = String(question || "").trim();
const lower = text.toLowerCase();
const reasons = [];
const compoundQuestionSignals = [];
const abstractMatches =
lower.match(
/\b(justified confidence|financial or operational cost|comparable decision support methods|commercial justification|decision support methods|value recipient)\b/g,
) || [];
const primaryConceptCount = countPrimaryConcepts(lower);
if ((text.match(/\?/g) || []).length !== 1) {
reasons.push("multiple_question_marks");
compoundQuestionSignals.push("multiple_question_marks");
}
if (/\band what\b|\bwhat .* and .* what\b/i.test(text)) {
reasons.push("multiple_requested_answers");
compoundQuestionSignals.push("joined_requests");
}
if (/,[^,]{0,60},/.test(text) || /,\s*(and|or)\b/i.test(text)) {
reasons.push("list_like_question");
compoundQuestionSignals.push("comma_list");
}
if (/\bcost\b.*\bbudget\b|\bbudget\b.*\bcost\b/i.test(text)) {
reasons.push("cost_and_budget_combined");
compoundQuestionSignals.push("distinct_measures_combined");
}
if (primaryConceptCount > 1) {
reasons.push("multiple_primary_concepts");
}
if (text.split(/\s+/).length > 20) {
reasons.push("very_long_question");
}
if (abstractMatches.length > 1) {
reasons.push("abstract_term_chain");
}
const cognitiveLoad =
reasons.length >= 3 ? "high" : reasons.length === 2 ? "medium" : "low";
return {
acceptable: reasons.length === 0,
primaryConceptCount,
compoundQuestionSignals: [...new Set(compoundQuestionSignals)],
abstractTermCount: abstractMatches.length,
cognitiveLoad,
reasons,
selectedUnknownId: selectedUnknown?.id ?? null,
graphCentralStatement: graph?.centralStatement ?? null,
};
}
function applyPlainLanguageNormalisations(question) {
const normalisations = [];
let next = String(question || "");
const replacements = [
[
/individuals experiencing insufficient justified confidence/gi,
"people who struggle to feel confident about a decision",
"simplified_justified_confidence_phrase",
],
[
/current financial or operational cost/gi,
"current cost",
"simplified_cost_phrase",
],
[
/comparable decision support methods/gi,
"other ways they deal with the problem",
"simplified_decision_support_phrase",
],
[
/the relevant customer, user, or value recipient/gi,
"the people affected",
"simplified_actor_phrase",
],
];
for (const [pattern, replacement, code] of replacements) {
if (pattern.test(next)) {
next = next.replace(pattern, replacement);
normalisations.push(code);
}
}
next = sanitizeQuestionText(next);
return { question: next, normalisations };
}
export function formulateQuestion({ node, graph, context = {} }) { export function formulateQuestion({ node, graph, context = {} }) {
if (context.selectionState?.status === "ambiguous") { if (context.selectionState?.status === "ambiguous") {
return formulateTieResolutionQuestion({ graph }); return formulateTieResolutionQuestion({ graph });
} }
const foundationalDirectQuestion = buildFoundationalDirectQuestion(node);
if (foundationalDirectQuestion) {
const plainLanguage = applyPlainLanguageNormalisations(
sanitizeQuestionText(foundationalDirectQuestion),
);
const questionComplexity = assessQuestionComplexity({
question: plainLanguage.question,
selectedUnknown: node,
graph,
});
return {
question: plainLanguage.question,
reason:
"Formulated as a direct foundational question because this child unknown should be answered one step at a time.",
strategy: null,
investigationStrategy: null,
questionComplexity,
plainLanguageNormalisations: plainLanguage.normalisations,
};
}
const investigationStrategy = selectInvestigationStrategy({ const investigationStrategy = selectInvestigationStrategy({
node, node,
graph, graph,
@@ -1016,6 +1180,8 @@ export function formulateQuestion({ node, graph, context = {} }) {
: buildNeutralClarificationQuestion(extractMeaning(node)); : buildNeutralClarificationQuestion(extractMeaning(node));
question = sanitizeQuestionText(question); question = sanitizeQuestionText(question);
const plainLanguage = applyPlainLanguageNormalisations(question);
question = plainLanguage.question;
const fallbackMeaning = extractMeaning(node); const fallbackMeaning = extractMeaning(node);
if ( if (
@@ -1044,6 +1210,12 @@ export function formulateQuestion({ node, graph, context = {} }) {
); );
} }
const questionComplexity = assessQuestionComplexity({
question,
selectedUnknown: node,
graph,
});
return { return {
question, question,
reason: investigationStrategy reason: investigationStrategy
@@ -1051,5 +1223,7 @@ export function formulateQuestion({ node, graph, context = {} }) {
: "Formulated as a neutral clarification question because no narrower investigation strategy clearly applied.", : "Formulated as a neutral clarification question because no narrower investigation strategy clearly applied.",
strategy: investigationStrategy?.key ?? null, strategy: investigationStrategy?.key ?? null,
investigationStrategy, investigationStrategy,
questionComplexity,
plainLanguageNormalisations: plainLanguage.normalisations,
}; };
} }
+203
View File
@@ -0,0 +1,203 @@
import { describe, expect, it } from "vitest";
import {
assessQuestionComplexity,
assessUnknownAtomicity,
formulateQuestion,
} from "@/lib/graph/question-formulator.js";
import { applyValidatedProposal } from "@/lib/graph/apply-proposal.js";
import { makeGraph, makeNode } from "@/lib/graph/schema.js";
const 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 makeCommercialValidationGraph() {
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: SCENARIO,
nodes: [parent],
edges: [],
activeUnknownNodeId: parent.id,
resolvedNodeIds: [],
currentSummary: "Commercial validation question simplicity fixture",
});
}
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,
};
}
describe("question simplicity", () => {
it("rejects the original long compound financial-cost plus budget question", () => {
const graph = makeCommercialValidationGraph();
const unknown = graph.nodes.find(
(node) => node.id === "n-commercial-parent",
);
const question =
"Have you measured the current financial or operational cost to users who lack justified confidence, and what baseline budget do they currently allocate for comparable decision-support methods?";
const result = assessQuestionComplexity({
question,
selectedUnknown: unknown,
graph,
});
expect(result.acceptable).toBe(false);
expect(result.primaryConceptCount).toBeGreaterThan(1);
expect(result.cognitiveLoad).toBe("high");
expect(result.reasons).toContain("multiple_requested_answers");
expect(result.reasons).toContain("cost_and_budget_combined");
});
it("accepts one simple concept question", () => {
const graph = makeCommercialValidationGraph();
const unknown = makeNode({
id: "n-who",
label: "Who experiences this problem",
description:
"Need to know who experiences this problem, because that must be clear before deciding whether it is commercially justified.",
kind: "unknown",
status: "unknown",
confidence: "medium",
parentId: "n-commercial-parent",
});
const result = formulateQuestion({ node: unknown, graph });
expect(result.question).toBe("Who experiences this problem?");
expect(result.questionComplexity.acceptable).toBe(true);
expect(result.questionComplexity.primaryConceptCount).toBe(1);
expect(result.question.match(/\?/g) || []).toHaveLength(1);
});
it("classifies broad commercial-validation unknowns as composite", () => {
const graph = makeCommercialValidationGraph();
const unknown = graph.nodes.find(
(node) => node.id === "n-commercial-parent",
);
const result = assessUnknownAtomicity({ node: unknown, graph });
expect(result.atomicity).toBe("composite");
expect(result.decompositionKind).toBe("commercial_validation");
});
it("decomposes a broad commercial-validation unknown instead of merely rewording it", () => {
const graph = makeCommercialValidationGraph();
const result = applyValidatedProposal({
situationGraph: graph,
proposal: makeMeaningfulNoOpProposal(),
});
expect(result.success).toBe(true);
expect(result.decompositionPerformed).toBe(true);
expect(result.selectedUnknownBefore).toBe("n-commercial-parent");
expect(result.selectedUnknownAfter).not.toBe("n-commercial-parent");
expect(result.childNodeIds.length).toBeGreaterThanOrEqual(2);
expect(result.selectedQuestion.nodeId).toBe(result.selectedUnknownAfter);
expect(result.selectedQuestion.question).toBe(
"Who experiences this problem?",
);
expect(result.selectedQuestion.question.match(/\?/g) || []).toHaveLength(1);
expect(result.questionComplexityAccepted).toBe(true);
expect(result.primaryConceptCount).toBe(1);
});
it("selects a foundational child rather than price or budget", () => {
const graph = makeCommercialValidationGraph();
const result = applyValidatedProposal({
situationGraph: graph,
proposal: makeMeaningfulNoOpProposal(),
});
const selectedNode = result.updatedSituationGraph.nodes.find(
(node) => node.id === result.selectedUnknownAfter,
);
expect(selectedNode.label).toBe("Who experiences this problem");
expect(selectedNode.label.toLowerCase()).not.toMatch(/pay|price|budget/);
expect(result.selectedQuestion.question.toLowerCase()).not.toMatch(
/pay|price|budget/,
);
});
it("plain-language replacements simplify formal phrasing when safe", () => {
const graph = makeCommercialValidationGraph();
const unknown = makeNode({
id: "n-actor-formal",
label: "Relevant customer or user",
description:
"Need to identify the relevant customer, user, or value recipient because the decision depends on it.",
kind: "unknown",
status: "unknown",
confidence: "medium",
});
const result = formulateQuestion({ node: unknown, graph });
const complexity = assessQuestionComplexity({
question:
"What evidence would clarify the relevant customer, user, or value recipient?",
selectedUnknown: unknown,
graph,
});
expect(complexity.acceptable).toBe(false);
expect(complexity.reasons).toContain("list_like_question");
});
it("does not remove scenario-relevant jargon blindly", () => {
const graph = makeGraph({
centralStatement:
"The team is deciding whether to continue a decision-support product.",
nodes: [
makeNode({
id: "n-jargon",
label: "Decision support methods",
description:
"Need evidence about decision support methods because the comparison depends on it.",
kind: "unknown",
status: "unknown",
confidence: "medium",
}),
],
edges: [],
activeUnknownNodeId: "n-jargon",
resolvedNodeIds: [],
currentSummary: "Jargon retention fixture",
});
const result = formulateQuestion({ node: graph.nodes[0], graph });
expect(result.question.toLowerCase()).toContain("decision support methods");
});
});