From f955b875aff6c690d28793427434f890bed90e31 Mon Sep 17 00:00:00 2001 From: robbond Date: Thu, 13 Aug 2026 17:45:28 +0100 Subject: [PATCH] fix(reasoning): clean proposition question formulation --- lib/graph/question-formulator.js | 59 ++++++++++++---- tests/graph/question-formulator.test.js | 94 +++++++++++++++++++++++++ 2 files changed, 138 insertions(+), 15 deletions(-) diff --git a/lib/graph/question-formulator.js b/lib/graph/question-formulator.js index 7189590..355751d 100644 --- a/lib/graph/question-formulator.js +++ b/lib/graph/question-formulator.js @@ -84,10 +84,15 @@ function hasAudienceIdentityQuestion(text) { function extractMeaning(node) { const raw = `${node?.label || ""} ${node?.description || ""}`.trim(); + const rawLabel = stripTrailingPunctuation(String(node?.label || "")).trim(); let meaning = stripTrailingPunctuation( String(node?.label || node?.description || "this uncertainty"), ).trim(); + if (isDirectInterrogativeMeaning(rawLabel)) { + return rawLabel; + } + const lowered = normaliseText(raw); if (hasAudienceIdentityQuestion(lowered)) { return "the relevant customer, user, or value recipient"; @@ -99,13 +104,23 @@ function extractMeaning(node) { .replace(/^uncertainty about\s+/i, "") .trim(); + const extractWhetherProposition = (text) => { + const match = String(text || "") + .trim() + .match( + /^(whether\b[\s\S]*?)(?:;\s+|,\s*(?:so\s+that|because)\b|\s+matters\s+because\b|$)/i, + ); + + return match?.[1]?.trim() || String(text || "").trim(); + }; + if ( /\b(status|likelihood|probability|chance|risk|uncertainty)\b/i.test( String(node?.label || ""), ) && /^whether\s+/i.test(strippedDescription) ) { - return sentenceCase(strippedDescription); + return sentenceCase(extractWhetherProposition(strippedDescription)); } meaning = meaning @@ -185,12 +200,13 @@ function isInterrogativeMeaning(meaning) { return true; } - // "whether" clauses — also already question-shaped - if (/^whether\b/i.test(trimmed)) return true; - return false; } +function isWhetherPropositionMeaning(meaning) { + return /^whether\b/i.test(String(meaning || "").trim()); +} + function wrapInterrogativeForTemplate(meaning) { const stripped = stripTrailingPunctuation(meaning).trim(); @@ -209,7 +225,7 @@ function wrapInterrogativeForTemplate(meaning) { function buildNeutralClarificationQuestion(meaning) { const content = wrapInterrogativeForTemplate(meaning); // If content is already interrogative (wh-), use it as-is with trailing context - if (isInterrogativeMeaning(content)) { + if (isDirectInterrogativeMeaning(content)) { return `${content}?`; } return `What would clarify ${content} in this situation?`; @@ -217,12 +233,18 @@ function buildNeutralClarificationQuestion(meaning) { function buildEvidenceFallbackQuestion(meaning) { const content = wrapInterrogativeForTemplate(meaning); - if (isInterrogativeMeaning(content)) { + if (isDirectInterrogativeMeaning(content)) { return `${content}?`; } return `What evidence would confirm or rule out ${content}?`; } +function isDirectInterrogativeMeaning(meaning) { + return ( + isInterrogativeMeaning(meaning) && !isWhetherPropositionMeaning(meaning) + ); +} + function extractConstraintClarificationSubject(node) { const label = stripTrailingPunctuation(node?.label || ""); const description = String(node?.description || ""); @@ -1269,7 +1291,7 @@ function buildQuestionFromFamily({ ); } // If meaning is already interrogative, use it directly instead of wrapping - if (isInterrogativeMeaning(meaning)) { + if (isDirectInterrogativeMeaning(meaning)) { return `${wrapInterrogativeForTemplate(meaning)}?`; } return `What evidence would clarify ${stripTrailingPunctuation(meaning)}?`; @@ -1277,7 +1299,7 @@ function buildQuestionFromFamily({ if (questionFamily === "definition") { // If meaning is already interrogative, use it directly - if (isInterrogativeMeaning(meaning)) { + if (isDirectInterrogativeMeaning(meaning)) { return `${wrapInterrogativeForTemplate(meaning)}?`; } return `What does ${meaning} mean in this situation?`; @@ -1286,20 +1308,20 @@ function buildQuestionFromFamily({ if (reasoningPattern === "comparison") { if (selectedQuestionTemplate === "comparison_timing_basis") { const cmpContent = wrapInterrogativeForTemplate(meaning); - if (isInterrogativeMeaning(cmpContent)) { + if (isDirectInterrogativeMeaning(cmpContent)) { return `${cmpContent}?`; } return `What evidence would clarify whether ${stripTrailingPunctuation(cmpContent)}?`; } if (selectedQuestionTemplate === "comparison_measurement_basis") { const cmpContent = wrapInterrogativeForTemplate(meaning); - if (isInterrogativeMeaning(cmpContent)) { + if (isDirectInterrogativeMeaning(cmpContent)) { return `${cmpContent}?`; } return `What evidence would clarify ${stripTrailingPunctuation(cmpContent)}?`; } // Default comparison — handle interrogative meaning - if (isInterrogativeMeaning(meaning)) { + if (isDirectInterrogativeMeaning(meaning)) { return `${wrapInterrogativeForTemplate(meaning)}?`; } return `What evidence would clarify ${stripTrailingPunctuation(meaning)}?`; @@ -1310,7 +1332,7 @@ function buildQuestionFromFamily({ return buildQuestionFromStrategy(investigationStrategy); } // If meaning is interrogative, use it directly - if (isInterrogativeMeaning(meaning)) { + if (isDirectInterrogativeMeaning(meaning)) { return `${wrapInterrogativeForTemplate(meaning)}?`; } return `What fact would resolve the contradiction about ${stripTrailingPunctuation(meaning)}?`; @@ -1328,7 +1350,10 @@ function buildQuestionFromFamily({ if (investigationStrategy) { return buildQuestionFromStrategy(investigationStrategy); } - if (isInterrogativeMeaning(meaning)) { + if (isWhetherPropositionMeaning(meaning)) { + return `What evidence would clarify ${stripTrailingPunctuation(meaning)}?`; + } + if (isDirectInterrogativeMeaning(meaning)) { return `${wrapInterrogativeForTemplate(meaning)}?`; } return buildNeutralClarificationQuestion(meaning); @@ -1649,7 +1674,11 @@ export function selectInvestigationStrategy({ node, graph, context = {} }) { }); } - if (!selectedStrategy && (hasDecisionValueLanguage || hasCriteriaLanguage)) { + if ( + !selectedStrategy && + (hasCriteriaLanguage || + (hasDecisionValueLanguage && !isWhetherPropositionMeaning(meaning))) + ) { selectedStrategy = buildInvestigationStrategy({ key: "decision_threshold", reason: @@ -1718,7 +1747,7 @@ export function selectInvestigationStrategy({ node, graph, context = {} }) { function buildQuestionFromStrategy(strategy) { // If meaning is interrogative, use it directly instead of embedding in a template const m = strategy.meaning; - if (isInterrogativeMeaning(m)) { + if (isDirectInterrogativeMeaning(m)) { return `${wrapInterrogativeForTemplate(m)}?`; } diff --git a/tests/graph/question-formulator.test.js b/tests/graph/question-formulator.test.js index ce4a7c5..8a4d85c 100644 --- a/tests/graph/question-formulator.test.js +++ b/tests/graph/question-formulator.test.js @@ -442,6 +442,100 @@ describe("formulateQuestion", () => { ); }); + it("60B.24 regression strips whether-rationale and uses evidence framing without mutating the source node", () => { + const description = + "Whether one prospective enterprise customer will sign if we launch this year; they account for ~£700k of the £1.2M expected annual revenue, so that resolving their intent is needed to assess the financial impact of launching now."; + const unknown = makeNode({ + id: "n-60b24-signing-status", + label: "Prospective enterprise customer signing status", + description, + kind: "unknown", + status: "unknown", + confidence: "medium", + }); + + const graph = makeGraphFor(unknown, { + centralStatement: + "We need to decide which launch timing option provides superior net value.", + }); + + const result = formulateQuestion({ node: unknown, graph }); + + expect(result.question).toContain( + "whether one prospective enterprise customer will sign if we launch this year", + ); + expect(result.question).toContain("What evidence would clarify"); + expect(result.question).not.toContain("£700k"); + expect(result.question).not.toContain("£1.2M"); + expect(result.question).not.toContain("resolving their intent"); + expect(result.question).not.toContain("financial impact"); + expect(unknown.description).toBe(description); + }); + + it("clean whether proposition uses evidence framing rather than a direct whether-question", () => { + const unknown = makeNode({ + id: "n-supplier-renewal", + label: "Supplier renewal likelihood", + description: "Whether the supplier will renew the contract.", + kind: "unknown", + status: "unknown", + confidence: "medium", + }); + + const result = formulateQuestion({ + node: unknown, + graph: makeGraphFor(unknown), + }); + + expect(result.question).toBe( + "What evidence would clarify whether the supplier will renew the contract?", + ); + expect(result.question).not.toBe( + "Whether the supplier will renew the contract?", + ); + }); + + it("wh-question remains unchanged", () => { + const unknown = makeNode({ + id: "n-wh-question", + label: "What would change the preferred option?", + description: "What would change the preferred option?", + kind: "unknown", + status: "unknown", + confidence: "medium", + }); + + const result = formulateQuestion({ + node: unknown, + graph: makeGraphFor(unknown), + }); + + expect(result.question).toBe("What would change the preferred option?"); + }); + + it("non-whether semicolon content is not globally truncated", () => { + const unknown = makeNode({ + id: "n-semicolon-baseline", + label: + "Unknown baseline measurement basis; current and previous figures were prepared differently.", + description: + "Baseline measurement basis; current and previous figures were prepared differently.", + kind: "unknown", + status: "unknown", + confidence: "medium", + }); + + const result = formulateQuestion({ + node: unknown, + graph: makeGraphFor(unknown), + }); + + expect(result.question).toContain( + "baseline measurement basis; current and previous figures were prepared differently", + ); + expect(result.question).not.toContain("What would clarify whether"); + }); + it("evidence-resolvable competing-cause unknown stays on an evidence route rather than neutral clarification", () => { const unknown = makeNode({ id: "n-delivery-cause",