feat(confidence-engine): v0.60c identity-aware investigation storage

- loadInvestigation(id) selects by durable ID when provided, null for unknown
- saveInvestigation(snapshot, id) persists under provider-chosen key derived from id
- clearInvestigation(id) removes specific investigation by identity when provided
- localStorage representation: confidence-engine-investigation:<durable-id>
- Backward-compatible singleton path preserved for existing unmigrated callers
- 6 new targeted tests proving two independently addressable Investigations
This commit is contained in:
2026-09-03 18:09:03 +01:00
parent f23d442eb3
commit 8c85120b1c
3 changed files with 220 additions and 18 deletions
+48 -18
View File
@@ -4,6 +4,9 @@ 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) {
@@ -20,16 +23,34 @@ function isPlainObject(value) {
);
}
// ── loadInvestigation ────────────────────────────────────────────────
// ── loadInvestigation (identity-aware) ───────────────────────────────
/**
* Returns the persisted investigation snapshot (normalised to current schema)
* or null when no usable state exists.
* 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() {
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) {
@@ -64,36 +85,45 @@ export function loadInvestigation() {
return parsed;
}
// ── saveInvestigation ────────────────────────────────────────────────
// ── saveInvestigation (identity-aware) ───────────────────────────────
/**
* Persists the supplied snapshot to the canonical localStorage key.
* 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) {
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;
_persist(storage, CANONICAL_KEY, JSON.stringify(record));
const key = id != null ? `${INVESTIGATION_PREFIX}${id}` : CANONICAL_KEY;
_persist(storage, key, JSON.stringify(record));
} catch (_) { /* storage errors must not crash caller */ }
}
// ── clearInvestigation ───────────────────────────────────────────────
// ── clearInvestigation (identity-aware) ──────────────────────────────
/** Removes the canonical investigation key and the legacy session key. */
export function clearInvestigation() {
/**
* 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;
try { storage.removeItem(CANONICAL_KEY); } catch (_) {}
// Also remove the legacy sessionStorage key so it cannot resurrect stale
// state after loadInvestigation falls through from missing canonical key.
const legacyStorage = _getLegacyStorage();
if (!legacyStorage) return;
try { legacyStorage.removeItem(LEGACY_KEY); } catch (_) {}
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 (_) {}
}
}
}
// ── internals ────────────────────────────────────────────────────────