fix(confidence-engine): keep focused investigation in current presentation
This commit is contained in:
@@ -1329,7 +1329,11 @@ export default function ReasoningWorkspace({
|
|||||||
|
|
||||||
// ── RTO.29D — manage initial reflection lifecycle ────────────
|
// ── RTO.29D — manage initial reflection lifecycle ────────────
|
||||||
useEffect(() => {
|
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) {
|
if (entering) {
|
||||||
setPostAnalyseStatus("success");
|
setPostAnalyseStatus("success");
|
||||||
setInitialReflectionActive(true);
|
setInitialReflectionActive(true);
|
||||||
@@ -1340,14 +1344,28 @@ export default function ReasoningWorkspace({
|
|||||||
}
|
}
|
||||||
}, [status, result, hasGraph]);
|
}, [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(() => {
|
useEffect(() => {
|
||||||
if (!initialReflectionActive) return;
|
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);
|
setInitialReflectionActive(false);
|
||||||
setPostAnalyseStatus(null);
|
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 (
|
return (
|
||||||
<div className="space-y-6" data-testid="reasoning-workspace">
|
<div className="space-y-6" data-testid="reasoning-workspace">
|
||||||
|
|||||||
@@ -252,3 +252,131 @@ describe("No duplicate Situation cards — regression guard for removed Path D a
|
|||||||
expect(countSituationCards(props)).toBe(0);
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user