From f861e2cac0edff673d7c1a0caafd3ba940c89bc2 Mon Sep 17 00:00:00 2001 From: robbond Date: Sun, 9 Aug 2026 16:20:06 +0100 Subject: [PATCH] reasoning: preserve evidence versus clarification distinction --- lib/graph/question-formulator.js | 76 ++++++++++++++++++++++++- tests/graph/question-formulator.test.js | 33 +++++++++-- 2 files changed, 103 insertions(+), 6 deletions(-) diff --git a/lib/graph/question-formulator.js b/lib/graph/question-formulator.js index e7b2db3..5a2bd7a 100644 --- a/lib/graph/question-formulator.js +++ b/lib/graph/question-formulator.js @@ -123,6 +123,49 @@ 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", @@ -1726,6 +1769,34 @@ export function formulateQuestion({ node, graph, context = {} }) { 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 && @@ -1794,7 +1865,7 @@ export function formulateQuestion({ node, graph, context = {} }) { ) ) { question = sanitizeQuestionText( - investigationStrategy && + (investigationStrategy && isClaimLikeUnknown( node, normaliseText( @@ -1807,7 +1878,8 @@ export function formulateQuestion({ node, graph, context = {} }) { .filter(Boolean) .join(" "), ), - ) + )) || + reasoningPatternSelection.pattern === "diagnosis" ? buildEvidenceFallbackQuestion(fallbackMeaning) : buildNeutralClarificationQuestion(fallbackMeaning), ); diff --git a/tests/graph/question-formulator.test.js b/tests/graph/question-formulator.test.js index 97a8512..e2f982b 100644 --- a/tests/graph/question-formulator.test.js +++ b/tests/graph/question-formulator.test.js @@ -280,12 +280,12 @@ describe("formulateQuestion", () => { expect(result.pattern).toBe("comparison"); }); - it("constraint unknown uses evidence-gathering within the fixed strategy set", () => { + it("user-owned constraint ambiguity produces a clarification question rather than an evidence request", () => { const unknown = makeNode({ id: "n-constraint", - label: "Budget constraint", + label: "Whether avoiding additional risk is a hard constraint", description: - "Need the main budget constraint because it limits the available options.", + "Need to know whether avoiding additional risk is a hard constraint or a preference/trade-off.", kind: "unknown", status: "unknown", confidence: "medium", @@ -296,8 +296,33 @@ describe("formulateQuestion", () => { graph: makeGraphFor(unknown), }); - expect(result.strategy).toBe("evidence_gathering"); + expect(result.strategy).toBeNull(); + expect(result.question).toBe( + "Is avoiding additional risk a hard constraint or a preference/trade-off?", + ); + }); + + it("evidence-resolvable competing-cause unknown stays on an evidence route rather than neutral clarification", () => { + const unknown = makeNode({ + id: "n-delivery-cause", + label: "Possible causes of the delivery delay", + description: + "Need to determine whether staff capacity or supplier lead times are responsible for the delivery delay.", + kind: "unknown", + status: "unknown", + confidence: "medium", + }); + + const result = formulateQuestion({ + node: unknown, + graph: makeGraphFor(unknown, { + centralStatement: "Delivery is delayed and the cause is still unknown.", + }), + }); + + expect(result.reasoningPattern).toBe("diagnosis"); expect(result.question).toContain("What evidence"); + expect(result.question).not.toContain("What would clarify"); }); it("the same unknown can produce different questions when paired with different strategies", () => {