Files
confidence-engine/lib/storage/providers/local-storage.js

232 lines
7.8 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// ── canonical identifiers ────────────────────────────────────────────
const CANONICAL_KEY = "confidence-engine-investigation";
const LEGACY_KEY = "confidence-engine-session";
const SCHEMA_VERSION = 1;
// 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;
// Preserve container fields, reset reasoning-state fields
record.situationGraph = null;
record.selectedQuestion = null;
record.summary = null;
record.focusedContributions = [];
record.findings = [];
record.investigationReport = null;
record.investigationRevision = 0;
record.updatedAt = new Date().toISOString();
_persist(storage, key, JSON.stringify(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;
}