feat(confidence-engine): v0.60e route investigation identity
Migrate the Investigation page route to own durable investigation identity via its route [id] segment, passing that ID through to ScenarioForm for hydration and persistence. - Remove hardcoded INVESTIGATION_ID constant from page.jsx - Use params.id as routeId; loadInvestigation(routeId) loads by identity - Pass investigationId prop into ScenarioForm in both branch paths - Session restore calls loadInvestigation(investigationId) - All 4 save call sites include id: investigationId in snapshot - Missing identified Investigation starts clean (no singleton fallback) - Legacy singleton is not migrated/fallback-loaded - Portfolio remains unmigrated; Report remains unmigrated; Restart untouched Deterministic tests: 34/34 pass (scenario-form-persistence + investigation-storage) Build: PASS Live Playwright: all criteria verified at /investigations/v060e-live
This commit is contained in:
@@ -4,18 +4,18 @@ import React from "react";
|
|||||||
import { loadInvestigation } from "@/lib/storage/investigation-storage";
|
import { loadInvestigation } from "@/lib/storage/investigation-storage";
|
||||||
import ScenarioForm from "@/components/scenario-form";
|
import ScenarioForm from "@/components/scenario-form";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/navigation";
|
import { useParams, useRouter } from "next/navigation";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
const INVESTIGATION_ID = "case-1";
|
export default function InvestigationPage({ params }) {
|
||||||
|
|
||||||
export default function InvestigationPage() {
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const routeId = typeof params?.id === "string" ? params.id : "";
|
||||||
const [existing, setExisting] = useState(null);
|
const [existing, setExisting] = useState(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setExisting(loadInvestigation());
|
if (!routeId) return;
|
||||||
}, []);
|
setExisting(loadInvestigation(routeId));
|
||||||
|
}, [routeId]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className="mx-auto max-w-[1600px] px-6 py-12">
|
<main className="mx-auto max-w-[1600px] px-6 py-12">
|
||||||
@@ -37,12 +37,14 @@ export default function InvestigationPage() {
|
|||||||
</p>
|
</p>
|
||||||
{existing ? (
|
{existing ? (
|
||||||
<ScenarioForm
|
<ScenarioForm
|
||||||
|
investigationId={routeId}
|
||||||
existingSnapshot={existing}
|
existingSnapshot={existing}
|
||||||
onNavigateToReport={() => router.push(`/investigations/${INVESTIGATION_ID}/report`)}
|
onNavigateToReport={() => router.push(`/investigations/${routeId}/report`)}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<ScenarioForm
|
<ScenarioForm
|
||||||
onNavigateToReport={() => router.push(`/investigations/${INVESTIGATION_ID}/report`)}
|
investigationId={routeId}
|
||||||
|
onNavigateToReport={() => router.push(`/investigations/${routeId}/report`)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
@@ -271,7 +271,7 @@ export async function executeEpisodeDone({
|
|||||||
return { success: true, nextGraph, synthesisResult };
|
return { success: true, nextGraph, synthesisResult };
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ScenarioForm({ onNavigateToReport }) {
|
export default function ScenarioForm({ investigationId, onNavigateToReport }) {
|
||||||
const [scenario, setScenario] = useState("");
|
const [scenario, setScenario] = useState("");
|
||||||
const [status, setStatus] = useState("idle"); // idle | loading | error | success
|
const [status, setStatus] = useState("idle"); // idle | loading | error | success
|
||||||
const [result, setResult] = useState(null);
|
const [result, setResult] = useState(null);
|
||||||
@@ -476,6 +476,7 @@ export default function ScenarioForm({ onNavigateToReport }) {
|
|||||||
|
|
||||||
// Trigger autosave to persist the report
|
// Trigger autosave to persist the report
|
||||||
void saveInvestigation({
|
void saveInvestigation({
|
||||||
|
id: investigationId,
|
||||||
scenario,
|
scenario,
|
||||||
situationGraph: result.situationGraph,
|
situationGraph: result.situationGraph,
|
||||||
selectedQuestion: result.selectedQuestion,
|
selectedQuestion: result.selectedQuestion,
|
||||||
@@ -543,7 +544,7 @@ export default function ScenarioForm({ onNavigateToReport }) {
|
|||||||
/* Restore persisted session on mount ─────────── */
|
/* Restore persisted session on mount ─────────── */
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (typeof window === "undefined") return;
|
if (typeof window === "undefined") return;
|
||||||
const saved = loadInvestigation();
|
const saved = investigationId ? loadInvestigation(investigationId) : null;
|
||||||
if (!saved) return;
|
if (!saved) return;
|
||||||
|
|
||||||
const hasGraph = Boolean(saved.situationGraph);
|
const hasGraph = Boolean(saved.situationGraph);
|
||||||
@@ -580,6 +581,7 @@ export default function ScenarioForm({ onNavigateToReport }) {
|
|||||||
if (!result?.situationGraph) return;
|
if (!result?.situationGraph) return;
|
||||||
|
|
||||||
void saveInvestigation({
|
void saveInvestigation({
|
||||||
|
id: investigationId,
|
||||||
scenario,
|
scenario,
|
||||||
situationGraph: result.situationGraph,
|
situationGraph: result.situationGraph,
|
||||||
selectedQuestion: result.selectedQuestion,
|
selectedQuestion: result.selectedQuestion,
|
||||||
@@ -678,7 +680,7 @@ export default function ScenarioForm({ onNavigateToReport }) {
|
|||||||
setResult(normalised);
|
setResult(normalised);
|
||||||
/* ── v0.59a — provenance: first meaningful change sets revision to 1 ── */
|
/* ── v0.59a — provenance: first meaningful change sets revision to 1 ── */
|
||||||
setInvestigationRevision(1);
|
setInvestigationRevision(1);
|
||||||
saveInvestigation({ scenario, situationGraph: normalised.situationGraph, selectedQuestion: normalised.selectedQuestion, summary: data.summary ?? null, updatedAt: new Date().toISOString(), focusedContributions, findings: [], investigationReport, investigationRevision: 1 });
|
saveInvestigation({ id: investigationId, scenario, situationGraph: normalised.situationGraph, selectedQuestion: normalised.selectedQuestion, summary: data.summary ?? null, updatedAt: new Date().toISOString(), focusedContributions, findings: [], investigationReport, investigationRevision: 1 });
|
||||||
} else {
|
} else {
|
||||||
setStatus("error");
|
setStatus("error");
|
||||||
setCurrentUnderstanding(data.summary ?? null);
|
setCurrentUnderstanding(data.summary ?? null);
|
||||||
@@ -765,7 +767,7 @@ export default function ScenarioForm({ onNavigateToReport }) {
|
|||||||
/* ── v0.59a — provenance: meaningful change advances revision ── */
|
/* ── v0.59a — provenance: meaningful change advances revision ── */
|
||||||
const nextRev = (investigationRevision ?? 0) + 1;
|
const nextRev = (investigationRevision ?? 0) + 1;
|
||||||
setInvestigationRevision(nextRev);
|
setInvestigationRevision(nextRev);
|
||||||
saveInvestigation({ scenario, situationGraph: nextGraph, selectedQuestion: normaliseUpdateSelectedQuestion(outcome.selectedQuestion), summary: currentUnderstanding, updatedAt: new Date().toISOString(), focusedContributions, findings: nextFindings, investigationReport, investigationRevision: nextRev });
|
saveInvestigation({ id: investigationId, scenario, situationGraph: nextGraph, selectedQuestion: normaliseUpdateSelectedQuestion(outcome.selectedQuestion), summary: currentUnderstanding, updatedAt: new Date().toISOString(), focusedContributions, findings: nextFindings, investigationReport, investigationRevision: nextRev });
|
||||||
} else {
|
} else {
|
||||||
setUpdateStatus("error");
|
setUpdateStatus("error");
|
||||||
setUpdateError(outcome);
|
setUpdateError(outcome);
|
||||||
|
|||||||
+36
-1
@@ -381,7 +381,42 @@ Migrate existing application callers to the identity-aware contract signatures:
|
|||||||
|
|
||||||
**Status:** UI/routes are **not** migrated. All existing callers continue via the singleton compatibility path (they call `saveInvestigation({ ... })` with no explicit second parameter, and their snapshots carry no `id` field). No production caller was modified in this increment.
|
**Status:** UI/routes are **not** migrated. All existing callers continue via the singleton compatibility path (they call `saveInvestigation({ ... })` with no explicit second parameter, and their snapshots carry no `id` field). No production caller was modified in this increment.
|
||||||
|
|
||||||
**Next restart point:** Caller migration — update application consumers to pass investigation ID through save calls so canonical identity-aware semantics activate for all writes.
|
## v0.60e — Route-Owned Investigation Identity
|
||||||
|
|
||||||
|
**Purpose:** Migrate the Investigation page route to own durable investigation identity via its route `[id]` segment, passing that ID through to ScenarioForm for hydration and persistence without migrating legacy singleton data or changing Portfolio/Report behaviour.
|
||||||
|
|
||||||
|
### What was implemented
|
||||||
|
|
||||||
|
| File | Change |
|
||||||
|
|---|---|
|
||||||
|
| `app/investigations/[id]/page.jsx` | Removed hardcoded `INVESTIGATION_ID = "case-1"` constant; route `[id]` param extracted as `routeId` via `params.id`; `loadInvestigation(routeId)` loads by route identity; `investigationId={routeId}` passed to ScenarioForm in both branch paths; report navigation uses `routeId`. |
|
||||||
|
| `components/scenario-form.jsx` | Added `investigationId` prop; session restore calls `loadInvestigation(investigationId)` when provided; all 4 save call sites include `id: investigationId` in snapshot (autosave effect, start-case submit, update-case submit, report overview autosave). |
|
||||||
|
|
||||||
|
### Contract crossings verified by deterministic test
|
||||||
|
|
||||||
|
- **Owning test files:** `tests/storage/scenario-form-persistence.test.js` (11 tests) + `tests/storage/investigation-storage.test.js` (23 tests, including v0.60d canonical identity section)
|
||||||
|
- All 34 tests pass on first run; no reruns required
|
||||||
|
|
||||||
|
### Verified behaviour
|
||||||
|
|
||||||
|
- Route ID is passed from `[id]/page.jsx` into ScenarioForm as `investigationId`: **YES**
|
||||||
|
- Missing identified Investigation (`loadInvestigation("v060e-live")` → null) starts clean: **YES**
|
||||||
|
- Legacy singleton fallback used: **NO** — no backward-compat load was needed; the route ID was absent from storage
|
||||||
|
- First meaningful saved snapshot carries route ID: **YES** (all 4 save sites inject `id: investigationId`)
|
||||||
|
- Subsequent save identity preserved: **YES** (storage layer uses `snapshot.id` as sole identity authority)
|
||||||
|
- Storage provider changed: **NO** — only `investigation-storage.js` wrapper, already-proved in v0.60d
|
||||||
|
|
||||||
|
### Portfolio
|
||||||
|
|
||||||
|
Not migrated. `app/page.jsx` retains hardcoded `INVESTIGATION_ID = "case-1"`. Create New navigation to a durable-ID route is later work.
|
||||||
|
|
||||||
|
### Report
|
||||||
|
|
||||||
|
Not migrated. `app/investigations/[id]/report/page.jsx` untouched. Remains a later caller migration increment.
|
||||||
|
|
||||||
|
### Restart
|
||||||
|
|
||||||
|
Untouched in this increment. All `clearInvestigation()` calls remain without an id argument (legacy singleton path). If identity-aware restart is needed, the next increment should pass `investigationId` through those clear calls.
|
||||||
|
|
||||||
## Next implementation boundary
|
## Next implementation boundary
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user