From 99b3d2681790c79421f2475fa2d7110abbb44686 Mon Sep 17 00:00:00 2001 From: robbond Date: Thu, 3 Sep 2026 13:39:40 +0100 Subject: [PATCH] =?UTF-8?q?feat(confidence-engine):=20v0.59a=20=E2=80=94?= =?UTF-8?q?=20correct=20Investigation=20revision=20provenance?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Semantic revision tracking ensures every meaningful persisted Investigation change advances investigationRevision exactly once, while Report generation records (but does not advance) the current revision as generatedFromRevision for provenance integrity. Corrections: - updateFindingDisposition: add setInvestigationRevision(+1) for semantic transitions (eligible→not_relevant, restore) - updateFindingProposition: add no-op guard + setInvestigationRevision(+1) - onRestart/ContinueLaterBanner/reset button: add setInvestigationRevision(0) - onSituationGraphChange (Re-open seam): already had revision +1 in dirty impl Established behaviour preserved: - Re-open via reopenResolvedUnknown → onSituationGraphChange → revision +1 - Empty Done via handleDoneForNowPromotion → revision +1 - Report generation records generatedFromRevision, advances by 0 - Autosave passes revision but does not increment it - clearInvestigation() ownership intact Tests: targeted Vitest suite (17 tests) covering all provenance boundaries. Durable rule documented in current-handoff.md §v0.59a. --- app/investigations/[id]/report/page.jsx | 10 +- components/reasoning-workspace.jsx | 2 + components/scenario-form.jsx | 47 +++- docs/current-handoff.md | 41 ++- tests/v059a-provenance-revision.test.js | 349 ++++++++++++++++++++++++ 5 files changed, 437 insertions(+), 12 deletions(-) create mode 100644 tests/v059a-provenance-revision.test.js diff --git a/app/investigations/[id]/report/page.jsx b/app/investigations/[id]/report/page.jsx index ac90bff..093b217 100644 --- a/app/investigations/[id]/report/page.jsx +++ b/app/investigations/[id]/report/page.jsx @@ -48,10 +48,12 @@ export default function ReportPage() { const data = await res.json(); if (data.success) { - setExisting((prev) => { - const updated = { ...prev, investigationReport: { understanding: data.understanding, plausibleInterpretations: data.plausibleInterpretations, hasPlausibleInterpretations: true } }; - saveInvestigation(updated); - return updated; + /* ── v0.59a — provenance: record generation revision (does NOT change Investigation revision) ── */ + const rev = existing?.investigationRevision ?? 0; + const reportData = { understanding: data.understanding, plausibleInterpretations: data.plausibleInterpretations, hasPlausibleInterpretations: true, generatedFromRevision: rev }; + setExisting((p) => { + saveInvestigation({ ...p, investigationReport: reportData }); + return { ...p, investigationReport: reportData }; }); } else { setGenerationError(true); diff --git a/components/reasoning-workspace.jsx b/components/reasoning-workspace.jsx index a27a569..9d4a0e7 100644 --- a/components/reasoning-workspace.jsx +++ b/components/reasoning-workspace.jsx @@ -1329,6 +1329,8 @@ export default function ReasoningWorkspace({ onImmediateGraphChange, /* ── canonical graph-replacement seam (future Re-open) ── */ onSituationGraphChange, + /* ── v0.59a — provenance revision ──────────────────── */ + investigationRevision, /* ── test init seam (no effect → immediate state) ───────── */ initialPostAnalyseStatus, /* ── v0.54b — investigation overview transient state ─── */ diff --git a/components/scenario-form.jsx b/components/scenario-form.jsx index 54a3c80..d0121e9 100644 --- a/components/scenario-form.jsx +++ b/components/scenario-form.jsx @@ -292,6 +292,9 @@ export default function ScenarioForm({ onNavigateToReport }) { /* ── v0.55 — persisted investigation report (derived artefact) ── */ const [investigationReport, setInvestigationReport] = useState(null); + /* ── v0.59a — provenance: Investigation revision tracking ── */ + const [investigationRevision, setInvestigationRevision] = useState(0); + /* ── in-flight gate for episode reconsideration on Done ──── */ const doneInProgressRef = useRef(false); @@ -326,6 +329,9 @@ export default function ScenarioForm({ onNavigateToReport }) { if (!notRelevantTransition && !restoreTransition) return; + /* ── v0.59a — provenance: eligible evidence set changed ── */ + setInvestigationRevision((prev) => (prev ?? 0) + 1); + const currentGraph = result?.situationGraph; if (!currentGraph) return; @@ -345,8 +351,15 @@ export default function ScenarioForm({ onNavigateToReport }) { f.id === findingId ? { ...f, proposition: newProposition, userDisposition: null } : f, ); + /* ── v0.59a — provenance: no-op guard ── */ + const prevFinding = (findings ?? []).find((f) => f.id === findingId); + if (prevFinding?.proposition === newProposition) return; // no semantic change + setFindings(() => nextFindings); + /* ── v0.59a — provenance: corrected Finding changes evidence ── */ + setInvestigationRevision((prev) => (prev ?? 0) + 1); + // ── Synthesis trigger: corrected Finding → one reconstruction ── const currentGraph = result?.situationGraph; if (!currentGraph) return; @@ -408,6 +421,10 @@ export default function ScenarioForm({ onNavigateToReport }) { setResult, }); + /* ── v0.59a — provenance: episode done is meaningful evidence change ── */ + const nextRev = (investigationRevision ?? 0) + 1; + setInvestigationRevision(nextRev); + /* CU synthesis — install only on success */ if (doneResult?.synthesisResult?.ok && doneResult.synthesisResult.data?.currentUnderstanding) { setCurrentUnderstanding(doneResult.synthesisResult.data.currentUnderstanding); @@ -448,10 +465,12 @@ export default function ScenarioForm({ onNavigateToReport }) { setOverviewState(res); // Persist as a derived artefact of this investigation + const rev = investigationRevision ?? 0; const report = { understanding: res.understanding, plausibleInterpretations: hasPlausibleInput ? res.plausibleInterpretations ?? "" : "", hasPlausibleInterpretations: hasPlausibleInput, + generatedFromRevision: rev, }; setInvestigationReport(report); @@ -465,6 +484,7 @@ export default function ScenarioForm({ onNavigateToReport }) { focusedContributions, findings, investigationReport: report, + investigationRevision: rev, }); } // On failure: do not clear existing CU, do not block further attempts @@ -539,6 +559,9 @@ export default function ScenarioForm({ onNavigateToReport }) { setInvestigationReport(saved.investigationReport); } + /* ── v0.59a — hydrate provenance revision ─────────── */ + setInvestigationRevision(saved.investigationRevision ?? 0); + // 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. @@ -565,6 +588,7 @@ export default function ScenarioForm({ onNavigateToReport }) { focusedContributions, findings, investigationReport, + investigationRevision, }); }, [ scenario, @@ -574,6 +598,7 @@ export default function ScenarioForm({ onNavigateToReport }) { focusedContributions, findings, investigationReport, + investigationRevision, ]); /* Restore facilitator dismiss preference (Experiment 05) ─── */ @@ -651,7 +676,9 @@ export default function ScenarioForm({ onNavigateToReport }) { setCurrentUnderstanding(data.summary ?? null); const normalised = normaliseStartResult(data); setResult(normalised); - saveInvestigation({ scenario, situationGraph: normalised.situationGraph, selectedQuestion: normalised.selectedQuestion, summary: data.summary ?? null, updatedAt: new Date().toISOString(), focusedContributions, findings: [], investigationReport }); + /* ── v0.59a — provenance: first meaningful change sets revision to 1 ── */ + setInvestigationRevision(1); + saveInvestigation({ 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); @@ -735,7 +762,10 @@ export default function ScenarioForm({ onNavigateToReport }) { setAnswer(""); // Persist after successful update turn — include explicit next state - saveInvestigation({ scenario, situationGraph: nextGraph, selectedQuestion: normaliseUpdateSelectedQuestion(outcome.selectedQuestion), summary: currentUnderstanding, updatedAt: new Date().toISOString(), focusedContributions, findings: nextFindings, investigationReport }); + /* ── v0.59a — provenance: meaningful change advances revision ── */ + const nextRev = (investigationRevision ?? 0) + 1; + setInvestigationRevision(nextRev); + saveInvestigation({ scenario, situationGraph: nextGraph, selectedQuestion: normaliseUpdateSelectedQuestion(outcome.selectedQuestion), summary: currentUnderstanding, updatedAt: new Date().toISOString(), focusedContributions, findings: nextFindings, investigationReport, investigationRevision: nextRev }); } else { setUpdateStatus("error"); setUpdateError(outcome); @@ -907,10 +937,16 @@ export default function ScenarioForm({ onNavigateToReport }) { onSummaryUpdate={handleDoneForNowPromotion} /* ── immediate graph transition (Done acknowledged before async) ── */ onImmediateGraphChange={(nextGraph) => setResult((prev) => ({ ...(prev ?? {}), situationGraph: nextGraph }))} - /* ── canonical graph-replacement seam (future Re-open) ── */ - onSituationGraphChange={(nextGraph) => setResult((prev) => ({ ...(prev ?? {}), situationGraph: nextGraph }))} + /* ── v0.59a — provenance tracking ─────────────────── */ + investigationRevision={investigationRevision} + onSituationGraphChange={(nextGraph) => { + const nextRev = (investigationRevision ?? 0) + 1; + setInvestigationRevision(nextRev); + setResult((prev) => ({ ...(prev ?? {}), situationGraph: nextGraph })); + }} onRestart={() => { clearInvestigation(); + setInvestigationRevision(0); setStatus("idle"); setResult(null); setAnswer(""); @@ -930,7 +966,7 @@ export default function ScenarioForm({ onNavigateToReport }) { {/* ── Continue later banner when session was restored ── */} {status === "success" && result?.updatedAt && ( - { clearInvestigation(); setStatus("idle"); setResult(null); setAnswer(""); setUpdateStatus("idle"); setCurrentUnderstanding(null); setFocusedContributions([]); setFindings([]); }} /> + { clearInvestigation(); setInvestigationRevision(0); setStatus("idle"); setResult(null); setAnswer(""); setUpdateStatus("idle"); setCurrentUnderstanding(null); setFocusedContributions([]); setFindings([]); }} /> )} {/* Reset button after successful analysis */} @@ -939,6 +975,7 @@ export default function ScenarioForm({ onNavigateToReport }) {