diff --git a/app/investigations/[id]/page.jsx b/app/investigations/[id]/page.jsx index ebd8ce7..8f945d2 100644 --- a/app/investigations/[id]/page.jsx +++ b/app/investigations/[id]/page.jsx @@ -4,18 +4,18 @@ import React from "react"; import { loadInvestigation } from "@/lib/storage/investigation-storage"; import ScenarioForm from "@/components/scenario-form"; import Link from "next/link"; -import { useRouter } from "next/navigation"; +import { useParams, useRouter } from "next/navigation"; import { useEffect, useState } from "react"; -const INVESTIGATION_ID = "case-1"; - -export default function InvestigationPage() { +export default function InvestigationPage({ params }) { const router = useRouter(); + const routeId = typeof params?.id === "string" ? params.id : ""; const [existing, setExisting] = useState(null); useEffect(() => { - setExisting(loadInvestigation()); - }, []); + if (!routeId) return; + setExisting(loadInvestigation(routeId)); + }, [routeId]); return (
@@ -37,12 +37,14 @@ export default function InvestigationPage() {

{existing ? ( router.push(`/investigations/${INVESTIGATION_ID}/report`)} + onNavigateToReport={() => router.push(`/investigations/${routeId}/report`)} /> ) : ( router.push(`/investigations/${INVESTIGATION_ID}/report`)} + investigationId={routeId} + onNavigateToReport={() => router.push(`/investigations/${routeId}/report`)} /> )}
diff --git a/components/scenario-form.jsx b/components/scenario-form.jsx index d0121e9..911fb4d 100644 --- a/components/scenario-form.jsx +++ b/components/scenario-form.jsx @@ -271,7 +271,7 @@ export async function executeEpisodeDone({ return { success: true, nextGraph, synthesisResult }; } -export default function ScenarioForm({ onNavigateToReport }) { +export default function ScenarioForm({ investigationId, onNavigateToReport }) { const [scenario, setScenario] = useState(""); const [status, setStatus] = useState("idle"); // idle | loading | error | success const [result, setResult] = useState(null); @@ -476,6 +476,7 @@ export default function ScenarioForm({ onNavigateToReport }) { // Trigger autosave to persist the report void saveInvestigation({ + id: investigationId, scenario, situationGraph: result.situationGraph, selectedQuestion: result.selectedQuestion, @@ -543,7 +544,7 @@ export default function ScenarioForm({ onNavigateToReport }) { /* Restore persisted session on mount ─────────── */ useEffect(() => { if (typeof window === "undefined") return; - const saved = loadInvestigation(); + const saved = investigationId ? loadInvestigation(investigationId) : null; if (!saved) return; const hasGraph = Boolean(saved.situationGraph); @@ -580,6 +581,7 @@ export default function ScenarioForm({ onNavigateToReport }) { if (!result?.situationGraph) return; void saveInvestigation({ + id: investigationId, scenario, situationGraph: result.situationGraph, selectedQuestion: result.selectedQuestion, @@ -678,7 +680,7 @@ export default function ScenarioForm({ onNavigateToReport }) { setResult(normalised); /* ── 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 }); + saveInvestigation({ id: investigationId, 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); @@ -765,7 +767,7 @@ export default function ScenarioForm({ onNavigateToReport }) { /* ── 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 }); + saveInvestigation({ id: investigationId, scenario, situationGraph: nextGraph, selectedQuestion: normaliseUpdateSelectedQuestion(outcome.selectedQuestion), summary: currentUnderstanding, updatedAt: new Date().toISOString(), focusedContributions, findings: nextFindings, investigationReport, investigationRevision: nextRev }); } else { setUpdateStatus("error"); setUpdateError(outcome); diff --git a/docs/current-handoff.md b/docs/current-handoff.md index 400a2d8..792c9da 100644 --- a/docs/current-handoff.md +++ b/docs/current-handoff.md @@ -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. -**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