Files
confidence-engine/app/investigations/[id]/report/page.jsx
T
robbond 99b3d26817 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.
2026-09-03 13:39:40 +01:00

170 lines
6.4 KiB
React

"use client";
import React, { useEffect, useRef, useState } from "react";
import { loadInvestigation, saveInvestigation } from "@/lib/storage/investigation-storage";
import Link from "next/link";
export default function ReportPage() {
const [existing, setExisting] = useState(null);
const [hydrated, setHydrated] = useState(false);
const [generationLoading, setGenerationLoading] = useState(false);
const [generationError, setGenerationError] = useState(false);
const generationAttempted = useRef(false);
useEffect(() => {
setExisting(loadInvestigation());
setHydrated(true);
}, []);
// First-generation: create report when none persists (v0.58)
useEffect(() => {
if (!hydrated) return;
if (existing?.investigationReport) return;
if (generationAttempted.current) return;
generationAttempted.current = true;
const situationGraph = existing?.situationGraph;
const findings = existing?.findings ?? [];
if (!situationGraph) {
setGenerationError(true);
return;
}
(async () => {
setGenerationLoading(true);
try {
const res = await fetch("/api/cases/overview", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ situationGraph, findings }),
});
if (!res.ok) {
setGenerationError(true);
return;
}
const data = await res.json();
if (data.success) {
/* ── 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);
}
} catch {
setGenerationError(true);
} finally {
setGenerationLoading(false);
}
})();
}, [hydrated, existing]);
const report = existing?.investigationReport || null;
const scenario = hydrated ? (existing?.scenario || "") : null;
const paragraphs = (report?.understanding || "")
.split("\n")
.filter(Boolean);
return (
<main className="mx-auto max-w-[800px] px-6 py-16">
<h1 className="mb-2 text-[15px] font-bold tracking-[.2em] uppercase text-teal-700/90">
Investigation Report
</h1>
{/* Situation */}
{scenario && (
<div className="mt-8 rounded-xl border-[2.5px] border-teal-300/70 bg-gradient-to-b from-teal-50/60 to-white px-8 pt-6 pb-7 shadow-sm">
<h2 className="mb-3 text-[11px] font-bold tracking-[.18em] uppercase text-teal-700/70">
Situation
</h2>
<p className="text-base leading-relaxed text-gray-800 whitespace-pre-wrap">
{scenario}
</p>
</div>
)}
{/* What we understand */}
{report ? (
<>
{paragraphs.length > 0 ? (
paragraphs.map((p, i) => (
<div key={i} className="mt-6 rounded-xl border-[2.5px] border-teal-300/70 bg-gradient-to-b from-teal-50/60 to-white px-8 pt-6 pb-7 shadow-sm">
<h2 className="mb-3 text-[11px] font-bold tracking-[.18em] uppercase text-teal-700/70">
What we understand
</h2>
<p className="text-base leading-relaxed text-gray-800">{p}</p>
</div>
))
) : (
<div className="mt-6 rounded-xl border-[2.5px] border-teal-300/70 bg-gradient-to-b from-teal-50/60 to-white px-8 pt-6 pb-7 shadow-sm">
<h2 className="mb-3 text-[11px] font-bold tracking-[.18em] uppercase text-teal-700/70">
What we understand
</h2>
<p className="text-base leading-relaxed text-gray-800">{report.understanding || ""}</p>
</div>
)}
{/* What remains plausible — conditional */}
{report.hasPlausibleInterpretations && report.plausibleInterpretations ? (
<div className="mt-6 rounded-xl border-[2.5px] border-blue-300/70 bg-gradient-to-b from-blue-50/60 to-white px-8 pt-6 pb-7 shadow-sm">
<h2 className="mb-3 text-[11px] font-bold tracking-[.18em] uppercase text-blue-700/70">
What remains plausible
</h2>
<p className="text-base leading-relaxed text-gray-800 italic">
{report.plausibleInterpretations}
</p>
</div>
) : null}
</>
) : (
/* Skeleton / loading state when no persisted report exists */
<>
<div className="mt-8 rounded-xl border-[2.5px] border-gray-200 bg-gray-50/50 px-8 pt-6 pb-7 shadow-sm">
<h2 className="mb-3 text-[11px] font-bold tracking-[.18em] uppercase text-gray-400">
What we understand
</h2>
{generationLoading ? (
<div className="flex flex-col gap-3 py-2" aria-live="polite">
<span className="text-[11px] font-semibold tracking-wider text-gray-400 uppercase">Generating report&#8230;</span>
{[0, 1, 2].map((i) => (
<div key={i} className="h-4 w-full rounded animate-pulse" style={{ backgroundColor: "rgb(229 231 235)", animationDelay: `${i * 150}ms`, width: i === 1 ? "80%" : i === 2 ? "65%" : "90%" }} />
))}
</div>
) : generationError ? (
<p className="text-sm text-red-600">Report generation failed. You may try again from the Investigation page.</p>
) : null}
</div>
</>
)}
{/* Back to investigation */}
<div className="mt-10">
<Link
href="/investigations/case-1"
className="rounded-lg border border-teal-600 bg-white px-4 py-2 text-sm font-medium text-teal-700 hover:bg-teal-50 transition"
>
Back to investigation
</Link>
</div>
{/* Back to portfolio */}
<div className="mt-3">
<Link
href="/"
className="rounded-lg border border-teal-600 bg-white px-4 py-2 text-sm font-medium text-teal-700 hover:bg-teal-50 transition"
>
Back to portfolio
</Link>
</div>
</main>
);
}