feat(confidence-engine): v0.59a — correct Investigation revision provenance

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.
This commit is contained in:
2026-09-03 13:39:40 +01:00
parent 37a9a12f93
commit 99b3d26817
5 changed files with 437 additions and 12 deletions
+2
View File
@@ -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 ─── */
+42 -5
View File
@@ -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 && (
<ContinueLaterBanner onRestart={() => { clearInvestigation(); setStatus("idle"); setResult(null); setAnswer(""); setUpdateStatus("idle"); setCurrentUnderstanding(null); setFocusedContributions([]); setFindings([]); }} />
<ContinueLaterBanner onRestart={() => { 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 }) {
<button
onClick={() => {
clearInvestigation();
setInvestigationRevision(0);
setScenario("");
setStatus("idle");
setResult(null);