feat(confidence-engine): case/update synthesis — dedicated reconstruction per update (v0.50)

Architecture: after successful /api/cases/update, derive explicit nextGraph +
nextFindings, call synthesizeFromFindings exactly once, replace Current
Understanding with reconstruction result.

Key invariants:
- outcome.summary retired as final CU authority → always synthesis reconstruction
- Explicit derived state (no React-state reread) for graph and findings
- Previous CU preserved on synthesis failure (no fallback to outcome.summary)
- Graph and Findings NOT lost on synthesis failure
- saveInvestigation persistence uses currentUnderstanding, not outcome.summary

Deterministic regression: 7 tests (Cases A-E + 2 edges) covering all rules.

Files: components/scenario-form.jsx, tests/ui/scenario-form-case-update-synthesis.test.jsx
This commit is contained in:
2026-08-31 09:22:50 +01:00
parent 989b88a4a1
commit b270aa5624
3 changed files with 492 additions and 9 deletions
+20 -9
View File
@@ -577,23 +577,21 @@ export default function ScenarioForm() {
const outcome = submission.data;
if (submission.ok && outcome.success) {
// Merge server-returned findings with local state
let newFindings = [...findings];
// ── Derive explicit next canonical state (no React-state reread) ──
const nextGraph = outcome.updatedSituationGraph;
let nextFindings = [...findings];
if (outcome.appendedFindings && Array.isArray(outcome.appendedFindings)) {
newFindings = [...newFindings, ...outcome.appendedFindings];
nextFindings = [...nextFindings, ...outcome.appendedFindings];
}
setUpdateStatus("success");
setCurrentUnderstanding(
outcome.summary ? outcome.summary : currentUnderstanding,
);
setUpdateResult({
...outcome,
previousSituationGraph: result?.situationGraph ?? null,
});
setResult((current) => ({
...current,
situationGraph: outcome.updatedSituationGraph,
situationGraph: nextGraph,
selectedQuestion: normaliseUpdateSelectedQuestion(
outcome.selectedQuestion,
),
@@ -602,9 +600,22 @@ export default function ScenarioForm() {
.map((node) => node.id),
diagnostics: outcome.diagnostics,
}));
setFindings(nextFindings);
// ── Coalesced transition: one synthesis per successful update ──
void synthesizeFromFindings(fetch, {
situationGraph: nextGraph,
findings: normalizeFindings(nextFindings),
}).then((res) => {
if (res.ok && res.data?.currentUnderstanding) {
setCurrentUnderstanding(res.data.currentUnderstanding);
}
// On synthesis failure: graph/Findings already persisted, CU preserved, no retry.
});
setAnswer("");
// Persist after successful update turn — include findings
saveInvestigation({ scenario, situationGraph: outcome.updatedSituationGraph, selectedQuestion: normaliseUpdateSelectedQuestion(outcome.selectedQuestion), summary: outcome.summary ?? currentUnderstanding, updatedAt: new Date().toISOString(), focusedContributions, findings: newFindings });
// 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 });
} else {
setUpdateStatus("error");
setUpdateError(outcome);