function normaliseText(value) { return String(value || "") .toLowerCase() .replace(/[^a-z0-9]+/g, " ") .trim(); } function sentenceCase(value) { const trimmed = String(value || "").trim(); if (!trimmed) return "this uncertainty"; return trimmed.charAt(0).toLowerCase() + trimmed.slice(1); } function stripTrailingPunctuation(value) { return String(value || "") .trim() .replace(/[.?!:;]+$/g, "") .trim(); } function buildNodeMap(graph) { return new Map((graph?.nodes || []).map((node) => [node.id, node])); } function collectRelatedNodes(node, graph) { if (!node || !graph) return []; const nodesById = buildNodeMap(graph); const relatedIds = new Set([ ...(node.dependsOn || []), ...(node.affects || []), ...(node.childIds || []), ]); if (node.parentId) { relatedIds.add(node.parentId); } for (const edge of graph.edges || []) { if (edge.fromNodeId === node.id) { relatedIds.add(edge.toNodeId); } if (edge.toNodeId === node.id) { relatedIds.add(edge.fromNodeId); } } return [...relatedIds].map((nodeId) => nodesById.get(nodeId)).filter(Boolean); } function collectResolvedContextValues(graph) { const resolvedSet = new Set(graph?.resolvedNodeIds || []); return (graph?.nodes || []) .filter((node) => resolvedSet.has(node.id)) .map((node) => node.value) .filter((value) => typeof value === "string" && value.trim().length > 0); } function extractMeaning(node) { const raw = `${node?.label || ""} ${node?.description || ""}`.trim(); let meaning = stripTrailingPunctuation( String(node?.label || node?.description || "this uncertainty"), ).trim(); const lowered = normaliseText(raw); if ( /\b(customer|user|buyer|stakeholder|recipient|audience)\b/.test(lowered) ) { return "the relevant customer, user, or value recipient"; } meaning = meaning .replace(/^uncertainty regarding\s+/i, "") .replace(/^uncertainty about\s+/i, "") .replace(/^lack of\s+/i, "") .replace(/^unknown\s+/i, "") .replace(/^whether\s+/i, "") .replace(/^the\s+/, "") .trim(); if (!meaning) { return "this uncertainty"; } return sentenceCase(meaning); } function isDefinitionLikeUnknown(nodeText, text) { return ( /\b(define|definition|meaning|term|terminology)\b/.test(nodeText) || (/\bdefinition\b/.test(text) && /\bdisagreement\b/.test(text)) || (/\b(define|definition|meaning|term|terminology)\b/.test(text) && /\b(unclear|ambiguous|inconsistent|undefined|used inconsistently)\b/.test( text, )) ); } function isClaimLikeUnknown(node, text) { return ( node?.kind === "reported_claim" || node?.kind === "conclusion" || /\b(claim|assertion|true|false|correct|incorrect|happened|happening)\b/.test( text, ) || /^whether\b/i.test(String(node?.label || "").trim()) ); } function sanitizeQuestionText(question) { return String(question || "") .replace(/\)\.\s+/g, ") ") .replace(/\s+/g, " ") .trim(); } function buildNeutralClarificationQuestion(meaning) { return `What would clarify ${stripTrailingPunctuation(meaning)} in this situation?`; } function buildEvidenceFallbackQuestion(meaning) { return `What evidence would confirm or rule out ${stripTrailingPunctuation(meaning)}?`; } function extractConstraintClarificationSubject(node) { const label = stripTrailingPunctuation(node?.label || ""); const description = String(node?.description || ""); const combined = `${label} ${description}`; const labelMatch = label.match( /^Whether\s+(.+)\s+is\s+a\s+hard constraint$/i, ); if (labelMatch?.[1]) { return labelMatch[1].trim(); } const descriptionMatch = combined.match( /whether\s+(.+?)\s+is\s+a\s+hard constraint\s+or\s+a\s+preference(?:\/|-|\s)trade(?:\/|-|\s)?off/i, ); if (descriptionMatch?.[1]) { return descriptionMatch[1].trim(); } return null; } function buildUserMeaningClarificationQuestion(node) { const subject = extractConstraintClarificationSubject(node); if (subject) { return `Is ${subject} a hard constraint or a preference/trade-off?`; } return buildNeutralClarificationQuestion(extractMeaning(node)); } function isUserOwnedMeaningBoundaryUnknown(node) { const text = normaliseText(`${node?.label || ""} ${node?.description || ""}`); return ( text.includes("hard constraint") && (text.includes("preference trade off") || text.includes("preference/trade-off") || text.includes("preference or trade off") || text.includes("preference or trade-off")) ); } function collectObservationNodes(graph) { return (graph?.nodes || []).filter( (node) => node.kind === "observation" && node.status === "supported", ); } function analyseObservationText(text) { const normalised = normaliseText(text); return { text, normalised, isMeasurementLike: /\b(increase|increased|decrease|decreased|fell|rose|doubled|halved|remained|average|score|scores|rate|time|traffic|sales|output|defects|complaints|production|revenue|cash|temperature|quality)\b/.test( normalised, ) || /%|percent/.test(String(text || "")), timeframeMentioned: /\b(period|timeframe|quarter|month|week|year|day|annual|daily|weekly|monthly|same period)\b/.test( normalised, ), scaleMentioned: /\b(average|rate|score|scores|per|percent|%)\b/.test( normalised, ), unitMentioned: /\b(celsius|fahrenheit|minutes|minute|hours|hour|days|day|units|sales|traffic|cash|revenue|complaints|defects)\b/.test( normalised, ), }; } export const COMPARABILITY_REASONING_NODE_ID = "reasoning:comparability"; function readStoredComparabilityState(graph) { const reasoningState = graph?.reasoningState; if (!reasoningState?.comparabilityStatus) return null; return { comparabilityStatus: reasoningState.comparabilityStatus, reason: reasoningState.comparabilityReason || "Comparability state was carried forward from earlier reasoning.", contradictionReasoningAllowed: reasoningState.comparabilityStatus === "confirmed", }; } export function assessComparability(graph) { const storedState = readStoredComparabilityState(graph); if (storedState) { return storedState; } const observations = collectObservationNodes(graph); const centralText = normaliseText(graph?.centralStatement || ""); const profiles = observations.map((node) => analyseObservationText(`${node.label} ${node.description}`), ); if (profiles.length < 2) { return { comparabilityStatus: "confirmed", reason: "Fewer than two supported observations need comparison.", contradictionReasoningAllowed: true, }; } if ( profiles.every((profile) => profile.normalised === profiles[0].normalised) ) { return { comparabilityStatus: "confirmed", reason: "The observations restate the same measurement.", contradictionReasoningAllowed: false, }; } if (profiles.some((profile) => !profile.isMeasurementLike)) { return { comparabilityStatus: "confirmed", reason: "The observations are not competing like-for-like measurements.", contradictionReasoningAllowed: true, }; } const hasExplicitTimeframe = /\b(period|timeframe|quarter|month|week|year|day|same period)\b/.test( centralText, ) || profiles.every((profile) => profile.timeframeMentioned); const hasSharedScale = profiles.every((profile) => profile.scaleMentioned); const hasSharedUnits = profiles.every((profile) => profile.unitMentioned); if (!hasExplicitTimeframe || !hasSharedScale || !hasSharedUnits) { return { comparabilityStatus: "uncertain", reason: "Comparability between the observations is not yet established across period, scale, or measurement basis.", contradictionReasoningAllowed: false, }; } return { comparabilityStatus: "uncertain", reason: "The observations appear comparable in form, but the basis for comparing them is still not established.", contradictionReasoningAllowed: false, }; } function buildReasoningStages(comparability, relationship, deferred = false) { return [ { stage: "comparability", status: comparability.comparabilityStatus, outcome: comparability.reason, }, { stage: "relationship", status: relationship.relationshipStatus, outcome: deferred ? "not assessed until comparability is established" : relationship.reason, }, ]; } function extractObservationConcepts(profile) { const concepts = new Set(); const text = profile.normalised; const conceptPatterns = [ ["sales", /\bsales\b/], ["revenue", /\brevenue\b/], ["cash", /\bcash\b/], ["complaints", /\bcomplaints?\b/], ["production", /\bproduction\b/], ["delivery_time", /\bdelivery time\b|\baverage delivery time\b/], ["cancellations", /\bcancellations?\b/], ["satisfaction", /\bsatisfaction\b/], ["temperature", /\btemperature\b/], ["ice", /\bice\b/], ["traffic", /\btraffic\b/], ["defects", /\bdefects?\b/], ["quality", /\bquality\b/], ["staffing", /\bstaff(ing)?\b/], ["availability", /\bavailable|availability|unavailable\b/], ["service", /\bservice\b/], ]; for (const [name, pattern] of conceptPatterns) { if (pattern.test(text)) concepts.add(name); } return [...concepts]; } function extractObservationDirection(profile) { const text = profile.normalised; if (/\bunavailable\b/.test(text)) return "unavailable"; if (/\b(increase|increased|rose|up|doubled)\b/.test(text)) return "up"; if (/\b(decrease|decreased|fell|down|halved)\b/.test(text)) return "down"; if (/\b(remained unchanged|unchanged|same)\b/.test(text)) return "flat"; if (/\bavailable\b/.test(text)) return "available"; if (/\bmelted\b/.test(text)) return "melted"; return "unknown"; } function classifyObservationRelationshipWhenComparable(graph) { const observations = collectObservationNodes(graph); const profiles = observations.map((node) => analyseObservationText(`${node.label} ${node.description}`), ); if (profiles.length < 2) { return { relationshipStatus: "insufficient_information", reason: "Fewer than two supported observations are available for comparison.", contradictionReasoningAllowed: false, questionRequired: false, questionSuppressedReason: "Not enough observations to classify a relationship.", }; } if ( profiles.every((profile) => profile.normalised === profiles[0].normalised) ) { return { relationshipStatus: "duplicate", reason: "The observations repeat the same measurement and direction.", contradictionReasoningAllowed: false, questionRequired: false, questionSuppressedReason: "Duplicate observations do not justify a follow-up question.", }; } const conceptSets = profiles.map((profile) => extractObservationConcepts(profile), ); const sharedConcepts = conceptSets.reduce((shared, concepts, index) => { if (index === 0) return new Set(concepts); return new Set(concepts.filter((concept) => shared.has(concept))); }, new Set()); const directions = profiles.map((profile) => extractObservationDirection(profile), ); const conceptUnion = new Set(conceptSets.flat()); const hasRevenueCashPair = conceptUnion.has("revenue") && conceptUnion.has("cash"); if ( sharedConcepts.size > 0 && directions.includes("available") && directions.includes("unavailable") ) { return { relationshipStatus: "contradictory", reason: "The observations assert mutually incompatible states about the same subject.", contradictionReasoningAllowed: true, questionRequired: true, }; } if ( sharedConcepts.size > 0 && directions.every((direction) => direction !== "unknown") ) { return { relationshipStatus: "potentially_related", reason: "The observations concern the same subject but do not assert a direct contradiction.", contradictionReasoningAllowed: false, questionRequired: true, }; } if ( hasRevenueCashPair && directions.every((direction) => direction !== "unknown") ) { return { relationshipStatus: "potentially_related", reason: "The observations concern connected business signals but do not establish a direct contradiction or cause.", contradictionReasoningAllowed: false, questionRequired: true, }; } if ( sharedConcepts.size === 0 && directions.every((direction) => direction !== "unknown") ) { return { relationshipStatus: "compatible", reason: "The observations can coexist without asserting incompatible states about the same subject.", contradictionReasoningAllowed: false, questionRequired: false, questionSuppressedReason: "Compatible observations do not justify a contradiction investigation.", }; } return { relationshipStatus: "insufficient_information", reason: "There is not enough structure to classify the relationship safely.", contradictionReasoningAllowed: false, questionRequired: false, questionSuppressedReason: "No meaningful relationship structure was established; uncertainty is preserved without fabricating an explanation problem.", }; } export function classifyObservationRelationship(graph) { const comparability = assessComparability(graph); if (comparability.comparabilityStatus !== "confirmed") { const deferredRelationship = { relationshipStatus: "insufficient_information", reason: "Relationship classification is deferred until comparability is established.", contradictionReasoningAllowed: false, questionRequired: comparability.comparabilityStatus === "uncertain", questionSuppressedReason: comparability.comparabilityStatus === "incompatible" ? "Relationship classification was not attempted because the observations are not yet comparable." : undefined, relationshipAssessed: false, }; return { ...deferredRelationship, reasoningStages: buildReasoningStages( comparability, deferredRelationship, true, ), }; } const classified = classifyObservationRelationshipWhenComparable(graph); return { ...classified, relationshipAssessed: true, reasoningStages: buildReasoningStages(comparability, classified, false), }; } export function buildReasoningState(graph, overrides = {}) { const relationship = classifyObservationRelationship({ ...graph, reasoningState: { ...(graph?.reasoningState || {}), ...(overrides || {}), }, }); return { comparabilityStatus: relationship.reasoningStages[0]?.status ?? null, comparabilityReason: relationship.reasoningStages[0]?.outcome ?? null, comparabilityEvidence: overrides.comparabilityEvidence ?? graph?.reasoningState?.comparabilityEvidence ?? [], relationshipStatus: relationship.relationshipStatus, relationshipReason: relationship.reason, relationshipAssessed: relationship.relationshipAssessed, contradictionReasoningAllowed: relationship.contradictionReasoningAllowed, reasoningStages: relationship.reasoningStages, }; } function buildComparabilityQuestion(graph, assessment) { const centralText = normaliseText(graph?.centralStatement || ""); const mentionsPeriod = /\b(period|timeframe|quarter|month|week|year|day)\b/.test(centralText); if (mentionsPeriod) { return "Were these figures measured on the same basis and at the same scale?"; } return "Were these figures measured over the same period and at the same scale?"; } function detectContradictionContext(graph) { const central = stripTrailingPunctuation( graph?.centralStatement || "this situation", ); const contradictionNode = (graph?.nodes || []).find((node) => { const text = normaliseText(`${node.label} ${node.description}`); return ( node.kind === "relationship" && /\b(contradiction|conflict|inconsistent|mismatch|divergent|opposing)\b/.test( text, ) ); }); return { centralStatement: central, contradictionLabel: stripTrailingPunctuation( contradictionNode?.label || "", ), }; } function buildBroadInvestigationQuestion(graph) { const central = sanitizeQuestionText( stripTrailingPunctuation(graph?.centralStatement || "these observations"), ); 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 (/^whether other people experience this problem$/i.test(label)) { return "What makes you think other people experience this problem too?"; } 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) { const text = normaliseText(`${node?.label || ""} ${node?.description || ""}`); if (isDirectlyAnswerableObservationChildText(text)) { return false; } return ( collectObservationNodes(graph).length >= 2 && /\b(explain|explanation|divergence|moved differently|difference between|change or event|what changed|why the observations)/.test( text, ) ); } function isBroadCompositeUnknownText(text) { return /\b(possible causes|possible reasons|root causes|causes of|drivers of|factors behind|factors affecting|what changed|explanation for why|why .* but|difference between|divergence|moved differently|broad explanation|independent dimensions)\b/.test( 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) { return ( /\b(timing|measurement|basis|cost|costs|debt|stock|tax|capital spending|mix|segment)\s+(and|or)\s+\b/.test( text, ) || /\b[a-z]+\/[a-z]+\b/.test(text) || /,\s*[a-z]+,\s*[a-z]+/.test(text) ); } function isFocusedAtomicUnknownText(text) { return /\b(define|definition|meaning|term|threshold|criterion|criteria|baseline|evidence|measure|metric|denominator|rate|date|period|budget|constraint|customer|actor|owner)\b/.test( text, ); } function isDirectlyAnswerableObservationChildText(text) { return /\b(whether the two observations reflect different timing|how the two observations were measured|change mainly affecting|one off event during the period|mix shift during the period)\b/.test( text, ); } 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 || ""}`, ); if ( isDirectlyAnswerableObservationChildText(nodeText) && !hasCompoundAbstractSignals(nodeText) ) { return { atomicity: "atomic", reason: "This unknown isolates one specific line of enquiry and can be investigated directly.", decompositionKind: null, }; } if (isRelationshipExplanationUnknown(node, graph)) { return { atomicity: "composite", reason: "This unknown asks for a broad explanation across multiple observations, so it should be decomposed before asking a direct question.", decompositionKind: "relationship_explanation", }; } if (hasCompoundAbstractSignals(nodeText)) { return { atomicity: "composite", reason: "This unknown still bundles multiple abstract uncertainties together, so it should be decomposed before asking it directly.", decompositionKind: "compound_child", }; } if ( isFocusedAtomicUnknownText(nodeText) && !isBroadCompositeUnknownText(nodeText) ) { return { atomicity: "atomic", reason: "This unknown already targets a single concrete detail that can be investigated directly.", decompositionKind: null, }; } if (isBroadCompositeUnknownText(nodeText)) { return { atomicity: "composite", reason: "This unknown combines multiple broad candidate explanations, so it should be split into smaller dimensions first.", decompositionKind: "broad_explanation", }; } 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 { atomicity: "atomic", reason: "No deterministic composite pattern was detected, so the unknown can be investigated directly.", decompositionKind: null, }; } const ALL_REASONING_PATTERNS = [ "decision", "explanation", "contradiction", "definition", "diagnosis", "comparison", "prioritisation", ]; const QUESTION_FAMILIES_BY_PATTERN = { decision: [ "decision_foundation", "decision_evidence", "decision_threshold", "definition", ], explanation: ["explanation", "comparison"], contradiction: ["contradiction", "comparison", "explanation"], definition: ["definition"], diagnosis: ["diagnosis", "comparison"], comparison: ["comparison"], prioritisation: ["prioritisation", "decision_threshold"], }; const STRATEGIES_BY_PATTERN = { decision: [ "decision_threshold", "evidence_gathering", "definition", "baseline_reconstruction", ], explanation: ["evidence_gathering", "baseline_reconstruction"], contradiction: [ "contradiction_resolution", "baseline_reconstruction", "evidence_gathering", ], definition: ["definition"], diagnosis: ["evidence_gathering", "baseline_reconstruction"], comparison: ["baseline_reconstruction", "evidence_gathering"], prioritisation: ["decision_threshold", "evidence_gathering"], }; function buildParentChain(node, graph) { const nodesById = buildNodeMap(graph); const chain = []; let current = node?.parentId ? nodesById.get(node.parentId) : null; while (current) { chain.push(current); current = current.parentId ? nodesById.get(current.parentId) : null; } return chain; } function hasDecisionContext(node, graph, relatedNodes = []) { const ancestry = buildParentChain(node, graph); const contextText = normaliseText( [ graph?.centralStatement, node?.label, node?.description, ...relatedNodes.map((relatedNode) => relatedNode.label), ...relatedNodes.map((relatedNode) => relatedNode.description), ...ancestry.map((ancestor) => ancestor.label), ...ancestry.map((ancestor) => ancestor.description), ...collectResolvedContextValues(graph), ] .filter(Boolean) .join(" "), ); return /\b(whether to|build|launch|continue|proceed|invest|commercially justified|commercial justification|commercial value|business case|viability)\b/.test( contextText, ); } function hasObservationRelationshipTopology(node, graph, relatedNodes = []) { const ancestry = buildParentChain(node, graph); const topologyNodes = [node, ...relatedNodes, ...ancestry].filter(Boolean); return ( collectObservationNodes(graph).length >= 2 && topologyNodes.some( (candidate) => candidate.kind === "observation" || candidate.kind === "relationship", ) ); } function isDefinitionPatternCandidate(node, graph) { const text = normaliseText(`${node?.label || ""} ${node?.description || ""}`); return isDefinitionLikeUnknown( text, `${text} ${graph?.centralStatement || ""}`, ); } function isContradictionPatternCandidate(node, graph, relatedNodes = []) { const relationshipStatus = graph?.reasoningState?.relationshipStatus ?? null; const text = normaliseText( [ node?.label, node?.description, ...relatedNodes.map((relatedNode) => relatedNode.label), ...relatedNodes.map((relatedNode) => relatedNode.description), ] .filter(Boolean) .join(" "), ); return ( relationshipStatus === "contradictory" || /\b(contradiction|contradict|conflict|inconsistent|mismatch|opposing)\b/.test( text, ) ); } function isComparisonPatternCandidate(node, graph, relatedNodes = []) { if (isRelationshipExplanationUnknown(node, graph)) { return false; } const text = normaliseText( [ node?.label, node?.description, ...relatedNodes.map((relatedNode) => relatedNode.label), ...relatedNodes.map((relatedNode) => relatedNode.description), ] .filter(Boolean) .join(" "), ); return ( (hasObservationRelationshipTopology(node, graph, relatedNodes) || collectObservationNodes(graph).length >= 2 || graph?.reasoningState?.comparabilityStatus === "uncertain") && /\b(compare|comparison|different timing|measured|measurement|basis|scale|period|alternative|alternatives|better than)\b/.test( text, ) ); } function isExplanationPatternCandidate(node, graph, relatedNodes = []) { if (isRelationshipExplanationUnknown(node, graph)) return true; const text = normaliseText( [ node?.label, node?.description, ...relatedNodes.map((relatedNode) => relatedNode.label), ...relatedNodes.map((relatedNode) => relatedNode.description), ] .filter(Boolean) .join(" "), ); return ( hasObservationRelationshipTopology(node, graph, relatedNodes) && /\b(explain|explanation|why .* but|difference between|divergence|what changed|moved differently)\b/.test( text, ) ); } function isPrioritisationPatternCandidate(node, graph, relatedNodes = []) { const text = normaliseText( [ node?.label, node?.description, graph?.centralStatement, ...relatedNodes.map((relatedNode) => relatedNode.label), ] .filter(Boolean) .join(" "), ); return /\b(prioritise|prioritize|priority|rank|ranking|trade off|tradeoff|which first)\b/.test( text, ); } export function selectReasoningPattern({ node, graph, context = {} }) { const relatedNodes = collectRelatedNodes(node, graph); const patternContext = { hasDecisionContext: hasDecisionContext(node, graph, relatedNodes), hasObservationRelationshipTopology: hasObservationRelationshipTopology( node, graph, relatedNodes, ), }; if (isDefinitionPatternCandidate(node, graph)) { return { pattern: "definition", reason: "Selected definition because the active unknown is about meaning, scope, or term boundaries.", context: patternContext, }; } if (isContradictionPatternCandidate(node, graph, relatedNodes)) { return { pattern: "contradiction", reason: "Selected contradiction because the graph indicates opposing claims or incompatible observations.", context: patternContext, }; } if (isComparisonPatternCandidate(node, graph, relatedNodes)) { return { pattern: "comparison", reason: "Selected comparison because the active unknown is about distinguishing measurements, timing, basis, or alternatives.", context: patternContext, }; } if (isExplanationPatternCandidate(node, graph, relatedNodes)) { return { pattern: "explanation", reason: "Selected explanation because the active unknown is about accounting for a relationship between observations.", context: patternContext, }; } if (patternContext.hasDecisionContext) { return { pattern: "decision", reason: "Selected decision because the active unknown sits inside a build, continue, invest, or commercial-justification decision context.", context: patternContext, }; } if (isPrioritisationPatternCandidate(node, graph, relatedNodes)) { return { pattern: "prioritisation", reason: "Selected prioritisation because the active unknown is about ordering options or trade-offs.", context: patternContext, }; } return { pattern: "diagnosis", reason: "Selected diagnosis as the default because the active unknown needs clarifying evidence or mechanism-level investigation.", context: patternContext, }; } function allowedQuestionFamiliesForPattern(pattern) { return QUESTION_FAMILIES_BY_PATTERN[pattern] || [pattern]; } function rejectedQuestionFamiliesForPattern(pattern) { const allowed = new Set(allowedQuestionFamiliesForPattern(pattern)); return ALL_REASONING_PATTERNS.flatMap((candidatePattern) => ( QUESTION_FAMILIES_BY_PATTERN[candidatePattern] || [candidatePattern] ).filter((family) => !allowed.has(family)), ).filter((family, index, list) => list.indexOf(family) === index); } function constrainStrategyToReasoningPattern(strategy, reasoningPattern) { if (!strategy) return null; const allowedStrategies = STRATEGIES_BY_PATTERN[reasoningPattern] || []; return allowedStrategies.includes(strategy.key) ? strategy : null; } function selectQuestionFamily({ node, graph, reasoningPattern, investigationStrategy, }) { const text = normaliseText(`${node?.label || ""} ${node?.description || ""}`); if (reasoningPattern === "definition") { return { family: "definition", template: "definition_meaning" }; } if (reasoningPattern === "contradiction") { if (/\b(period|timing|basis|scale|measure|measured)\b/.test(text)) { return { family: "comparison", template: "comparison_reconcile_measurement", }; } return { family: "contradiction", template: "contradiction_resolve_opposition", }; } if (reasoningPattern === "explanation") { return { family: "explanation", template: "explanation_broad_investigation", }; } if (reasoningPattern === "comparison") { return /\b(period|timing)\b/.test(text) && !/\bhow the two observations were measured|measurement\b/.test(text) ? { family: "comparison", template: "comparison_timing_basis" } : { family: "comparison", template: "comparison_measurement_basis" }; } if (reasoningPattern === "decision") { if ( /\b(audience|customer|user|buyer|stakeholder|recipient|who experiences)\b/.test( text, ) ) { return { family: "decision_foundation", template: "decision_audience" }; } if ( /\b(alternative|alternatives|better than|different from|deal with)\b/.test( text, ) ) { return { family: "decision_foundation", template: "decision_current_alternatives", }; } if (/\b(problem|need|demand)\b/.test(text)) { return { family: "decision_foundation", template: "decision_problem_existence", }; } if (investigationStrategy?.key === "decision_threshold") { return { family: "decision_threshold", template: "decision_threshold_outcome", }; } return { family: "decision_evidence", template: "decision_evidence_clarification", }; } if (reasoningPattern === "prioritisation") { return { family: "prioritisation", template: "prioritisation_tradeoff" }; } return investigationStrategy?.key === "baseline_reconstruction" ? { family: "comparison", template: "diagnosis_baseline_comparison" } : { family: "diagnosis", template: "diagnosis_evidence" }; } function buildQuestionFromFamily({ node, graph, reasoningPattern, questionFamily, selectedQuestionTemplate, investigationStrategy, }) { const meaning = extractMeaning(node); if (reasoningPattern === "decision") { if (selectedQuestionTemplate === "decision_audience") { return "Who experiences this problem?"; } if (selectedQuestionTemplate === "decision_current_alternatives") { return "How do people deal with this today?"; } if (selectedQuestionTemplate === "decision_problem_existence") { return "What makes you think this is a real problem?"; } if (selectedQuestionTemplate === "decision_threshold_outcome") { return buildQuestionFromStrategy( investigationStrategy || { key: "decision_threshold", meaning, actionPhrase: null, }, ); } return `What evidence would clarify ${stripTrailingPunctuation(meaning)}?`; } if (questionFamily === "definition") { return `What does ${meaning} mean in this situation?`; } if (reasoningPattern === "comparison") { if (selectedQuestionTemplate === "comparison_timing_basis") { return `What evidence would clarify whether ${stripTrailingPunctuation(meaning)}?`; } if (selectedQuestionTemplate === "comparison_measurement_basis") { return `What evidence would clarify ${stripTrailingPunctuation(meaning)}?`; } return `What evidence would clarify ${stripTrailingPunctuation(meaning)}?`; } if (reasoningPattern === "contradiction") { return investigationStrategy?.key === "contradiction_resolution" ? buildQuestionFromStrategy(investigationStrategy) : `What fact would resolve the contradiction about ${stripTrailingPunctuation(meaning)}?`; } if (reasoningPattern === "explanation") { return buildBroadInvestigationQuestion(graph); } if (reasoningPattern === "prioritisation") { return `Which option should be investigated first, and why?`; } return investigationStrategy ? buildQuestionFromStrategy(investigationStrategy) : buildNeutralClarificationQuestion(meaning); } export function formulateTieResolutionQuestion({ graph }) { const comparability = assessComparability(graph); if (comparability.comparabilityStatus === "uncertain") { const deferredRelationship = classifyObservationRelationship(graph); return { question: buildComparabilityQuestion(graph, comparability), reason: "Formulated to confirm whether the observations are comparable before exploring competing explanations.", strategy: null, investigationStrategy: null, selectionStatus: "ambiguous", comparabilityStatus: comparability.comparabilityStatus, comparabilityReason: comparability.reason, contradictionReasoningAllowed: comparability.contradictionReasoningAllowed, relationshipStatus: deferredRelationship.relationshipStatus, relationshipReason: deferredRelationship.reason, relationshipAssessed: deferredRelationship.relationshipAssessed, reasoningPattern: "comparison", reasoningPatternReason: "Tie resolution is using the comparison family because comparability is still unresolved.", allowedQuestionFamilies: allowedQuestionFamiliesForPattern("comparison"), rejectedQuestionFamilies: rejectedQuestionFamiliesForPattern("comparison"), questionFamily: "comparison", selectedQuestionTemplate: "comparison_tie_resolution", questionRequired: true, reasoningStages: deferredRelationship.reasoningStages, }; } const relationship = classifyObservationRelationship(graph); if (!relationship.questionRequired) { return { question: null, reason: relationship.reason, strategy: null, investigationStrategy: null, selectionStatus: "ambiguous", comparabilityStatus: comparability.comparabilityStatus, comparabilityReason: comparability.reason, relationshipStatus: relationship.relationshipStatus, relationshipReason: relationship.reason, relationshipAssessed: relationship.relationshipAssessed, contradictionReasoningAllowed: relationship.contradictionReasoningAllowed, reasoningPattern: "comparison", reasoningPatternReason: "Tie resolution remains in the comparison family because no distinct winning unknown exists.", allowedQuestionFamilies: allowedQuestionFamiliesForPattern("comparison"), rejectedQuestionFamilies: rejectedQuestionFamiliesForPattern("comparison"), questionFamily: "comparison", selectedQuestionTemplate: "comparison_no_question_required", questionRequired: relationship.questionRequired, questionSuppressedReason: relationship.questionSuppressedReason, reasoningStages: relationship.reasoningStages, }; } if (relationship.relationshipStatus === "potentially_related") { return { question: buildBroadInvestigationQuestion(graph), reason: "Formulated as a neutral relationship question because the observations may be related without being contradictory.", strategy: null, investigationStrategy: null, selectionStatus: "ambiguous", comparabilityStatus: comparability.comparabilityStatus, comparabilityReason: comparability.reason, relationshipStatus: relationship.relationshipStatus, relationshipReason: relationship.reason, relationshipAssessed: relationship.relationshipAssessed, contradictionReasoningAllowed: relationship.contradictionReasoningAllowed, reasoningPattern: "explanation", reasoningPatternReason: "Tie resolution is using the explanation family because the observations appear related and need a neutral explanation question.", allowedQuestionFamilies: allowedQuestionFamiliesForPattern("explanation"), rejectedQuestionFamilies: rejectedQuestionFamiliesForPattern("explanation"), questionFamily: "explanation", selectedQuestionTemplate: "explanation_broad_investigation", questionRequired: relationship.questionRequired, reasoningStages: relationship.reasoningStages, }; } const { centralStatement, contradictionLabel } = detectContradictionContext(graph); const focus = centralStatement || contradictionLabel || "these conflicting signals"; const question = sanitizeQuestionText( `What changed during the period that could explain why ${focus}?`, ); return { question, reason: "Formulated to distinguish between tied unresolved explanations without prematurely choosing one branch.", strategy: null, investigationStrategy: null, selectionStatus: "ambiguous", comparabilityStatus: comparability.comparabilityStatus, comparabilityReason: comparability.reason, relationshipStatus: relationship.relationshipStatus, relationshipReason: relationship.reason, relationshipAssessed: relationship.relationshipAssessed, contradictionReasoningAllowed: relationship.contradictionReasoningAllowed, reasoningPattern: "contradiction", reasoningPatternReason: "Tie resolution is using the contradiction family because the graph is trying to distinguish incompatible explanations.", allowedQuestionFamilies: allowedQuestionFamiliesForPattern("contradiction"), rejectedQuestionFamilies: rejectedQuestionFamiliesForPattern("contradiction"), questionFamily: "contradiction", selectedQuestionTemplate: "contradiction_distinguishing_change", questionRequired: relationship.questionRequired, reasoningStages: relationship.reasoningStages, }; } function extractActionPhrase(texts) { for (const text of texts) { const value = String(text || "").trim(); if (!value) continue; const matches = [ value.match(/\b(?:whether|deciding|decision) to\s+([^.,;:]+)/i), value.match(/\b(?:justify|continuing|proceeding with)\s+([^.,;:]+)/i), value.match( /\b(build|launch|adopt|buy|continue|proceed|invest in|fund)\s+([^.,;:]+)/i, ), ].filter(Boolean); const match = matches[0]; if (!match) continue; const phrase = (match[1] || `${match[1] || ""} ${match[2] || ""}`) .replace(/^to\s+/i, "") .trim(); if (phrase) { return phrase; } } return null; } function toGerundPhrase(phrase) { const trimmed = String(phrase || "").trim(); if (!trimmed) return "proceeding with this decision"; const [firstWord, ...rest] = trimmed.split(/\s+/); const lower = firstWord.toLowerCase(); const irregular = { be: "being", build: "building", continue: "continuing", decide: "deciding", proceed: "proceeding", launch: "launching", invest: "investing", fund: "funding", buy: "buying", pay: "paying", adopt: "adopting", }; let gerund = irregular[lower]; if (!gerund) { if (lower.endsWith("e") && !lower.endsWith("ee")) { gerund = `${lower.slice(0, -1)}ing`; } else { gerund = `${lower}ing`; } } return [gerund, ...rest].join(" "); } function buildInvestigationStrategy({ key, reason, node, graph, relatedNodes, meaning, actionPhrase, }) { return { key, reason, nodeId: node?.id ?? null, nodeLabel: node?.label ?? null, meaning, actionPhrase, relatedNodeIds: relatedNodes.map((relatedNode) => relatedNode.id), centralStatement: graph?.centralStatement ?? null, }; } export function selectInvestigationStrategy({ node, graph, context = {} }) { const effectiveReasoningPattern = context.reasoningPattern || selectReasoningPattern({ node, graph, context }).pattern; const relatedNodes = collectRelatedNodes(node, graph); const meaning = extractMeaning(node); const combinedText = [ node?.label, node?.description, ...relatedNodes.map((relatedNode) => relatedNode.label), ...relatedNodes.map((relatedNode) => relatedNode.description), graph?.centralStatement, ...(context.resolvedValues || []), ] .filter(Boolean) .join(" "); const text = normaliseText(combinedText); const nodeText = normaliseText( `${node?.label || ""} ${node?.description || ""}`, ); const relatedText = normaliseText( relatedNodes .map((relatedNode) => `${relatedNode.label} ${relatedNode.description}`) .join(" "), ); const resolvedValues = collectResolvedContextValues(graph); const actionPhrase = extractActionPhrase([ ...resolvedValues, ...relatedNodes.map((relatedNode) => relatedNode.value), ...relatedNodes.map((relatedNode) => relatedNode.label), ...relatedNodes.map((relatedNode) => relatedNode.description), graph?.centralStatement, ]); const decisionContext = /\b(decision|whether to|build|launch|continue|proceed|invest|allocate)\b/.test( `${text} ${relatedText} ${resolvedValues.join(" ")}`, ) || Boolean(actionPhrase); const hasConstraintLanguage = /\b(constraint|limit|budget|deadline|requirement|regulation|capacity)\b/.test( text, ); const hasPrimaryConstraintLanguage = /\b(constraint|limit|budget|deadline|requirement|regulation|capacity)\b/.test( nodeText, ); const hasBaselineLanguage = /\b(before|previous|baseline|prior|comparable state)\b/.test(text); const hasPrimaryBaselineLanguage = /\b(before|previous|baseline|prior|comparable state)\b/.test(nodeText); let selectedStrategy = null; if (hasBaselineLanguage && hasPrimaryBaselineLanguage) { selectedStrategy = buildInvestigationStrategy({ key: "baseline_reconstruction", reason: "Selected because the unknown explicitly references a missing previous or baseline state.", node, graph, relatedNodes, meaning, actionPhrase, }); } const hasDefinitionLanguage = isDefinitionLikeUnknown(nodeText, text); const hasPrimaryDefinitionLanguage = /\b(define|definition|meaning|term|terminology)\b/.test(nodeText); const hasCriteriaLanguage = /\b(success criteria|success threshold|threshold|decision criteria|criterion|justify|sufficient)\b/.test( nodeText, ); const hasDecisionValueLanguage = decisionContext && /\b(value|commercial value|commercial viability|viability|justify|sufficient|success|threshold|criterion)\b/.test( text, ); const hasMeasurementLanguage = /\b(metric|measure|measurable|roi|revenue projection|benchmark)\b/.test( text, ); const hasEvidenceLanguage = /\b(evidence|proof|validate|validation|signal|demand)\b/.test(text) || isClaimLikeUnknown(node, text); const hasContradictionLanguage = /\b(contradiction|contradict|conflict|inconsistent|inconsistency|disagree|mismatch)\b/.test( `${text} ${relatedText}`, ) || relatedNodes.some( (relatedNode) => relatedNode.status === "contradicted" || relatedNode.kind === "conclusion", ); if ( !selectedStrategy && (hasPrimaryDefinitionLanguage || hasDefinitionLanguage) ) { selectedStrategy = buildInvestigationStrategy({ key: "definition", reason: "Selected because the unknown is primarily about clarifying what a term means in this case.", node, graph, relatedNodes, meaning, actionPhrase, }); } if (!selectedStrategy && (hasDecisionValueLanguage || hasCriteriaLanguage)) { selectedStrategy = buildInvestigationStrategy({ key: "decision_threshold", reason: "Selected because the unknown determines the threshold for making or justifying a decision.", node, graph, relatedNodes, meaning, actionPhrase, }); } if ( !selectedStrategy && (hasPrimaryBaselineLanguage || hasBaselineLanguage) ) { selectedStrategy = buildInvestigationStrategy({ key: "baseline_reconstruction", reason: "Selected because reconstructing the prior state is the most direct way to resolve the unknown.", node, graph, relatedNodes, meaning, actionPhrase, }); } if (!selectedStrategy && hasContradictionLanguage) { selectedStrategy = buildInvestigationStrategy({ key: "contradiction_resolution", reason: "Selected because the graph context indicates conflicting claims or inconsistent states that must be reconciled.", node, graph, relatedNodes, meaning, actionPhrase, }); } if ( !selectedStrategy && (hasEvidenceLanguage || hasMeasurementLanguage || hasConstraintLanguage) ) { selectedStrategy = buildInvestigationStrategy({ key: "evidence_gathering", reason: hasConstraintLanguage && hasPrimaryConstraintLanguage ? "Selected because evidence about the practical limiting factor is needed before the unknown can be resolved." : "Selected because resolving the unknown requires evidence, signals, or measurable confirmation.", node, graph, relatedNodes, meaning, actionPhrase, }); } return constrainStrategyToReasoningPattern( selectedStrategy, effectiveReasoningPattern, ); } function buildQuestionFromStrategy(strategy) { switch (strategy.key) { case "decision_threshold": return strategy.actionPhrase ? `What outcome would demonstrate enough value to justify ${toGerundPhrase(strategy.actionPhrase)}?` : "What outcome would be sufficient to justify this decision?"; case "definition": return `What does ${strategy.meaning} mean in this situation?`; case "evidence_gathering": return `What evidence would clarify ${stripTrailingPunctuation(strategy.meaning)}?`; case "baseline_reconstruction": return `What was the comparable state before ${strategy.meaning}?`; case "contradiction_resolution": return `What fact would resolve the contradiction about ${strategy.meaning}?`; default: return `What specific fact would resolve whether ${strategy.meaning} is true?`; } } function isCompoundQuestion(question) { const trimmed = String(question || "").trim(); const questionMarks = (trimmed.match(/\?/g) || []).length; if (questionMarks !== 1) return true; if (/\?\s*(and|or)\b/i.test(trimmed)) return true; if (/\b(and|or)\b[^?]{0,80}\?/i.test(trimmed) && /,/.test(trimmed)) { return true; } return false; } function validateFormulatedQuestion(question, meaning) { const trimmed = String(question || "").trim(); const lower = trimmed.toLowerCase(); const meaningWords = normaliseText(meaning) .split(" ") .filter((word) => word.length > 3); const overlappingWord = meaningWords.find((word) => lower.includes(word)); if (!trimmed) return false; if ((trimmed.match(/\?/g) || []).length !== 1) return false; if (isCompoundQuestion(trimmed)) return false; if (/^what is\s+/i.test(trimmed)) return false; if (/^how should uncertainty regarding\b/i.test(trimmed)) return false; if (/^what would resolve uncertainty regarding\b/i.test(trimmed)) return false; if (/\)\.\s+[A-Z]/.test(trimmed)) return false; if (/\bis true\?$/i.test(trimmed) && !/^whether\b/i.test(meaning)) return false; if ( /\bprice|pricing|price point\b/i.test(trimmed) && !/\bprice\b/i.test(meaning) ) { return false; } if ( !overlappingWord && !/\b(decision|evidence|constraint|customer|value|outcome)\b/i.test(trimmed) ) { return false; } 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 = {} }) { if (context.selectionState?.status === "ambiguous") { return formulateTieResolutionQuestion({ graph }); } const reasoningPatternSelection = selectReasoningPattern({ node, graph, context, }); const allowedQuestionFamilies = allowedQuestionFamiliesForPattern( reasoningPatternSelection.pattern, ); const rejectedQuestionFamilies = rejectedQuestionFamiliesForPattern( reasoningPatternSelection.pattern, ); if (isUserOwnedMeaningBoundaryUnknown(node)) { const question = sanitizeQuestionText( buildUserMeaningClarificationQuestion(node), ); const questionComplexity = assessQuestionComplexity({ question, selectedUnknown: node, graph, }); return { question, reason: "Formulated as a user-clarification question because this unresolved distinction depends on the user's own meaning rather than external evidence.", strategy: null, investigationStrategy: null, reasoningPattern: reasoningPatternSelection.pattern, reasoningPatternReason: reasoningPatternSelection.reason, questionFamily: "prioritisation", allowedQuestionFamilies, rejectedQuestionFamilies, selectedQuestionTemplate: "user_meaning_clarification", questionComplexity, plainLanguageNormalisations: [], }; } const foundationalDirectQuestion = buildFoundationalDirectQuestion(node); if ( foundationalDirectQuestion && reasoningPatternSelection.pattern === "decision" ) { 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, reasoningPattern: reasoningPatternSelection.pattern, reasoningPatternReason: reasoningPatternSelection.reason, questionFamily: "decision_foundation", allowedQuestionFamilies, rejectedQuestionFamilies, selectedQuestionTemplate: "decision_foundation_direct_child", questionComplexity, plainLanguageNormalisations: plainLanguage.normalisations, }; } const investigationStrategy = selectInvestigationStrategy({ node, graph, context: { ...context, reasoningPattern: reasoningPatternSelection.pattern, }, }); const questionFamilySelection = selectQuestionFamily({ node, graph, reasoningPattern: reasoningPatternSelection.pattern, investigationStrategy, }); let question = buildQuestionFromFamily({ node, graph, reasoningPattern: reasoningPatternSelection.pattern, questionFamily: questionFamilySelection.family, selectedQuestionTemplate: questionFamilySelection.template, investigationStrategy, }); question = sanitizeQuestionText(question); const plainLanguage = applyPlainLanguageNormalisations(question); question = plainLanguage.question; const fallbackMeaning = extractMeaning(node); if ( !validateFormulatedQuestion( question, investigationStrategy?.meaning || fallbackMeaning, ) ) { question = sanitizeQuestionText( (investigationStrategy && isClaimLikeUnknown( node, normaliseText( collectRelatedNodes(node, graph) .map( (relatedNode) => `${relatedNode.label} ${relatedNode.description}`, ) .concat([node?.label, node?.description]) .filter(Boolean) .join(" "), ), )) || reasoningPatternSelection.pattern === "diagnosis" ? buildEvidenceFallbackQuestion(fallbackMeaning) : buildNeutralClarificationQuestion(fallbackMeaning), ); } const questionComplexity = assessQuestionComplexity({ question, selectedUnknown: node, graph, }); return { question, reason: investigationStrategy ? `Formulated from graph context using the ${investigationStrategy.key} investigation strategy.` : "Formulated as a neutral clarification question because no narrower investigation strategy clearly applied.", strategy: investigationStrategy?.key ?? null, investigationStrategy, reasoningPattern: reasoningPatternSelection.pattern, reasoningPatternReason: reasoningPatternSelection.reason, questionFamily: questionFamilySelection.family, allowedQuestionFamilies, rejectedQuestionFamilies, selectedQuestionTemplate: questionFamilySelection.template, questionComplexity, plainLanguageNormalisations: plainLanguage.normalisations, }; }