From 4c5666dfb37dee7b10597b0937bf6c9df3bf1124 Mon Sep 17 00:00:00 2001 From: robbond Date: Mon, 10 Aug 2026 10:17:35 +0100 Subject: [PATCH] reasoning: suppress explanation question without relationship structure --- lib/graph/question-formulator.js | 4 +- tests/graph/comparability-assessment.test.js | 63 ++++++++++ .../reasoning-pattern-validation.test.js | 118 ++++++++++++++++++ 3 files changed, 184 insertions(+), 1 deletion(-) diff --git a/lib/graph/question-formulator.js b/lib/graph/question-formulator.js index 5a2bd7a..7d337d9 100644 --- a/lib/graph/question-formulator.js +++ b/lib/graph/question-formulator.js @@ -436,7 +436,9 @@ function classifyObservationRelationshipWhenComparable(graph) { reason: "There is not enough structure to classify the relationship safely.", contradictionReasoningAllowed: false, - questionRequired: true, + questionRequired: false, + questionSuppressedReason: + "No meaningful relationship structure was established; uncertainty is preserved without fabricating an explanation problem.", }; } diff --git a/tests/graph/comparability-assessment.test.js b/tests/graph/comparability-assessment.test.js index 48b5510..fb48df8 100644 --- a/tests/graph/comparability-assessment.test.js +++ b/tests/graph/comparability-assessment.test.js @@ -128,6 +128,69 @@ describe("comparability assessment", () => { ]); }); + it("returns questionRequired=false for fallback (no meaningful relationship structure)", () => { + // Two non-measurement-like observations with no shared concept and no known direction. + // They pass comparability (confirmed) but fall through all classification conditions: + // - not duplicate, no available+unavailable, no revenue/cash pair, no all-known-directions + // → hits the fallback which now returns questionRequired=false + const graph = { + centralStatement: + "Temperature was measured. Ice was observed.", + nodes: [ + { + id: "obs-temperature", + label: "Temperature was measured.", + description: "Temperature was measured.", + kind: "observation", + status: "supported", + confidence: "high", + value: null, + unit: null, + evidenceIds: [], + dependsOn: [], + affects: [], + parentId: null, + childIds: [], + }, + { + id: "obs-ice", + label: "Ice was observed.", + description: "Ice was observed.", + kind: "observation", + status: "supported", + confidence: "high", + value: null, + unit: null, + evidenceIds: [], + dependsOn: [], + affects: [], + parentId: null, + childIds: [], + }, + ], + edges: [], + activeUnknownNodeId: null, + resolvedNodeIds: [], + currentSummary: "Fallback regression fixture", + }; + + const relationship = classifyObservationRelationship(graph); + + expect(relationship.relationshipStatus).toBe("insufficient_information"); + expect(relationship.questionRequired).toBe(false); + // Verify reasoning stages exist and have correct status + const stages = relationship.reasoningStages; + expect(stages.length).toBe(2); + expect(stages[0].stage).toBe("comparability"); + expect(stages[0].status).toBe("confirmed"); + expect(stages[1].stage).toBe("relationship"); + expect(stages[1].status).toBe("insufficient_information"); + + // questionRequired=false means buildEmergentReasoningUnknown returns early, + // so no "Explanation for why..." unknown is created — the engine preserves + // uncertainty rather than fabricating an explanation problem. + }); + it("allows contradiction reasoning only for genuine contradictions", () => { const serviceGraph = { centralStatement: diff --git a/tests/graph/reasoning-pattern-validation.test.js b/tests/graph/reasoning-pattern-validation.test.js index 772d1b7..9be28d8 100644 --- a/tests/graph/reasoning-pattern-validation.test.js +++ b/tests/graph/reasoning-pattern-validation.test.js @@ -279,4 +279,122 @@ describe("reasoning-pattern validation", () => { expect(result.selectedQuestion?.nodeId).not.toBe("n-incompatible-child"); expect(result.selectedQuestion?.reasoningPattern).toBe("decision"); }); + + it("preserves potentially_related with questionRequired=true for meaningful shared structure", () => { + // Two observations sharing "revenue" concept, both with known direction ("up"). + // With confirmed comparability (via stored state), condition 2 triggers: + // sharedConcepts.size > 0 && all directions known → potentially_related + const graph = makeGraph({ + centralStatement: "Revenue increased. Revenue doubled.", + nodes: [ + makeNode({ + id: "n-revenue-1", + label: "Revenue increased.", + description: "Revenue increased.", + kind: "observation", + status: "supported", + confidence: "high", + }), + makeNode({ + id: "n-revenue-2", + label: "Revenue doubled.", + description: "Revenue doubled.", + kind: "observation", + status: "supported", + confidence: "high", + }), + ], + edges: [], + activeUnknownNodeId: null, + resolvedNodeIds: [], + currentSummary: "Potentially related regression fixture", + reasoningState: { + comparabilityStatus: "confirmed", + comparabilityReason: "Stored from earlier assessment.", + comparabilityEvidence: [], + relationshipStatus: "potentially_related", + relationshipReason: "Observations concern the same subject but do not assert a direct contradiction.", + relationshipAssessed: true, + contradictionReasoningAllowed: false, + reasoningStages: [ + { stage: "comparability", status: "confirmed", outcome: "Stored." }, + ], + }, + }); + + const relationship = classifyObservationRelationship(graph); + + expect(relationship.relationshipStatus).toBe("potentially_related"); + expect(relationship.questionRequired).toBe(true); + + const result = applyValidatedProposal({ + situationGraph: graph, + proposal: { + addedNodes: [], + updatedNodes: [ + { + nodeId: "n-revenue-2", + previousStatus: "supported", + newStatus: "resolved", + previousValue: null, + newValue: "Revenue doubled confirmed by accounts.", + reason: "Regression test update.", + }, + ], + addedEdges: [], + removedEdgeIds: [], + resolvedUnknownNodeIds: [], + affectedNodeIds: [], + selectedQuestion: null, + }, + }); + + expect(result.success).toBe(true); + }); + + it("preserves contradiction with questionRequired=true for genuinely opposing observations", () => { + const graph = makeGraph({ + centralStatement: + "The service was reported as available throughout the hour and unavailable throughout the same hour.", + nodes: [ + makeNode({ + id: "n-available", + label: "The service was available throughout the hour.", + description: "The service was available throughout the hour.", + kind: "observation", + status: "supported", + confidence: "high", + }), + makeNode({ + id: "n-unavailable", + label: "The service was unavailable throughout the same hour.", + description: "The service was unavailable throughout the same hour.", + kind: "observation", + status: "supported", + confidence: "high", + }), + ], + edges: [], + activeUnknownNodeId: null, + resolvedNodeIds: [], + currentSummary: "Contradiction regression fixture", + reasoningState: { + comparabilityStatus: "confirmed", + comparabilityReason: "Stored from earlier assessment.", + comparabilityEvidence: [], + relationshipStatus: "contradictory", + relationshipReason: "The observations assert mutually incompatible states about the same subject.", + relationshipAssessed: true, + contradictionReasoningAllowed: true, + reasoningStages: [ + { stage: "comparability", status: "confirmed", outcome: "Stored." }, + ], + }, + }); + + const relationship = classifyObservationRelationship(graph); + + expect(relationship.relationshipStatus).toBe("contradictory"); + expect(relationship.questionRequired).toBe(true); + }); });