Files
confidence-engine/docs/current-handoff.md
T
robbond 8c85120b1c 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
2026-09-03 18:09:03 +01:00

22 KiB
Raw Blame History

Current Handoff — Confidence Engine

Role: Concise operational snapshot for resuming work today. Not a historical diary. The design evolution archive index at docs/design-evolution/README.md provides progressive loading of experiment history; load the relevant chapter only when a specific historical question requires it.

Repository checkpoint

  • Branch: feature/investigation-report-v0.55
  • HEAD: 37a9a12 — route design evolution provenance through archive index
  • Working tree: will be clean after v0.59a commit

Current product architecture

Three distinct routes, not a single page:

/                              → Portfolio (notebook index)
/investigations/case-1          → Investigation (working case/pages)
/investigations/case-1/report   → Investigation Report (readable derived summary)

Portfolio = investigator notebook index. Shows the single canonical persisted investigation card with actions: View report, Continue investigation, Restart investigation. Below the card: + Create new investigation (portfolio-level, not inside the card).

Investigation = working case. Contains ScenarioForm + ReasoningWorkspace. Handles graph reasoning, focused investigation turns, Done/Re-open semantics, Current Understanding synthesis. No Report presentation — that is owned by the dedicated Report route.

Report = derived artefact. Renders persisted investigationReport snapshot. Generation is on-demand, triggered by the Report page itself (not ReasoningWorkspace). Exactly one /api/cases/overview call on first visit; zero calls on subsequent visits. The Report is not canonical reasoning evidence — it is a derived summary for review/export/use.

Current working product journey

Portfolio (/)
  → "Continue investigation"
    → Investigation page (/investigations/case-1)
      → Focused question asked → user answers → Done for now
      → Current Understanding synthesizes
      → If zero Open Questions: "Review current understanding" appears
        OR → "Review current understanding"
          → Report page (/investigations/case-1/report)
            → Generates via /api/cases/overview (once)
            → Persists investigationReport
            → Subsequent visits render persisted snapshot (zero calls)

Restart flow

Portfolio card Restart investigation → confirmation dialog (title: "Restart this investigation?") → destructive second confirmation → clearInvestigation() (canonical localStorage clear seam). No direct storage-key manipulation.

Current reasoning / ownership invariants

Evidence discipline:

RAW USER EVIDENCE
≠ MODEL-DERIVED CONTRIBUTION SEMANTICS
≠ CURRENT CANONICAL FINDING
≠ IMMUTABLE SOURCE OBSERVATION
≠ USER DISPOSITION / AUTHORITY
≠ TURN CONTEXT / PROVENANCE

Finding dispositions: null (eligible working premise), agree (user-endorsed), not_relevant (excluded from eligible reasoning, provenance retained), corrected Finding. User disposition never directly mutates authoritative graph state.

Evidence distinctions preserved by the persistence layer. Contributions preserve: question/context, verbatim answer, model observations, uncertainties, assumptions, relationships, follow-ups, target/provenance, sequence/order. Findings preserve: canonical proposition, sourceObservation, contributionId, originatingTargetNodeId, userDisposition.

Focused investigation presentation ownership (v0.52)

FocusedQuestionBody derives a thread-local subset (targetNodeId || originatingTargetNodeId) for every presentation surface. Previously answered content does NOT bleed from one question to another. Verified manually and by targeted Vitest.

Empty Done + Re-open semantics (v0.53)

  • Empty Done is valid: parks/resolves the question locally, does NOT invoke episode processing, does NOT produce no_episodic_content 400.
  • Re-open returns the question to Open Questions and removes from doneForNowIds.
  • Older stale development localStorage states (pre-v0.53 shape) may be discarded during current dev phase. No migration required.

v0.59a — Investigation revision provenance rule

  • Investigation has a semantic revision (investigationRevision).
  • Meaningful persisted Investigation changes advance it by exactly 1.
  • Persistence activity itself (autosave, save) does NOT advance revision.
  • Report generation records generatedFromRevision: investigationRevision.
  • Report generation advances revision by 0.
  • Equal revisions mean Report reflects current Investigation.
  • Different revisions mean the Investigation has changed since Report generation.
  • Existing Report remains available (not invalidated).
  • Report update/regeneration remains manual (user-triggered).
  • Restart clears Investigation + Report via clearInvestigation() + setInvestigationRevision(0).
  • Report history/comparison remains deferred beyond MVP.

v0.59b — Report freshness UI on the Report page

  • Report page shows Current (matching revisions) or Update available (differing revisions).
  • Manual Update report action on Report page triggers regeneration via /api/cases/overview.
  • Duplicate prevention guard during in-flight update.
  • No automatic regeneration.
  • Explanation copy: "The investigation has changed since this report was generated."

v0.59c — Report freshness state surfaced on the Portfolio

  • Portfolio surfaces Current / Update available alongside existing View report link.
  • Derives freshness solely from revision provenance (investigationReport.generatedFromRevision === investigationRevision).
  • Zero model calls during Portfolio render.
  • No mutation of Investigation.
  • No Update report action on the Portfolio — manual Report updating remains owned by the Report page.
  • If no Report, neither freshness state is shown.

Semantic transitions that advance revision:

Episode Done (with content)          → +1
Re-open (resolved → unknown)         → +1
Finding proposition correction       → +1
Eligible → not_relevant              → +1
not_relevant → eligible (restore)    → +1
First meaningful change              → =1 (from initial 0)

Transitions that do NOT advance revision:

Autosave                             → 0
Report generation                    → 0
Hydration                            → 0 (reads value)
Empty proposition correction (no-op) → 0
null → null disposition (no-op)      → 0

Zero Open Questions milestone (v0.51)

When all unknowns are resolved and clarified questions exist: "You've now worked through all of the questions we surfaced. Would you like to see an overview of what we understand so far?" with "Review current understanding" button. This occupies the former Open Questions position. The invitation is a milestone, not a readiness/completion judgement.

Current Understanding refresh invariants

Reconstruct CU when canonical meaning or eligible evidence set changes — NOT when investigation/question status changes alone. Re-open ≠ change what we understand; Finding correction / Not Relevant / completed episode = change what we understand.

Persistence and Report ownership

  • Canonical persistence owner: lib/storage/providers/local-storage.js (saveInvestigation / loadInvestigation). All routes read from the same snapshot.
  • Report generation owner: Report page only (NOT ReasoningWorkspace, NOT Investigation page).
  • localStorage key: confidence-engine-investigation (single canonical key — multi-investigation not yet implemented).
  • Temporary identity: case-1. True multi-investigation persistence/identity is future work.
  • Portfolio client hydration: Portfolio page uses 'use client' — initial pre-hydration empty state ≠ absence of persisted data. Always wait for hydrated semantic controls before classifying state.

MVP boundaries (v0.59b)

Implemented in MVP:

  • visible Report freshness state (Current / Update available)
  • explanation that the Investigation has changed since Report generation
  • manual Update report action (user-triggered)
  • no automatic regeneration
  • duplicate prevention guard during in-flight update

Deferred beyond MVP:

  • Report history
  • retaining multiple Reports
  • Report comparison
  • modelling/preview comparison between Investigation revisions

Current development / verification constraints

  • Canonical dev server at http://localhost:3000. Never start/stop/restart/probe it. If unavailable → BLOCKED and stop.
  • Playwright MCP: use Run Playwright code with semantic locators for known controls. Snapshot refs ([ref=...]) are observational only.
  • For async/hydration states: use waitFor({ state: 'visible', timeout }) — not arbitrary sleeps.
  • If a prescribed semantic locator cannot find its control → STOP. No fallback to CSS/XPath/DOM traversal.
  • Live freeze: once Playwright verification begins, no production file edits until evidence is captured and classified.
  • Tests are instruments, not product truth. At first deterministic failure: classify PRODUCT vs APPARATUS failure, then stop. Do not enter test-harness repair loops.
  • Mocked boundary ownership: if a lower-layer function is mocked, test the value crossing the seam — do not require the mock to reproduce real implementation.

Current limitations / genuinely open boundaries

Not yet implemented:

  • Multi-investigation portfolio (search/tag/archive/group)
  • Durable investigation identities beyond case-1
  • Export/copy of Reports to Jira or external document
  • Portfolio expansion beyond one canonical investigation
  • Report history / comparison

Known boundaries:

  • Current Understanding and Investigation Report are architecturally distinct artefacts. Plausible interpretations in the Report remain explicitly interpretive, not evidence.
  • The ≤5 processing bound observed during development was an experimental apparatus constraint, NOT a product requirement. Six Open Questions surfacing is legitimate product output, not a formulation defect.
  • Evidence discipline: what proves useful in live experimentation must be captured at provenance level, not as test diary entries that become operational constraints.

v0.60 — Multi-Investigation Storage Contract Decision

Completed on branch feature/multi-investigation-storage-v0.60. Bounded architecture/contract decision task following v0.60 audit. Zero production changes. Documentation-only decision checkpoint.

1. Contract ownership

Decision: lib/storage/investigation-storage.js is the application-facing persistence boundary. It defines the contract whose semantics (not just signatures) are immutable across provider implementations. Consumers import only from this module.

lib/storage/providers/local-storage.js remains one concrete provider implementation behind the contract, not an application import target.

Invariant: No consumer imports providers/local-storage directly.

2. Durable Investigation identity

Decision: Canonical Investigation carries a durable id field.

Property Value
ID allocation owner Application (not storage contract)
Allocation timing On create new, before any persistence call
Mutable after creation NO
Route [id] represents same identity YES — direct round-trip: app sets → route reads → storage uses

3. Minimum contract operations — resolved in v0.60b

Four semantic operations. createEmptyInvestigation() does not belong to the storage contract (resolved below).

# Operation Input Output Product use
1 listInvestigations() InvestigationSummary[] (lightweight) Portfolio renders index/list
2 loadInvestigation(id) durable ID string Investigation | null Investigation/Report page hydration
3 saveInvestigation(investigation) full snapshot (with id already present) void Autosave, Report generation, all writes
4 restartInvestigation(id) durable Investigation ID void (reasoning state cleared) "Restart this investigation"

Explicitly rejected: search, pagination, sorting, sync, merge, transactions, history, report versions, sharing, permissions, accounts. No existing product behaviour requires them.


3b. Investigation creation ownership — v0.60b resolution

Contract correction decision. The v0.60a contract contained a contradiction: Section 2 stated "ID allocation owner: Application" while Section 3 listed createEmptyInvestigation() as a storage operation producing { id: string }. These are incompatible models (application-owned identity vs. storage-owned creation). They cannot both be true.

Evidence from current product:

  1. Create new behaviour today: Portfolio's "+ Create new investigation" is a <Link href="/investigations/case-1"> — a static navigation link with zero ID allocation and zero storage call. It navigates to the Investigation route which calls loadInvestigation(), receives null (no persisted data), and shows an empty ScenarioForm state. No empty Investigation is persisted on click.

  2. First meaningful persistence event: scenario-form.jsx line 681 — after submitScenarioForStartCase returns successfully. The user has selected a scenario, submitted it, and the application receives a structured result. Only then does saveInvestigation() persist with real data (scenario + situationGraph + investigationRevision: 1). This is a domain-driven persistence boundary, not an identity-driven one.

  3. No product requirement for empty-persisted Investigations: There is no current product behaviour that creates, displays, or expects empty persisted Investigations in Portfolio. The concept of persisting an abandoned start as a visible Portfolio item was asserted in v0.60a without product evidence.

  4. No product requirement for storage to allocate identity: Current code uses hardcoded INVESTIGATION_ID = "case-1" in application code with no storage involvement in ID generation or allocation.

Decision: MODEL A wins — application-owned identity, deferred persistence.

  • ID allocation owner: Application layer (not storage contract).
  • When allocated: On user click of "+ Create new investigation", before navigation.
  • First persistence: When the user produces meaningful Investigation state (scenario submitted) and saveInvestigation(investigation) is called — not on create-new click.
  • Empty abandoned Investigation persisted: NO. No durable Investigation exists until meaningful state triggers save.
  • Storage allocates identity: NO.

createEmptyInvestigation() does not belong in the storage contract. It never did — it was inconsistent with Section 2's "Application (not storage)" decision. The creation flow lives entirely at the application layer: app allocates durable ID → navigates to /investigations/{id} → user produces state → saveInvestigation(investigation) persists.

Create new investigation flow (corrected):

  1. User clicks "+ Create new investigation" on Portfolio.
  2. Application allocates a durable immutable ID (library/algorithm deferred).
  3. Application navigates to /investigations/{id}.
  4. User fills out ScenarioForm → submits → first meaningful saveInvestigation(investigation) persists the Investigation under its already-known ID. The Investigation becomes listable in Portfolio at that point.

Investigation properties (unchanged from v0.60a):

  • Carries durable id: YES
  • ID immutable: YES
  • ID exists before first persistence: YES

Restart decision retained: YES — identity preserved, reasoning/report state cleared.**

4. Portfolio listing returns lightweight metadata

listInvestigations() returns summaries, not full snapshots. Minimum information:

  • id — navigation target
  • scenario (or title framing) — card text
  • updatedAt — freshness display
  • investigationRevision — revision metadata
  • Report existence and generatedFromRevision — "Current" / "Update available" indicator

Full Investigation loads separately via loadInvestigation(id) on navigation.

5. Restart = reset reasoning state, NOT remove

Restart clears reasoning state (findings, questions, report) within the Investigation but preserves the Investigation's durable identity and container. The Investigation remains visible in Portfolio listing as a named item with cleared state.

A separate "Delete investigation" UI is not required by current MVP. Contract-level removeInvestigation(id) is acceptable for future use but has no product owner yet.

6. Create new investigation flow

User clicks "+ Create new investigation" → application allocates durable immutable ID (library/algorithm deferred) → navigates to /investigations/{id} → Investigation page loads, loadInvestigation(id) returns null (no persisted state yet) → ScenarioForm renders in empty-start mode → user fills scenario and submits → first meaningful state is captured → saveInvestigation(investigation) persists the full snapshot under its already-known ID → Investigation becomes listable in Portfolio. No empty Investigation is persisted at creation time.

7. localStorage ≠ contract

The storage contract accepts/returns domain-level Investigation objects keyed by id. How each provider maps those to its storage mechanism is implementation detail. Contract does not dictate key scheme, row schema, or collection structure.

8. Synchronization separate

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.

Next restart point

Consult docs/design-evolution/README.md for progressive loading of product reasoning and provenance chronology; load the relevant chapter only when a specific historical question requires it.

The current handoff captures all operational facts needed to resume today. For historical decisions, experiment evidence, or methodology evolution — consult the design evolution archive index or task-context packs as appropriate.

Provenance pointers

Need Read
Product evolution v0.51v0.58 docs/design-evolution/README.md (progressive loading)
Methodology / Return-to-Origin axioms docs/current-working-principles.md §0 (A1A12)
Architecture guardrails .claude/architecture-guardrails.md
Task routing by work type docs/task-context-packs.md
Broader architectural intent docs/architectural-principles.md
Experiment history (specific) docs/design-evolution/README.md → relevant chapter