// ── canonical identifiers ──────────────────────────────────────────── const CANONICAL_KEY = "confidence-engine-investigation"; const LEGACY_KEY = "confidence-engine-session"; const SCHEMA_VERSION = 1; import { restartSnapshot } from "../restart-investigation.js"; // Multi-Investigation key prefix (v0.60c) const INVESTIGATION_PREFIX = "confidence-engine-investigation:"; // ── helpers ────────────────────────────────────────────────────────── function safeGet(storage, key) { if (!storage || typeof storage.getItem !== "function") return null; try { return storage.getItem(key); } catch (_) { return null; } } function isPlainObject(value) { return ( value !== null && typeof value === "object" && !Array.isArray(value) && Object.prototype.toString.call(value) === "[object Object]" ); } // ── loadInvestigation (identity-aware) ─────────────────────────────── /** * Returns the persisted investigation snapshot keyed by durable id * (normalised to current schema), or null when no usable state exists. * * When called without an id argument, reads from the legacy singleton key * for backward-compatible consumers that have not yet migrated. */ export function loadInvestigation(id) { const storage = _getTargetStorage(); if (!storage) return null; // — identity-aware contract: select by durable id — if (id != null) { const raw = safeGet(storage, `${INVESTIGATION_PREFIX}${id}`); if (raw === null) return null; try { const parsed = JSON.parse(raw); if (isPlainObject(parsed)) { if (!("schemaVersion" in parsed)) parsed.schemaVersion = SCHEMA_VERSION; return parsed; } } catch (_) { return null; } return null; } // — backward-compatible singleton path (no id supplied) — // 1 – canonical key first let raw = safeGet(storage, CANONICAL_KEY); if (raw !== null) { try { const parsed = JSON.parse(raw); if (isPlainObject(parsed)) { if (!("schemaVersion" in parsed)) parsed.schemaVersion = SCHEMA_VERSION; return parsed; } } catch (_) { /* malformed — fall through to legacy */ } } // 2 – legacy fallback const legacyStorage = _getLegacyStorage(); if (!legacyStorage) return null; raw = safeGet(legacyStorage, LEGACY_KEY); if (raw === null) return null; let parsed; try { parsed = JSON.parse(raw); } catch (_) { return null; } if (!isPlainObject(parsed)) return null; // Normalise schemaVersion for legacy data if (!("schemaVersion" in parsed)) parsed.schemaVersion = SCHEMA_VERSION; // Migrate into canonical key _persist(storage, CANONICAL_KEY, JSON.stringify(parsed)); return parsed; } // ── saveInvestigation (identity-aware) ─────────────────────────────── /** * Persists the supplied snapshot under its durable id key. * The caller's object is never mutated. * * When called without an id argument, writes to the legacy singleton key * for backward-compatible consumers that have not yet migrated. */ export function saveInvestigation(snapshot, id) { const storage = _getTargetStorage(); if (!storage) return; // silently no-op in non-browser try { const record = JSON.parse(JSON.stringify(snapshot)); record.schemaVersion = SCHEMA_VERSION; const key = id != null ? `${INVESTIGATION_PREFIX}${id}` : CANONICAL_KEY; _persist(storage, key, JSON.stringify(record)); } catch (_) { /* storage errors must not crash caller */ } } // ── clearInvestigation (identity-aware) ────────────────────────────── /** * Removes a specific investigation by durable id. * When called without an id, clears the legacy singleton keys only. */ export function clearInvestigation(id) { const storage = _getTargetStorage(); if (!storage) return; if (id != null) { try { storage.removeItem(`${INVESTIGATION_PREFIX}${id}`); } catch (_) {} } else { try { storage.removeItem(CANONICAL_KEY); } catch (_) {} const legacyStorage = _getLegacyStorage(); if (legacyStorage) { try { legacyStorage.removeItem(LEGACY_KEY); } catch (_) {} } } } // ── listInvestigations (durable-ID only) ─────────────────────── /** * Enumerates all durable-ID Investigation records and returns lightweight summaries. * Skips malformed entries; excludes legacy singleton, sessionStorage state, unrelated keys. */ export function listInvestigations() { const storage = _getTargetStorage(); if (!storage) return []; const summaries = []; for (let i = 0; i < storage.length; i++) { const key = storage.key(i); if (!key || !key.startsWith(INVESTIGATION_PREFIX)) continue; try { const raw = safeGet(storage, key); if (!raw) continue; const record = JSON.parse(raw); if (!isPlainObject(record)) continue; if (!record.id) continue; summaries.push({ id: record.id, scenario: record.scenario ?? null, updatedAt: record.updatedAt ?? null, investigationRevision: record.investigationRevision ?? 0, reportExists: !!record.investigationReport, reportGeneratedFromRevision: record.investigationReport ? record.investigationReport.generatedFromRevision ?? null : null, }); } catch (_) { // Malformed durable-ID entry — skip silently } } summaries.sort((a, b) => { const ta = a.updatedAt ?? ""; const tb = b.updatedAt ?? ""; if (ta > tb) return -1; if (ta < tb) return 1; return a.id > b.id ? -1 : 1; }); return summaries; } // ── restartInvestigation (semantic reset within container) ─────────── /** * Resets the Investigation to a clean state suitable for a new reasoning pass. * * Preserved: id, scenario, schemaVersion (container-level identity/framing). * Reset: situationGraph → null, selectedQuestion → null, summary → null, * focusedContributions → [], findings → [], investigationReport → null, * investigationRevision → 0, updatedAt → new ISO timestamp. */ export function restartInvestigation(id) { const storage = _getTargetStorage(); if (!storage) return; try { const key = id != null ? `${INVESTIGATION_PREFIX}${id}` : CANONICAL_KEY; const raw = safeGet(storage, key); if (raw === null) return; // nothing to restart const record = JSON.parse(raw); if (!isPlainObject(record)) return; _persist(storage, key, JSON.stringify(restartSnapshot(record))); } catch (_) { /* storage errors must not crash caller */ } } // ── internals ──────────────────────────────────────────────────────── function _persist(storage, key, value) { try { storage.setItem(key, value); } catch (_) {} } function _getTargetStorage() { if (typeof globalThis.window === "undefined") return null; const s = globalThis.window.localStorage; if (!s || typeof s.getItem !== "function") return null; return s; } function _getLegacyStorage() { if (typeof globalThis.window === "undefined") return null; const s = globalThis.window.sessionStorage; if (!s || typeof s.getItem !== "function") return null; return s; }