diff --git a/components/scenario-form.jsx b/components/scenario-form.jsx index bd27b5e..6cbc898 100644 --- a/components/scenario-form.jsx +++ b/components/scenario-form.jsx @@ -209,6 +209,44 @@ function clearSession() { try { sessionStorage.removeItem(SESSION_KEY); } catch (_) {} } +/** + * Derives whether the current component state represents a valid investigation + * context sufficient to render a workspace surface. + * + * Valid only when: + * - result carries a situationGraph (renderable graph), OR + * - status is "success" AND there is a non-empty scenario + * (from session restoration with real data). + * + * This predicate is the single source of truth for all render-gate decisions. + * showExperimentView, fixture availability, or sessionStorage keys alone are + * NOT sufficient to constitute valid context. + */ +export function hasValidInvestigationContext(result, status, scenario) { + return Boolean(result?.situationGraph) || + (status === "success" && Boolean(scenario?.trim())); +} + +/** + * Derives the primary surface that must render for the given state tuple. + * Enforces exactly-one-primary-surface invariant: no zero, no two. + */ +export function derivePrimarySurface(result, status, showExperimentView, scenario, activeBranchId) { + if (status === "loading") return "LOADING"; + if (status === "error") return "ERROR_SURFACE"; + + const valid = hasValidInvestigationContext(result, status, scenario); + + // RTO.28A: show branch-selection surface when investigation exists but no branch is active + if (showExperimentView && valid && !activeBranchId) return "BRANCH_SELECTION"; + + if (showExperimentView && valid) return "EXPERIMENT_NOTEBOOK"; + if (!showExperimentView && valid) return "NORMAL_WORKSPACE"; + if (!showExperimentView) return "SCENARIO_ENTRY"; + // showExperimentView === true but no valid context → fall back to entry + return "SCENARIO_ENTRY"; +} + export default function ScenarioForm() { const [scenario, setScenario] = useState(""); const [status, setStatus] = useState("idle"); // idle | loading | error | success @@ -225,7 +263,8 @@ export default function ScenarioForm() { /* ── RTO.25A — passive late-result branch switcher (experimental) ── */ - const [activeBranchId, setActiveBranchId] = useState("branch-a"); + // RTO.28A: no active branch until the user explicitly chooses one. + const [activeBranchId, setActiveBranchId] = useState(null); // Pre-seed Competitor development with a late result for RTO.27A testing const [branchNewResults, setBranchNewResults] = useState({ "branch-a": true }); @@ -291,30 +330,54 @@ export default function ScenarioForm() { return result; }, [activeBranchId, BRANCHES, branchNewResults, doneForNowBranchIds]); + /* ── Valid investigation predicate ─────────────────────── */ + + // Delegated to the exported utility below. + const validCtx = hasValidInvestigationContext(result, status, scenario); + /* Restore persisted session on mount (Phase 3) ─────────── */ useEffect(() => { if (typeof window === "undefined") return; const saved = getSession(); if (!saved) return; + + const hasGraph = Boolean(saved.situationGraph); + setScenario(saved.scenario || ""); - setResult(saved.situationGraph ? { ...saved, situationGraph: saved.situationGraph } : null); + setResult(hasGraph ? { ...saved, situationGraph: saved.situationGraph } : null); setCurrentUnderstanding(saved.summary || null); - setStatus("success"); + + // Partial sessions (present but no graph) must NOT suppress the + // scenario-entry form. Only promote to success when there is actual + // investigation data to render. + if (hasGraph) { + setStatus("success"); + setShowExperimentView(true); + } }, []); /* Restore experiment view preference from storage (after hydration) ─ */ useEffect(() => { if (typeof window === "undefined") return; + + // Only promote to experiment mode when there is real investigation + // data to render. The ce-show-experiment flag is a presentation + // preference, not proof that an investigation exists. + if (!validCtx) return; + const savedExp = sessionStorage?.getItem("ce-show-experiment"); if (savedExp === "true") { setShowExperimentView(true); return; } + + // Also enable experiment mode if session data provides a graph + // (covers the pre-restoration case where scenario was typed but not yet submitted). const session = getSession(); if (session?.situationGraph) { setShowExperimentView(true); } - }, []); + }, [validCtx]); /* Restore facilitator dismiss preference (Experiment 05) ─── */ useEffect(() => { @@ -471,67 +534,106 @@ export default function ScenarioForm() { return (
{currentUnderstanding}
+These are the current lines of inquiry. Pick whichever you want to work on.
+ + {BRANCHES.map((branch) => ( + + ))} +