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 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: true, }; } 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 isRelationshipExplanationUnknown(node, graph) { const text = normaliseText(`${node?.label || ""} ${node?.description || ""}`); 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 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, ); } export function assessUnknownAtomicity({ node, graph }) { const nodeText = normaliseText( `${node?.label || ""} ${node?.description || ""}`, ); 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 ( 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", }; } return { atomicity: "atomic", reason: "No deterministic composite pattern was detected, so the unknown can be investigated directly.", decompositionKind: null, }; } 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, 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, 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, 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, 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 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); if (hasBaselineLanguage && hasPrimaryBaselineLanguage) { return 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 (hasPrimaryDefinitionLanguage || hasDefinitionLanguage) { return 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 (hasDecisionValueLanguage || hasCriteriaLanguage) { return buildInvestigationStrategy({ key: "decision_threshold", reason: "Selected because the unknown determines the threshold for making or justifying a decision.", node, graph, relatedNodes, meaning, actionPhrase, }); } if (hasPrimaryBaselineLanguage || hasBaselineLanguage) { return 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 (hasContradictionLanguage) { return 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 (hasEvidenceLanguage || hasMeasurementLanguage || hasConstraintLanguage) { return 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 null; } 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; } export function formulateQuestion({ node, graph, context = {} }) { if (context.selectionState?.status === "ambiguous") { return formulateTieResolutionQuestion({ graph }); } const investigationStrategy = selectInvestigationStrategy({ node, graph, context, }); let question = investigationStrategy ? buildQuestionFromStrategy(investigationStrategy) : isRelationshipExplanationUnknown(node, graph) ? buildBroadInvestigationQuestion(graph) : buildNeutralClarificationQuestion(extractMeaning(node)); question = sanitizeQuestionText(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(" "), ), ) ? buildEvidenceFallbackQuestion(fallbackMeaning) : buildNeutralClarificationQuestion(fallbackMeaning), ); } 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, }; }