feat(confidence-engine): use server investigation persistence

This commit is contained in:
2026-09-08 19:21:05 +01:00
parent 6dd447e56a
commit d6df1d210e
15 changed files with 425 additions and 114 deletions
+30 -12
View File
@@ -6,7 +6,7 @@ import DiagnosticsView from "@/components/diagnostics-view";
import ReasoningWorkspace, { LoadingOverlay, ContinueLaterBanner } from "@/components/reasoning-workspace";
import { mockFetch, AVAILABLE_SCENARIOS } from "@/lib/mocks/confidence-engine/mock-client";
import { deriveFindingsFromContributions, normalizeFindings } from "@/lib/graph/finding-helpers";
import { loadInvestigation, saveInvestigation, restartInvestigation, clearInvestigation } from "@/lib/storage/investigation-storage";
import { loadInvestigation, saveInvestigation, restartInvestigation } from "@/lib/storage/investigation-storage";
/* Compile-time env resolution — NEXT_PUBLIC_ vars are injected by Next.js at build */
const MOCK_ENABLED = process.env.NEXT_PUBLIC_CONFIDENCE_ENGINE_MOCKS === "true";
@@ -303,6 +303,16 @@ export default function ScenarioForm({ investigationId, onNavigateToReport }) {
/* ── v2 findings from focused contributions ─────────────── */
const [findings, setFindings] = useState([]);
const [hydrated, setHydrated] = useState(false);
function persist(snapshot) {
void saveInvestigation(snapshot).catch((error) => console.error("Investigation autosave failed", error));
}
function restartPersistedInvestigation() {
void restartInvestigation(investigationId)
.catch((error) => console.error("Investigation restart failed", error));
}
function appendFinding(finding) {
setFindings((prev) => {
@@ -544,8 +554,11 @@ export default function ScenarioForm({ investigationId, onNavigateToReport }) {
/* Restore persisted session on mount ─────────── */
useEffect(() => {
if (typeof window === "undefined") return;
const saved = investigationId ? loadInvestigation(investigationId) : null;
if (!saved) return;
let active = true;
(async () => {
try {
const saved = investigationId ? await loadInvestigation(investigationId) : null;
if (!active || !saved) return;
const hasGraph = Boolean(saved.situationGraph);
@@ -569,7 +582,12 @@ export default function ScenarioForm({ investigationId, onNavigateToReport }) {
if (hasGraph) {
setStatus("success");
}
}, []);
} finally {
if (active) setHydrated(true);
}
})();
return () => { active = false; };
}, [investigationId]);
/* ── Canonical autosave — persist whenever state changes (Phase 2) ── */
@@ -578,9 +596,9 @@ export default function ScenarioForm({ investigationId, onNavigateToReport }) {
// Guard: no valid investigation yet → skip autosave during idle/start flows.
// Also prevents overwriting an existing saved investigation with the initial
// empty state of a fresh ScenarioForm instance (hydration race guard).
if (!result?.situationGraph) return;
if (!hydrated || !result?.situationGraph) return;
void saveInvestigation({
persist({
id: investigationId,
scenario,
situationGraph: result.situationGraph,
@@ -600,7 +618,7 @@ export default function ScenarioForm({ investigationId, onNavigateToReport }) {
focusedContributions,
findings,
investigationReport,
investigationRevision,
investigationRevision, hydrated,
]);
/* Restore facilitator dismiss preference (Experiment 05) ─── */
@@ -680,7 +698,7 @@ export default function ScenarioForm({ investigationId, onNavigateToReport }) {
setResult(normalised);
/* ── v0.59a — provenance: first meaningful change sets revision to 1 ── */
setInvestigationRevision(1);
saveInvestigation({ id: investigationId, scenario, situationGraph: normalised.situationGraph, selectedQuestion: normalised.selectedQuestion, summary: data.summary ?? null, updatedAt: new Date().toISOString(), focusedContributions, findings: [], investigationReport, investigationRevision: 1 });
persist({ id: investigationId, scenario, situationGraph: normalised.situationGraph, selectedQuestion: normalised.selectedQuestion, summary: data.summary ?? null, updatedAt: new Date().toISOString(), focusedContributions, findings: [], investigationReport, investigationRevision: 1 });
} else {
setStatus("error");
setCurrentUnderstanding(data.summary ?? null);
@@ -767,7 +785,7 @@ export default function ScenarioForm({ investigationId, onNavigateToReport }) {
/* ── v0.59a — provenance: meaningful change advances revision ── */
const nextRev = (investigationRevision ?? 0) + 1;
setInvestigationRevision(nextRev);
saveInvestigation({ id: investigationId, scenario, situationGraph: nextGraph, selectedQuestion: normaliseUpdateSelectedQuestion(outcome.selectedQuestion), summary: currentUnderstanding, updatedAt: new Date().toISOString(), focusedContributions, findings: nextFindings, investigationReport, investigationRevision: nextRev });
persist({ id: investigationId, scenario, situationGraph: nextGraph, selectedQuestion: normaliseUpdateSelectedQuestion(outcome.selectedQuestion), summary: currentUnderstanding, updatedAt: new Date().toISOString(), focusedContributions, findings: nextFindings, investigationReport, investigationRevision: nextRev });
} else {
setUpdateStatus("error");
setUpdateError(outcome);
@@ -947,7 +965,7 @@ export default function ScenarioForm({ investigationId, onNavigateToReport }) {
setResult((prev) => ({ ...(prev ?? {}), situationGraph: nextGraph }));
}}
onRestart={() => {
restartInvestigation(investigationId);
restartPersistedInvestigation();
setInvestigationRevision(0);
setStatus("idle");
setResult(null);
@@ -968,7 +986,7 @@ export default function ScenarioForm({ investigationId, onNavigateToReport }) {
{/* ── Continue later banner when session was restored ── */}
{status === "success" && result?.updatedAt && (
<ContinueLaterBanner onRestart={() => { restartInvestigation(investigationId); setInvestigationRevision(0); setStatus("idle"); setResult(null); setAnswer(""); setUpdateStatus("idle"); setCurrentUnderstanding(null); setFocusedContributions([]); setFindings([]); }} />
<ContinueLaterBanner onRestart={() => { restartPersistedInvestigation(); setInvestigationRevision(0); setStatus("idle"); setResult(null); setAnswer(""); setUpdateStatus("idle"); setCurrentUnderstanding(null); setFocusedContributions([]); setFindings([]); }} />
)}
{/* Reset button after successful analysis */}
@@ -976,7 +994,7 @@ export default function ScenarioForm({ investigationId, onNavigateToReport }) {
<div className="text-center">
<button
onClick={() => {
restartInvestigation(investigationId);
restartPersistedInvestigation();
setInvestigationRevision(0);
setScenario("");
setStatus("idle");