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);
+ });
+});