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
+65
View File
@@ -284,6 +284,71 @@ The storage contract accepts/returns domain-level Investigation objects keyed by
Storage contract = persistence only. Not synchronization. No sync-specific fields (syncStatus, remoteId, dirtyFlags, lastSyncedAt) belong in the Investigation shape during this increment. Decisions here do not prevent future sync — the Investigation object carries its own durable ID sufficient for identity resolution.
## v0.60c — Identity-Aware Investigation Storage Implementation
**First production implementation increment of v0.60.** Bounded capability: two independently addressable Investigations can be persisted and loaded through the storage contract.
### What was implemented
- `loadInvestigation(id)` — accepts optional durable ID; selects by that identity when provided; returns `null` for unknown IDs
- `saveInvestigation(investigation, id)` — accepts optional durable ID; persists under provider-chosen key derived from `id`; does NOT allocate or replace the supplied ID
- `clearInvestigation(id)` — accepts optional durable ID; removes specific investigation by identity when provided
### Representation chosen (internal to localStorage provider)
Each Investigation is stored as a separate top-level localStorage key:
```
confidence-engine-investigation:<durable-id>
```
e.g. `confidence-engine-investigation:inv-abc123`
This was the smallest sufficient representation because:
- Direct key lookup provides O(1) per-investigation access without needing an index
- No generic repository layer required — each key is independently addressable by its durable ID
- The two-Investigation proof requires independent storage and retrieval, which this achieves with zero indexes or aggregation
Representation is **not exposed** to callers — the contract does not reveal localStorage key structure.
### Two-Investigation proof results (deterministic test)
| Invariant | Result |
|---|---|
| saveInvestigation(A) persists A under A.id | ✅ PASS |
| saveInvestigation(B) persists B independently under B.id | ✅ PASS |
| saving B does not overwrite A | ✅ PASS |
| loadInvestigation(A.id) returns A | ✅ PASS |
| loadInvestigation(B.id) returns B | ✅ PASS |
| loadInvestigation(unknownId) returns null | ✅ PASS |
| saveInvestigation() does not invent/change supplied ID | ✅ PASS |
### Singleton compatibility seam
- Existing consumers call `loadInvestigation()` and `clearInvestigation()` **without** an id argument — these continue via the legacy singleton path (canonical key + sessionStorage fallback)
- No caller was refactored in this increment
- The compatibility seam is: functions accept optional second parameter; when absent, behaviour matches pre-v0.60c singleton semantics
- `case-1` is **NOT** introduced as canonical identity — it exists only in existing consumer route URLs and legacy data
### Legacy migration
- No legacy singleton → new durable ID migration policy was invented
- Existing `case-1` localStorage data continues to be served by the backward-compatible path
- Migration of existing consumers to the new identity-aware calls is deferred to a later increment
### Production files changed
| File | Purpose |
|---|---|
| `lib/storage/providers/local-storage.js` | Identity-aware storage contract implementation |
| `tests/storage/investigation-storage.test.js` | 6 new targeted tests for v0.60c invariants |
### Next restart point
Migrate existing application callers to the identity-aware contract signatures:
1. `[id]/page.jsx` — pass route `[id]` to `loadInvestigation(id)`
2. `[id]/report/page.jsx` — pass route `[id]` to `loadInvestigation(id)` and `saveInvestigation(..., id)`
3. `scenario-form.jsx` — pass investigation ID through save calls
4. `app/page.jsx` — migrate Portfolio to use `listInvestigations()` (next increment)
## Next implementation boundary
Smallest next increment: implement the four-operation storage contract in `lib/storage/investigation-storage.js` as a re-export of a provider-backed interface whose signatures accept/return domain Investigation objects keyed by durable ID — without committing to any specific localStorage or database representation. This means defining the exported function signatures and the Investigation shape that flows through them, while deferring key scheme, row schema, and collection structure to a later implementation decision.