From 6288ef10312d05f1b0227f70ea7cc2ab1d582bbb Mon Sep 17 00:00:00 2001 From: robbond Date: Wed, 26 Aug 2026 11:30:59 +0100 Subject: [PATCH] fix(confidence-engine): keep focused investigation in current presentation --- components/reasoning-workspace.jsx | 26 +++++- tests/situation-rendering.test.jsx | 128 +++++++++++++++++++++++++++++ 2 files changed, 150 insertions(+), 4 deletions(-) diff --git a/components/reasoning-workspace.jsx b/components/reasoning-workspace.jsx index 126e875..b39e03b 100644 --- a/components/reasoning-workspace.jsx +++ b/components/reasoning-workspace.jsx @@ -1329,7 +1329,11 @@ export default function ReasoningWorkspace({ // ── RTO.29D — manage initial reflection lifecycle ──────────── useEffect(() => { - const entering = status === "success" && hasGraph && propUnderstanding && !hasSelectedQuestion; + // Entry is now based on successful initial reconstruction state ONLY. + // selectedQuestion is internal reasoning data — it must NOT gate the + // post-Analyse presentation path. Both null and populated + // selectedQuestion enter Run A identically. + const entering = status === "success" && hasGraph && propUnderstanding; if (entering) { setPostAnalyseStatus("success"); setInitialReflectionActive(true); @@ -1340,14 +1344,28 @@ export default function ReasoningWorkspace({ } }, [status, result, hasGraph]); - // Deactivate initial reflection when user does anything meaningful + // Deactivate initial reflection when user submits a turn (leaves post-analyse). + // DO NOT deactivate on formulationStep or processingStep transitions — those + // are part of the focused workflow and must not break Run A's lifecycle. useEffect(() => { if (!initialReflectionActive) return; - if (selectedPresentationItemId || focusedPresentationItemId || investigationHistory.length > 0 || formulationStep !== "idle" || processingStep !== "idle") { + + // If we've already transitioned out of post-analyse mode, bail early. + // This prevents the effect from re-triggering on every state change after + // an explicit deactivation and avoids spurious double-fires when multiple + // dependencies change in the same commit. + if (postAnalyseStatus !== "success") return; + + // Genuine exit: a turn has been submitted → history grows past zero. + // No other lifecycle state (formulation, processing, focused IDs, etc.) + // deactivates Run A during a focused investigation cycle. + if (investigationHistory.length > 0) { setInitialReflectionActive(false); setPostAnalyseStatus(null); } - }, [selectedPresentationItemId, focusedPresentationItemId, investigationHistory, formulationStep, processingStep]); + // All other lifecycle events within the focused workflow are intentionally + // ignored here — Run A stays active until an explicit turn is submitted. + }, [initialReflectionActive, investigationHistory, postAnalyseStatus]); return (
diff --git a/tests/situation-rendering.test.jsx b/tests/situation-rendering.test.jsx index 027d950..eed1b8c 100644 --- a/tests/situation-rendering.test.jsx +++ b/tests/situation-rendering.test.jsx @@ -252,3 +252,131 @@ describe("No duplicate Situation cards — regression guard for removed Path D a expect(countSituationCards(props)).toBe(0); }); }); + +// ── Entry routing fix: Run A must render regardless of selectedQuestion ── + +describe("Run A entry routing — selectedQuestion neutrality", () => { + /** + * Simulates the corrected post-Analyse entry condition from reasoning-workspace.jsx: + * const entering = status === "success" && hasGraph && propUnderstanding; + * BEFORE fix it was: `&& !hasSelectedQuestion` which caused Run B when selectedQuestion was populated. + */ + function simulateRunAEntry({ status, hasGraph, propUnderstanding, hasSelectedQuestion }) { + // Corrected entry condition — no longer gates on selectedQuestion + const entering = status === "success" && hasGraph && propUnderstanding; + + if (!entering) return { run: "B", initialReflectionActive: false, postAnalyseStatus: null }; + + return { + run: "A", + initialReflectionActive: true, + postAnalyseStatus: "success", + hasSelectedQuestion, // preserved — not mutated by routing logic + }; + } + + /** + * Simulates CurrentInvestigationCard rendering: shown only when NOT in post-Analyse reflection. + */ + function currentInvestigationCardRendered(postAnalyseStatus) { + return postAnalyseStatus !== "success"; + } + + // ── Entry equivalence ── + + it("selectedQuestion = null → Run A (initial-reflection-surface)", () => { + const result = simulateRunAEntry({ + status: "success", + hasGraph: true, + propUnderstanding: "The company should pursue a phased European expansion.", + hasSelectedQuestion: false, // null + }); + + expect(result.run).toBe("A"); + expect(result.initialReflectionActive).toBe(true); + expect(result.postAnalyseStatus).toBe("success"); + expect(currentInvestigationCardRendered(result.postAnalyseStatus)).toBe(false); + }); + + it("selectedQuestion = populated object → Run A (initial-reflection-surface)", () => { + const result = simulateRunAEntry({ + status: "success", + hasGraph: true, + propUnderstanding: "The company should pursue a phased European expansion.", + hasSelectedQuestion: true, // populated — was the old Run B trigger + }); + + expect(result.run).toBe("A"); + expect(result.initialReflectionActive).toBe(true); + expect(result.postAnalyseStatus).toBe("success"); + expect(currentInvestigationCardRendered(result.postAnalyseStatus)).toBe(false); + }); + + it("Green CurrentInvestigationCard NOT shown in either null or populated initial state", () => { + const nullCase = simulateRunAEntry({ + status: "success", hasGraph: true, propUnderstanding: "X", hasSelectedQuestion: false, + }); + const popCase = simulateRunAEntry({ + status: "success", hasGraph: true, propUnderstanding: "X", hasSelectedQuestion: true, + }); + + expect(currentInvestigationCardRendered(nullCase.postAnalyseStatus)).toBe(false); + expect(currentInvestigationCardRendered(popCase.postAnalyseStatus)).toBe(false); + }); + + // ── Focused continuation invariant (populated selectedQuestion path) ── + + it("Run A + click Open Question → formulation stays in Run A (lifecycle preserved)", () => { + // Start: post-Analyse reflection active, has selectedQuestion (Run B scenario) + let initialReflectionActive = true; + let postAnalyseStatus = "success"; + let investigationHistoryLength = 0; + + // Simulate lifecycle deactivation effect (the second useEffect in the component) + function checkDeactivate() { + if (!initialReflectionActive) return false; // already deactivated + if (postAnalyseStatus !== "success") return false; + if (investigationHistoryLength > 0) { + initialReflectionActive = false; + postAnalyseStatus = null; + return true; // deactivated + } + return false; // still active + } + + // User clicks "Open Question" — starts focused formulation + expect(checkDeactivate()).toBe(false); // still in Run A + expect(initialReflectionActive).toBe(true); + expect(postAnalyseStatus).toBe("success"); + + // Answer entered → deconstruction processing + expect(checkDeactivate()).toBe(false); // still in Run A + expect(initialReflectionActive).toBe(true); + + // Deconstruction completes — result available, history still empty (no turn submitted yet) + expect(checkDeactivate()).toBe(false); + + // User submits a turn (leaves post-Analyse) → history grows + investigationHistoryLength = 1; + const deactivated = checkDeactivate(); + expect(deactivated).toBe(true); + expect(initialReflectionActive).toBe(false); + expect(postAnalyseStatus).toBe(null); + + // After deactivation, workspace grid renders with CurrentInvestigationCard + expect(currentInvestigationCardRendered(postAnalyseStatus)).toBe(true); + }); + + it("selectedQuestion data is preserved (not mutated) through routing", () => { + const selectedQuestionData = { nodeId: "u1", question: "What is the revenue model?" }; + + // Even with populated selectedQuestion, routing should enter Run A + const result = simulateRunAEntry({ + status: "success", hasGraph: true, propUnderstanding: "X", hasSelectedQuestion: true, + }); + + expect(result.run).toBe("A"); + // The entry function receives the original hasSelectedQuestion value — it does NOT mutate it + expect(result.hasSelectedQuestion).toBe(true); + }); +});