Files
confidence-engine/lib/storage/providers/local-storage.js
T
robbond 7ba1771bcf feat(confidence-engine): v0.60g1 list investigation summaries
Recover to clean v0.60f then implement only the storage listing contract.

- Add listInvestigations() to provider: enumerate by prefix, project lightweight summary (id, scenario, updatedAt, investigationRevision, reportExists, reportGeneratedFromRevision), sort by updatedAt desc
- Add application-facing wrapper in investigation-storage.js
- Add 8 deterministic tests covering all listing invariants (coexistence, correct IDs, lightweight projection, legacy exclusion, unrelated exclusion, independent update, ordering, malformed skip)
- Fix MockStorageMap WebStorage API compatibility (.length + .key(i))
- Portfolio NOT migrated — that is v0.60g2
2026-09-04 06:45:26 +01:00

197 lines
6.5 KiB
JavaScript
Raw 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;
}
// ── 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;
}