Compare commits

...
Author SHA1 Message Date
robbond 719a65b3d3 Merge pull request 'Feature/product platform foundation v0.62' (#1) from feature/product-platform-foundation-v0.62 into feature/emergent-unknowns-v0.5
Reviewed-on: #1
2026-09-09 07:58:20 +01:00
robbond e6c78f87fe build(confidence-engine): add production container packaging 2026-09-09 06:35:15 +01:00
robbond d6df1d210e feat(confidence-engine): use server investigation persistence 2026-09-08 19:21:05 +01:00
robbond 6dd447e56a feat(confidence-engine): add authenticated investigation persistence 2026-09-08 17:15:46 +01:00
robbond b949eea831 feat(confidence-engine): define investigation persistence schema 2026-09-08 16:56:07 +01:00
robbond 30bf44f2e5 feat(confidence-engine): establish authenticated product boundary 2026-09-08 16:30:07 +01:00
robbond 1c17452bee feat(confidence-engine): add dark mode 2026-09-08 11:04:49 +01:00
robbond 24d9e466f6 docs(confidence-engine): checkpoint commercially testable product loop 2026-09-08 10:40:21 +01:00
robbond 85b9f4411f fix(confidence-engine): reopen resolved unknowns by graph state 2026-09-08 09:42:46 +01:00
robbond 0b5a38f83d fix(confidence-engine): persist done-for-now episode closure 2026-09-07 18:36:38 +01:00
robbond 7af708159e docs(confidence-engine): record Terra journey boundary fixes 2026-09-07 15:51:18 +01:00
robbond 949a7024b3 fix(confidence-engine): allow no-op episode reconsideration 2026-09-07 15:50:50 +01:00
robbond ae00e70ced fix(confidence-engine): unwrap synthesis provider response 2026-09-07 15:50:50 +01:00
robbond 7548a6af59 fix(confidence-engine): supply synthesis output schema 2026-09-07 13:44:57 +01:00
robbond 3f2e2e05ae fix(confidence-engine): define focused relationship contract 2026-09-07 13:28:38 +01:00
robbond 14630cf6b7 experiment(confidence-engine): trace focused deconstruction failures 2026-09-07 13:08:08 +01:00
robbond 0cbe49913e experiment(confidence-engine): trace live openai schema boundary 2026-09-07 12:51:13 +01:00
robbond 0d27d4935b fix(confidence-engine): enforce openai strict object invariants 2026-09-07 12:22:03 +01:00
robbond e289f0be1f fix(confidence-engine): handle propertyless openai object schemas 2026-09-07 10:59:28 +01:00
robbond 0eadef6e3b fix(confidence-engine): honor openai alternate output schema 2026-09-07 10:18:41 +01:00
robbond bb3082d633 experiment(confidence-engine): route UI journey provider centrally 2026-09-07 09:35:53 +01:00
robbond 642a969b18 refactor(confidence-engine): compact current-handoff to operational snapshot; archive v0.61 experiment history to ch19
- Reduce docs/current-handoff.md from 2003 → 185 lines (90% reduction)
- Move all initial-decomposition v0.61 experiment narrative to
  docs/archive/experiments/vol-1-chapters/ch19/initial-decomposition-v0.61.md
- Update design-evolution/README.md with ch19 Era 8 entry
- Add CURRENT MVP DIRECTION section (frozen; OpenAI investigation is next question)
- Fix stale branch reference in current-project-state.md
- Update Return-to-Work Summary to reflect v0.61 completion
- Update Verification Marker for v0.60/v0.61 status
2026-09-07 07:51:08 +01:00
robbond 5878ce45ec experiment(confidence-engine): add reconstruction-only helper flag 2026-09-06 18:22:20 +01:00
robbond be8b725a8d docs(confidence-engine): record focused deconstruction repeatability 2026-09-06 17:02:57 +01:00
robbond a93b6798cc fix(confidence-engine): supply focused deconstruction schema 2026-09-06 16:21:26 +01:00
robbond 188dd04ab9 fix(confidence-engine): unwrap focused deconstruction response 2026-09-06 14:47:23 +01:00
robbond a0f90e8885 docs(confidence-engine): record matched provider reconstruction evidence 2026-09-06 13:28:55 +01:00
robbond d24ad48f62 test(confidence-engine): add canonical manufacturing scenario 2026-09-06 12:17:46 +01:00
robbond e8d401e506 fix(confidence-engine): extract raw openai response text 2026-09-06 11:45:24 +01:00
robbond 0bb2f01100 experiment(confidence-engine): isolate reconstruction observation 2026-09-06 10:31:45 +01:00
robbond 215c783d11 experiment(confidence-engine): complete openai reconstruction apparatus 2026-09-06 10:06:06 +01:00
robbond a45dd903cf experiment(confidence-engine): add alias-capable experiment runtime 2026-09-06 08:29:31 +01:00
robbond 1daf2bb6ce experiment(confidence-engine): expose reconstruction provider seam 2026-09-06 08:01:29 +01:00
robbond 860ee6fc5b experiment(confidence-engine): add openai provider apparatus 2026-09-06 07:50:47 +01:00
robbond 653934559c experiment(confidence-engine): reinforce outcome preservation salience 2026-09-06 07:17:15 +01:00
robbond 5c9f94ca13 experiment(confidence-engine): preserve supplied outcome categories 2026-09-06 07:08:57 +01:00
robbond 726746f22d fix(confidence-engine): extend reconstruction chat timeout 2026-09-06 06:16:36 +01:00
robbond 7070342fb1 fix(confidence-engine): preserve successful chat detection 2026-09-05 19:41:01 +01:00
robbond cd1c6f4fc5 chore(confidence-engine): expose reconstruction fallback path 2026-09-05 19:13:39 +01:00
robbond c7a0a79d0f feat(confidence-engine): constrain reconstruction chat output 2026-09-05 18:44:47 +01:00
robbond 8c5b47bcf5 chore(confidence-engine): upgrade to zod 4 2026-09-05 18:21:15 +01:00
robbond 46b9bd8b03 fix(confidence-engine): retain successful provider path 2026-09-05 17:12:31 +01:00
robbond dabd9e2245 fix(confidence-engine): expose failed provider path 2026-09-05 17:00:33 +01:00
robbond 677f5e5757 fix(confidence-engine): use configured model for chat detection 2026-09-05 16:46:24 +01:00
robbond 8c3edfec7d fix(confidence-engine): log case-start failures 2026-09-05 14:37:29 +01:00
robbond 72324e63c8 fix(confidence-engine): enforce relationship endpoint references 2026-09-05 14:09:25 +01:00
robbond 84fc53f017 fix(confidence-engine): expose reconstruction validation issues 2026-09-05 13:48:48 +01:00
robbond e5de8564a4 fix(confidence-engine): preserve intervention fit dependency 2026-09-05 13:33:01 +01:00
robbond 55e935066c fix(confidence-engine): project unexplained transitions 2026-09-05 13:15:11 +01:00
robbond e1839147b1 fix(confidence-engine): clarify relationship direction 2026-09-05 12:55:39 +01:00
robbond fb49df87aa feat(confidence-engine): expose initial reconstruction evidence 2026-09-05 12:39:17 +01:00
robbond 7472b6ecb0 feat(confidence-engine): preserve initial reconstruction relationships 2026-09-05 11:53:58 +01:00
robbond 13fbceee7a feat(confidence-engine): strengthen initial reconstruction contract 2026-09-05 10:43:57 +01:00
robbond 37245a8e28 fix(confidence-engine): expose reconstruction failure evidence 2026-09-05 08:18:04 +01:00
robbond a59d60262e fix(confidence-engine): correct experiment helper project root 2026-09-05 06:15:05 +01:00
robbond f2a761d26f test(confidence-engine): expose reconstruction experiment seam 2026-09-04 19:42:27 +01:00
robbond 844ec0eb8c docs(confidence-engine): correct v0.61 experiment 5 closeout 2026-09-04 18:23:02 +01:00
robbond 863f7dcbf2 docs(confidence-engine): record v0.61 repeated decomposition experiment 2026-09-04 18:04:16 +01:00
robbond 65c5ded9ab docs(confidence-engine): restore v0.61 decomposition objective 2026-09-04 17:17:53 +01:00
robbond 6218a3ed6d docs(confidence-engine): correct v0.61 accessibility evidence 2026-09-04 16:51:35 +01:00
robbond 898c3dcaaf docs(confidence-engine): record v0.61 intervention accessibility experiment 2026-09-04 16:32:55 +01:00
robbond 42da768e66 docs(confidence-engine): record v0.61 intervention-fit experiment 2026-09-04 16:11:38 +01:00
robbond 95d9965420 docs(confidence-engine): correct v0.61 experiment interpretation 2026-09-04 16:05:05 +01:00
robbond 580b2a122e docs(confidence-engine): record v0.61 decomposition experiment 2 2026-09-04 15:56:00 +01:00
robbond 642554038e test(confidence-engine): prove direct helper production seam 2026-09-04 14:33:02 +01:00
robbond 928954ee4a test(confidence-engine): verify direct decomposition helper 2026-09-04 14:02:36 +01:00
robbond 41ea2cb6b9 test(confidence-engine): add direct initial decomposition apparatus
Establishes reusable apparatus for asking: given scenario text X,
what structured initial decomposition does current production path produce?

- Direct curl/Postman via existing /api/cases/start route (no new API)
- Thin CJS helper at scripts/start-case-experiment-helper.cjs for Claude
  experiments (imports startCase directly, zero code duplication)
- Zero-live-call verification: all four seam checks confirmed by existing
  tests (cases-start-route.test.js, start-case-summary.test.js)
- No browser state, no persistence mutation, no Investigation ID required
  by the route itself

Files:
  + scripts/start-case-experiment-helper.cjs (new helper script)
  M docs/current-handoff.md (§v0.61 apparatus documentation)
2026-09-04 13:36:25 +01:00
robbond e6f2249413 docs(confidence-engine): close v0.60 multi-investigation work 2026-09-04 12:19:20 +01:00
robbond cc3a5dabd4 feat(confidence-engine): v0.60j preserve investigation on restart 2026-09-04 10:18:22 +01:00
robbond 4bc998ee3f test(confidence-engine): verify v0.60h report identity 2026-09-04 09:34:03 +01:00
robbond 2af5971987 feat(confidence-engine): v0.60h migrate report route to use route [id] identity 2026-09-04 08:17:01 +01:00
robbond df142ca76c feat(confidence-engine): v0.60g2 render investigation portfolio 2026-09-04 07:58:04 +01:00
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
robbond 4b55ad1eae feat(confidence-engine): v0.60f allocate investigation identity on create
Replace Portfolio's static "+ Create new investigation" link (href:
/investigations/case-1) with a <button> that allocates an opaque
application-owned durable ID via crypto.randomUUID() and navigates
via router.push to /investigations/{id} without persisting any empty
Investigation.

INVESTIGATION_ID constant retained only for card links (Continue
investigation / View report) — not migrated in this increment.

Test: deterministic Create New activation test verifies UUID allocation,
navigation to generated ID route, and zero saveInvestigation calls.
2026-09-03 19:20:28 +01:00
robbond 01e141aa66 feat(confidence-engine): v0.60e route investigation identity
Migrate the Investigation page route to own durable investigation identity
via its route [id] segment, passing that ID through to ScenarioForm for
hydration and persistence.

- Remove hardcoded INVESTIGATION_ID constant from page.jsx
- Use params.id as routeId; loadInvestigation(routeId) loads by identity
- Pass investigationId prop into ScenarioForm in both branch paths
- Session restore calls loadInvestigation(investigationId)
- All 4 save call sites include id: investigationId in snapshot
- Missing identified Investigation starts clean (no singleton fallback)
- Legacy singleton is not migrated/fallback-loaded
- Portfolio remains unmigrated; Report remains unmigrated; Restart untouched

Deterministic tests: 34/34 pass (scenario-form-persistence + investigation-storage)
Build: PASS
Live Playwright: all criteria verified at /investigations/v060e-live
2026-09-03 19:02:37 +01:00
robbond 827411f254 refactor(confidence-engine): v0.60d clarify investigation storage identity
Replace bare re-export in investigation-storage.js with explicit wrapper
functions that own the canonical identity contract: snapshot.id is the sole
save identity authority. The provider never allocates or changes IDs.

7 new deterministic tests prove: identified snapshots persist under their
own id key, explicit competing id arguments are ignored, A/B remain
independently addressable, unknown IDs return null, and legacy singleton
compatibility is preserved for unmigrated callers.

No application callers modified. UI/routes not migrated.
2026-09-03 18:29:00 +01:00
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
robbond f23d442eb3 docs(confidence-engine): resolve v0.60 investigation creation ownership 2026-09-03 17:44:17 +01:00
robbond 06a200bb03 docs(confidence-engine): define v0.60 investigation storage contract 2026-09-03 17:35:55 +01:00
robbond 2df026d024 fix(confidence-engine): align portfolio report freshness layout 2026-09-03 16:41:18 +01:00
robbond ede5d54e36 fix(confidence-engine): v0.59c-layout — stack freshness beneath View report
Replace flex-row View report + status with flex-col stack so the
freshness label sits below the button and no longer floats between
actions on the Portfolio.
2026-09-03 15:50:52 +01:00
robbond f06138de32 feat(confidence-engine): v0.59b-c — report freshness on Report page + Portfolio
v0.59b — Report page freshness UI:
- Shows Current / Update available beside the generated report
- Manual Update report action with duplicate prevention guard
- Explanation copy about investigation changes since generation
- Persists generatedFromRevision during update flow

v0.59c — Portfolio Report freshness state:
- Surfaces Current / Update available alongside existing View report link
- Derives solely from revision provenance (zero model calls)
- No Update report action on Portfolio (manual update owned by Report page)
- Neither state shown when no Report exists
- Updated makeSnapshot with investigationRevision for realistic test data
2026-09-03 14:41:02 +01:00
robbond 99b3d26817 feat(confidence-engine): v0.59a — correct Investigation revision provenance
Semantic revision tracking ensures every meaningful persisted
Investigation change advances investigationRevision exactly once,
while Report generation records (but does not advance) the current
revision as generatedFromRevision for provenance integrity.

Corrections:
- updateFindingDisposition: add setInvestigationRevision(+1) for
  semantic transitions (eligible→not_relevant, restore)
- updateFindingProposition: add no-op guard + setInvestigationRevision(+1)
- onRestart/ContinueLaterBanner/reset button: add setInvestigationRevision(0)
- onSituationGraphChange (Re-open seam): already had revision +1 in dirty impl

Established behaviour preserved:
- Re-open via reopenResolvedUnknown → onSituationGraphChange → revision +1
- Empty Done via handleDoneForNowPromotion → revision +1
- Report generation records generatedFromRevision, advances by 0
- Autosave passes revision but does not increment it
- clearInvestigation() ownership intact

Tests: targeted Vitest suite (17 tests) covering all provenance boundaries.

Durable rule documented in current-handoff.md §v0.59a.
2026-09-03 13:39:40 +01:00
robbond 37a9a12f93 docs(confidence-engine): route design evolution provenance through archive index 2026-09-03 11:56:36 +01:00
robbond fb2384cff1 docs(confidence-engine): promote design evolution archive index 2026-09-03 11:49:42 +01:00
robbond 2ed91468ed docs(confidence-engine): complete design evolution archive extraction 2026-09-03 11:42:40 +01:00
robbond c33bcdbefa docs(confidence-engine): checkpoint design evolution archive tranche seven 2026-09-03 11:34:04 +01:00
robbond 53cb99ee8f docs(confidence-engine): checkpoint design evolution archive tranche six 2026-09-03 11:19:05 +01:00
robbond bb3da3d197 docs(confidence-engine): checkpoint design evolution archive tranche five 2026-09-03 11:10:03 +01:00
robbond 37b1892d86 docs(confidence-engine): checkpoint design evolution archive tranche four 2026-09-03 11:02:11 +01:00
robbond 1f3b26c983 docs(confidence-engine): checkpoint design evolution archive tranche three 2026-09-03 10:55:03 +01:00
robbond e611283e3e docs(confidence-engine): checkpoint design evolution archive tranche two 2026-09-03 10:45:12 +01:00
robbond 83a66560ae docs(confidence-engine): checkpoint design evolution archive tranche one 2026-09-03 10:35:07 +01:00
robbond 577781eff8 docs(confidence-engine): consolidate current context and provenance 2026-09-03 09:37:04 +01:00
robbond 7db28c8611 feat(confidence-engine): generate investigation report on demand 2026-09-03 09:18:29 +01:00
robbond 99b75dca4e feat(confidence-engine): confirm destructive investigation restart 2026-09-03 07:36:43 +01:00
robbond 0da7b63e30 refine(confidence-engine): clarify portfolio investigation actions 2026-09-03 07:01:18 +01:00
robbond 32e1b01767 feat(confidence-engine): separate investigation report routes 2026-09-03 06:39:35 +01:00
robbond 745026f0a0 feat(confidence-engine): v0.54b integrate Investigation Overview UI seam + bounded scroll cleanup
- Wire transient overview state from ScenarioForm to ReasoningWorkspace
- Inline rendering of investigation overview below milestone invitation
- Remove obsolete scrollIntoView after overview request (scrolled away from rendered content)
- All four overview props consumed in ReasoningWorkspace render path
- Targeted Vitest: 9/9 PASS (tests/ui/investigation-overview-ui.test.jsx)
- Production build: compiles successfully
2026-09-02 15:14:20 +01:00
robbond 83818c0c71 feat(confidence-engine): add investigation overview synthesis seam 2026-09-02 14:34:32 +01:00
robbond 194a742772 docs(confidence-engine): checkpoint empty done and reopen 2026-09-02 13:51:05 +01:00
robbond f2c9e4c0b2 fix(confidence-engine): align empty done and reopen state
- Empty Done immediate transition now sets node.status to resolved
  alongside resolvedNodeIds/doneForNowIds — same canonical parked
  shape as populated Done (no server call required)
- Clarified-question Re-open removes target from doneForNowIds so
  the question visibly returns to Open Questions
- Immediate graph mutation creates new node objects immutably
  (React state semantics), touching only the target node
2026-09-02 13:37:15 +01:00
robbond 2b2096e41d fix(confidence-engine): scope focused presentation to active question
- FocusedQuestionBody derives thread-local contribution subset using
  targetNodeId || originatingTargetNodeId matching
- hasCompletedContext, latest completed contrib, and all effective
  presentation fallbacks use scoped collection only
- scenario-wide focusedContributions history preserved in memory
- Fresh Question B no longer bleeds Question A's content across
  every presentation surface (Previously answered, What this tells us,
  Still unclear, Questions this raises, Assumptions, Connections)
- Reopening or revisiting Question A still uses its own history
- Targeted regression: 3 new Vitest cases pass
- Handoff docs updated with v0.52 correction record
2026-09-02 12:46:50 +01:00
robbond a061428711 feat(confidence-engine): place zero-Open-Questions milestone at Open Questions position
Move the milestone invitation from after Clarified Questions to occupy
the same spatial position as Open Questions — between Current Understanding
and Questions we have clarified. Uses ternary: openUnknowns > 0 ? OpenQuestionsUI : milestoneAllowed ? MilestoneInvitation : null, followed by ClarifiedQuestionsUI unconditionally. No duplication of clarified cards or Re-open controls.
2026-09-02 12:02:55 +01:00
robbond 043ba5f264 feat(confidence-engine): clarify understanding during Done refresh 2026-09-02 07:53:45 +01:00
robbond b647236d44 fix(confidence-engine): constrain understanding to supported evidence 2026-09-01 18:17:26 +01:00
robbond 161527f66c docs(confidence-engine): record understanding refresh invariant 2026-09-01 16:37:42 +01:00
robbond cd895a33ff feat(confidence-engine): reopen clarified questions 2026-09-01 16:07:22 +01:00
robbond a23da2b727 refactor(confidence-engine): expose canonical graph replacement seam 2026-09-01 15:15:55 +01:00
robbond 9da0928453 feat(confidence-engine): retain clarified questions 2026-09-01 15:09:45 +01:00
robbond 76c6096905 fix(confidence-engine): resolve model for episode reasoning 2026-09-01 13:24:37 +01:00
robbond d22c992f60 fix(confidence-engine): avoid Done result shadowing 2026-09-01 12:17:32 +01:00
robbond 7177c7bb61 refactor(confidence-engine): make episode preparation server-owned 2026-09-01 11:54:02 +01:00
robbond 8432ed45d4 fix(confidence-engine): keep episode reasoning server-side 2026-09-01 11:24:48 +01:00
robbond 650877e5bf feat(confidence-engine): wire authoritative Done-for-now episode reconsideration
Adopt executeEpisodeDone orchestration as the canonical path for
'Done for now' activity boundary: one user Done triggers exactly
prepareCompletedEpisode -> reconsiderCompletedEpisode -> applyValidatedProposal
-> Current Understanding synthesis -> leave focused workspace.

Production changes (components/scenario-form.jsx):
- Add prepareCompletedEpisode, reconsiderCompletedEpisode, applyValidatedProposal imports
- Export executeEpisodeDone({params}) with all 4 domain functions as named
  parameters (defaults to module exports) for deterministic test wiring
- Rewrite handleDoneForNowPromotion(targetNodeId) as async: delegates to
  executeEpisodeDone pipeline; CU synthesis installed only on success
- Add doneInProgressRef useRef(false) for exactly-once Done enforcement
- On synthesis failure: KEEP updated graph, KEEP Findings, KEEP existing CU
- Retire produceFindingInformedSummary from ScenarioForm (legacy CU writer)
- Remove legacy idempotence guard and Evidence:[] regex dedup

Test changes (tests/ui/scenario-form-episode-done.test.jsx):
- 9 tests verifying orchestration pipeline correctness:
  1. Successful path order: prepare -> reconsider -> apply -> synthesis
  2. Correct prepared episode input parameters
  3. Structured application evidence (no answer fields in context)
  4. nextGraph used for synthesis (not stale result state)
  5. Reasoning failure: apply not called, CU synthesis not called
  6. Application failure: CU synthesis not called, graph not replaced
  7. Synthesis failure: nextGraph remains installed (no rollback)
  8. Exactly-once per call for each domain function
  9. Legacy Done writer retired (pipeline does not produce deterministic summary)
2026-09-01 10:05:12 +01:00
robbond c89cc51ae6 feat(confidence-engine): add completed episode reasoning seam 2026-09-01 09:28:02 +01:00
robbond ab655e2222 fix(confidence-engine): scope episode closure authority 2026-09-01 08:55:38 +01:00
robbond 0752c53a25 feat(confidence-engine): accept episode evidence at graph application 2026-09-01 08:30:26 +01:00
robbond 6b77e32771 feat(confidence-engine): accept completed episode reasoning input 2026-09-01 06:59:04 +01:00
robbond efa39f52de feat(confidence-engine): prepare completed episode evidence 2026-09-01 06:48:55 +01:00
robbond 18a7eb97cc docs(confidence-engine): align durable context with current baseline 2026-09-01 06:19:55 +01:00
robbond 0059c10f14 docs(confidence-engine): baseline current handoff 2026-09-01 06:14:02 +01:00
robbond 0624bc20e2 fix(confidence-engine): gate focused completion during processing 2026-08-31 18:29:06 +01:00
robbond b270aa5624 feat(confidence-engine): case/update synthesis — dedicated reconstruction per update (v0.50)
Architecture: after successful /api/cases/update, derive explicit nextGraph +
nextFindings, call synthesizeFromFindings exactly once, replace Current
Understanding with reconstruction result.

Key invariants:
- outcome.summary retired as final CU authority → always synthesis reconstruction
- Explicit derived state (no React-state reread) for graph and findings
- Previous CU preserved on synthesis failure (no fallback to outcome.summary)
- Graph and Findings NOT lost on synthesis failure
- saveInvestigation persistence uses currentUnderstanding, not outcome.summary

Deterministic regression: 7 tests (Cases A-E + 2 edges) covering all rules.

Files: components/scenario-form.jsx, tests/ui/scenario-form-case-update-synthesis.test.jsx
2026-08-31 09:22:50 +01:00
robbond 989b88a4a1 feat(confidence-engine): synthesize restored findings 2026-08-31 08:18:44 +01:00
robbond addec52461 feat(confidence-engine): synthesize not relevant findings 2026-08-31 08:03:24 +01:00
robbond 5fb32e628c feat(confidence-engine): synthesize corrected findings 2026-08-31 07:48:29 +01:00
robbond 8e941b0c7b fix(confidence-engine): resolve synthesis model configuration 2026-08-31 07:32:04 +01:00
robbond 75f7c6bafd feat(confidence-engine): synthesize understanding from focused findings 2026-08-30 19:27:30 +01:00
robbond ff1119b4d5 feat(confidence-engine): establish current understanding synthesis seam 2026-08-30 18:48:38 +01:00
robbond 00ba343ed9 docs(confidence-engine): close v0.49 current understanding boundary 2026-08-30 17:23:44 +01:00
robbond 8c98ce94de docs(confidence-engine): close workspace controls boundary 2026-08-30 12:13:29 +01:00
robbond bdb234262c fix(confidence-engine): close workspace after done for now 2026-08-30 12:06:33 +01:00
robbond 07e1363368 docs(confidence-engine): document v0.49 workspace controls recovery 2026-08-30 11:46:54 +01:00
robbond 16cab4645a fix(confidence-engine): workspace control cleanup — rename close button, remove 'Back to open questions' from navigation 2026-08-30 11:35:15 +01:00
robbond 922f58a49f fix(confidence-engine): project processing indicator to active follow-up block
Repair LOCATION-A defect where processing feedback rendered near the
completed narrative instead of inside the active follow-up block.

Changes:
  - components/reasoning-workspace.jsx: three targeted edits using a single
    spinner component with conditional rendering; hasActiveFollowUp routes
    ownership to the correct container
  - tests/open-questions-vs-assumptions.test.jsx: regression test confirming
    exactly one indicator, DOM child of follow-up-block, ownership separation

Accepted criteria met:
   Exactly one processing indicator during follow-up processing
   Indicator is a DOM child of follow-up-block
   Top-level indicator suppressed when follow-up active
   Initial answer flow preserved (top-level when no follow-up)
   Successful follow-up promotion intact
   All existing context retained
   No new state/lifecycle changes/error redesign
2026-08-30 11:09:24 +01:00
robbond bf7629691f fix(confidence-engine): preserve follow-up context while processing 2026-08-30 10:43:57 +01:00
robbond ae1201bb27 fix(confidence-engine): simplify active follow-up presentation 2026-08-30 08:51:18 +01:00
robbond 8bded90094 fix(confidence-engine): preserve follow-up progression ownership 2026-08-30 08:28:29 +01:00
robbond 17c6048047 fix(confidence-engine): preserve completed-narrative when selecting follow-up + reverse prior-contribs display
Two presentation fixes (no reasoning-engine changes):

A. Follow-up answer continuity — selectFollowUpQuestion clears focused.answer,
   which previously caused the completed-narrative framing ('Previously answered'
   and 'Your response') to disappear mid-investigation. The guard now treats
   a non-null result as sufficient evidence of a completed-context state, so the
   user's verbatim answer and derived findings remain visible while a follow-up is
   being formulated.

B. Prior-contributions chronology — display order in 'Previous learning' panels
   has been reversed at the presentation boundary (newest → oldest). This means
   users see the most recently learned evidence first, without modifying data-order
   anywhere else. Applies to both PriorContributionsSummary and
   SecondaryPreviousLearning.
2026-08-29 19:04:06 +01:00
robbond 890a18c5a7 feat(confidence-engine): present completed results as coherent provenance narrative
When a reopened completed turn is displayed, distinguish it from an active question:

- 'PREVIOUSLY ANSWERED' + 'YOUR RESPONSE' headings for completed turns (hasAnswer=true)
- Bare 'QUESTION' heading preserved for active follow-ups (answer=null)
- Verbatim user answer rendered under its own heading — never conflated with Engine-derived findings
- Causal narrative: Question → Your response → What this tells us

Gate results:
- 102 tests passed (78 existing + 24 new v0.49 provenance narrative tests)
- Clean production build
- Live verification on localhost:3000 confirmed correct rendering
2026-08-29 18:45:55 +01:00
robbond 50a66749ae docs(confidence-engine): preserve evidence provenance 2026-08-29 18:24:32 +01:00
robbond 88d9768276 fix(confidence-engine): distinguish completed focused result 2026-08-29 18:09:26 +01:00
robbond dac19a3552 fix(confidence-engine): show focused investigation activity 2026-08-29 16:46:51 +01:00
robbond b9c0b6f6f7 fix(confidence-engine): show focused investigation activity 2026-08-29 15:15:28 +01:00
robbond 0f4dfcbb17 fix(confidence-engine): show canonical findings in previous learning 2026-08-29 14:38:04 +01:00
robbond a8539e2494 fix(confidence-engine): restore finding controls on reopen 2026-08-28 13:25:24 +01:00
robbond 06f3f501d2 docs(confidence-engine): record restored workspace findings 2026-08-28 12:03:13 +01:00
robbond 10cbcbdd05 fix(confidence-engine): preserve investigation activity across turns
ThreadContributionsBadge, PriorContributionsSummary, and
SecondaryPreviousLearning all filtered contributions via
c.targetNodeId === nodeId. Multi-turn follow-up Contributions carry a
different immediate targetNodeId while the canonical origin remains on
Findings (originatingTargetNodeId).

Repaired: all contribution filters now match on EITHER
c.targetNodeId === nodeId || c.originatingTargetNodeId === nodeId.
handleDeconstructSubmit carries originatingTargetNodeId from
focusedPresentationItemId as provenance for cold-return recovery.
2026-08-28 11:56:31 +01:00
robbond b9a54589f0 docs(confidence-engine): record Phase 6 UNCLEAR + INVESTIGATING cue results
- Document live Playwright verification of amber INVESTIGATING indicator
  on Open Questions cards with matching contribution targetNodeId
- Document 11 new deterministic tests in focused-investigation-history describe block
- Confirm UNCLEAR and INVESTIGATING coexist independently (epistemic vs activity)
- Note that cue only renders inside OpenQuestionsPanel, not initial reflection surface
2026-08-28 11:26:57 +01:00
robbond 556acfb156 feat(confidence-engine): render INVESTIGATING cue on Open Question cards with focused history
- ThreadContributionsBadge (rendered per-node on OpenQuestionsPanel
  cards and Done-for-now cards) now shows an amber INVESTIGATING
  indicator when the node has matching contributions via targetNodeId
  identity match.
- UNCLEAR and INVESTIGATING cues coexist independently on the same
  card — UNCLEAR is epistemic state, INVESTIGATING is activity cue.
- Deterministic test suite added: focused-investigation-history (11
  tests) covering identity matching, zero-contrib edge cases,
  multiple-contrib coalescing, done-for-now retention, and uncoupling
  from UNCLEAR state.
- All 57 tests pass.
2026-08-28 11:25:40 +01:00
robbond f1bd91faf8 feat(confidence-engine): promote focused learning on done 2026-08-28 10:48:45 +01:00
robbond e221bd3bf8 feat(confidence-engine): isolate finding-informed understanding 2026-08-28 08:16:37 +01:00
robbond c45b703b3a docs(confidence-engine): record v0.48 storage closure and next boundary handoff
Record: v0.48 persistence objective complete; Finding eligibility resolved;
next boundary is isolated Finding-informed Current Understanding (feature/
finding-informed-understanding-v0.49); broader Finding-system questions
intentionally deferred. No production or test changes.
2026-08-28 07:59:50 +01:00
robbond b215846478 docs(confidence-engine): reconcile v0.48 persistence evidence 2026-08-28 07:06:10 +01:00
robbond 3e9123fe0e chore: scope experiment artifact ignores 2026-08-27 19:08:03 +01:00
robbond 3c5257cbd7 docs(confidence-engine): update v0.48 storage handoff 2026-08-27 19:05:30 +01:00
robbond d55f179d37 refactor(confidence-engine): remove legacy workspace persistence 2026-08-27 16:52:55 +01:00
robbond 166ee91698 feat(confidence-engine): persist canonical investigation state 2026-08-27 16:21:41 +01:00
robbond bc35e05253 refactor(confidence-engine): migrate scenario persistence to storage provider 2026-08-27 15:44:58 +01:00
robbond ba956eeb3a refactor(confidence-engine): add investigation storage provider 2026-08-27 15:24:38 +01:00
robbond 22e1d7484b feat(confidence-engine): support finding corrections 2026-08-27 13:43:23 +01:00
robbond 5c926154cd feat(confidence-engine): support not-relevant findings 2026-08-27 12:08:11 +01:00
robbond bf6c4241a5 chore: ignore local playwright mcp artifacts 2026-08-27 10:42:31 +01:00
robbond 0f4e49fc6c feat(confidence-engine): render focused findings from canonical state 2026-08-27 10:38:50 +01:00
robbond 7858650334 feat(confidence-engine): expose correlated findings to focused presentation 2026-08-27 09:23:51 +01:00
robbond ca7d8e5384 feat(confidence-engine): correlate focused result with contribution 2026-08-27 08:37:23 +01:00
robbond 0659599795 feat(confidence-engine): wire focused contribution finding derivation 2026-08-27 07:45:15 +01:00
robbond dc558e9c37 docs(confidence-engine): verify findings derivation gap in production chain
Verified: deriveFindingsFromContributions() exists with passing tests but
is never called in production. The contributions → findings seam is un-wired:

1. handleDeconstructSubmit() sends contribution to ScenarioForm
2. appendFocusedContribution() stores it in focusedContributions[]
3. findings state stays [] — no derivation ever runs
4. empty findings sent to /api/cases/update (which only echoes them back)
5. nothing renders from the findings surface

Fix: call deriveFindingsFromContributions after contribution is appended.
2026-08-27 07:40:50 +01:00
robbond 061ea364b3 feat(confidence-engine): derive findings from focused contributions 2026-08-27 06:44:07 +01:00
robbond 6a5cb43a30 docs(confidence-engine): checkpoint focused investigation workspace 2026-08-26 19:26:52 +01:00
robbond abeb3fcb03 feat(confidence-engine): add focused investigation overlay workspace 2026-08-26 19:22:54 +01:00
robbond 990b51aecf docs(confidence-engine): define progressive investigation workspace model 2026-08-26 17:26:04 +01:00
robbond 48b7185176 docs(confidence-engine): record current understanding isolation blocker 2026-08-26 16:30:32 +01:00
robbond 3235c35cf0 feat(confidence-engine): add focused finding handoff plumbing 2026-08-26 16:07:21 +01:00
robbond d3015f63d8 docs(confidence-engine): define minimum finding handoff slice 2026-08-26 15:07:57 +01:00
robbond 854160726e docs(confidence-engine): define focused finding handoff contract 2026-08-26 14:50:15 +01:00
robbond 10aa18d367 docs(confidence-engine): define finding graph reasoning contract 2026-08-26 14:28:33 +01:00
robbond ac5fbe7896 fix(confidence-engine): preserve focused learning across turns 2026-08-26 14:07:16 +01:00
robbond b26ea7d0ba docs(confidence-engine): establish contributions and findings distinction 2026-08-26 14:07:05 +01:00
robbond cb707c0192 test(confidence-engine): capture tentative mapping regression 2026-08-26 14:06:51 +01:00
robbond 787c8114ad docs(confidence-engine): record focused progression walkthrough findings 2026-08-26 12:40:37 +01:00
robbond fdb173d0e9 feat(confidence-engine): anchor focused frontier to investigation relevance 2026-08-26 12:16:41 +01:00
robbond cfd463d8b3 test(confidence-engine): capture structural frontier priority regression 2026-08-26 12:10:07 +01:00
robbond fd02be0f29 fix(confidence-engine): render focused investigation in current presentation 2026-08-26 11:52:18 +01:00
robbond 6288ef1031 fix(confidence-engine): keep focused investigation in current presentation 2026-08-26 11:30:59 +01:00
robbond 81dda77392 fix(confidence-engine): reopen completed focused investigation 2026-08-26 10:20:45 +01:00
robbond 42a7e82d88 feat(confidence-engine): tighten focused relationship attribution 2026-08-26 07:56:27 +01:00
robbond 3ca37b0918 test(confidence-engine): checkpoint focused deconstruction regression cases 2026-08-26 07:34:22 +01:00
robbond 112739b8e5 feat(confidence-engine): checkpoint focused deconstruction reasoning 2026-08-25 15:10:09 +01:00
robbond 9d670822a3 feat(confidence-engine): preserve focused deconstruction semantic fidelity 2026-08-24 10:11:06 +01:00
robbond 2c108df5a9 checkpoint: preserve latest live run graph output json 2026-08-23 19:56:07 +01:00
robbond c134b5cb04 feat(confidence-engine): stabilize investigation workspace with semantic decomposition and deterministic presentation anchors 2026-08-23 16:59:55 +01:00
robbond 01c57788ee feat(confidence-engine): stabilize user-directed investigation flow
Intentional changes in this checkpoint:
- Deconstruct route: use body.targetNodeId (client identity) over raw.model-invented ID
- ThreadContributionsBadge: compact per-thread contribution indicator with expandable history
- Reopen continuation: resume from accumulated contributions instead of reformulating
- showEvidenceLimit gate: hide evidence-limit card during active investigation paths
- Evidence-limit visibility correction in rendering pipeline
- Section ordering: assumptions and connections after 'Still unclear' in focused result
- Prompt v0.3: preserve user-stated alternatives as separate unknowns; no count inflation
- 3 durable regression tests (target identity, contribution persistence, reopen state)
- evidence-limit card visibility gate test suite

Temporary residue removed:
- test-analysis.mjs (scratch diagnostic)
- 5 diagnostic console.log blocks from reasoning-workspace.jsx
2026-08-23 12:05:51 +01:00
robbond 96ad0e7915 refine(ui): restore visual hierarchy and Situation context in initial workspace
- Enhance Current Understanding prominence with subtle teal/teal border
  gradient, stronger heading, larger body text, more internal spacing
- Restore Situation panel as right-hand column in initial reflection view;
  uses OriginalSituation when graph exists, scenario text fallback otherwise
- Stacks layout on narrow screens via grid-cols-1/gap-6/lg:grid-cols-3
- Apply teal styling to normal-state CurrentUnderstandingCard and
  PlainLanguageCard (was flat gray border with bg-transparent)
- Surface assumption nodes alongside unknowns in Open Questions; add
  Unclear / Plausible interpretation tags
- Wire up follow-up question buttons in deconstructed results
2026-08-22 19:01:45 +01:00
robbond 517d780e2c checkpoint: preserve semantic decomposition investigation state 2026-08-22 08:21:29 +01:00
robbond 68be2344c6 fix(rto): persist focused contributions immediately after deconstruct success
The successful focused deconstruct calls onFocusedContribution which
updates parent state, but never persisted the new collection to
sessionStorage. This meant an immediate reload would lose the
contribution.

Fix: add a useEffect in ReasoningWorkspace that watches the
focusedContributions prop for changes and saves via the existing
saveSession mechanism. A ref guard prevents double-save alongside the
existing updateStatus-success effect.
2026-08-21 18:33:20 +01:00
robbond fce68a050f feat(ui): RTO.31 ownership of focused contributions flows to scenario form
- Add focusedContributions state + appendFocusedContribution callback in ScenarioForm
- Contributions persist through session lifecycle (save/restore/restart)
- Pass onFocusedContribution and focusedContributions to ReasoningWorkspace
- Call onFocusedContribution on successful deconstruct with full result shape
- Test: contribution sequence, field preservation, same/different target coexistence
2026-08-21 18:03:49 +01:00
robbond 41afd9b49f checkpoint: preserve reflection and response-contract work 2026-08-21 14:29:39 +01:00
robbond c9335cf850 fix(ui): make initial reflection surface exclusive to post-Analyse state
Add three exclusivity guards that suppress legacy surfaces during the
initial post-Analyse reflection state (postAnalyseStatus === 'success'):

- CurrentInvestigationCard: suppressed because its selectedQuestion
  from startCase was leaking into the initial reflection view
- OpenQuestionsPanel: suppressed because it rendered whenever hasGraph
  was true, regardless of initial reflection state
- Terminal state cards (EvidenceLimitCard / CompletionCard): suppressed
  because they fired on status='success' && !hasSelectedQuestion

Transition out of initial reflection happens when user clicks a proposed
finding, which sets formulationStep='active' and triggers the existing
deactivation useEffect.

No reasoning changes. No startCase changes. No mock changes.
2026-08-21 11:51:33 +01:00
robbond 86287bebe8 feat(ui): surface initial semantic reconstruction 2026-08-21 10:34:22 +01:00
robbond 412551c968 test(ui): restore deconstruction before question choice 2026-08-20 15:58:23 +01:00
robbond 4b264c5681 test(ui): make inferred questions originate branches 2026-08-20 14:29:42 +01:00
robbond 65ced2e406 test(ui): make initial branch selection user owned 2026-08-20 14:11:37 +01:00
robbond 4761d07a76 test(ui): stabilize fresh start hydration 2026-08-20 10:00:18 +01:00
robbond 173240d76c test(ui): restore fresh start scenario entry 2026-08-20 09:42:32 +01:00
robbond cef8f46bd6 test(ui): restore entry lifecycle and notebook rendering 2026-08-20 09:33:06 +01:00
robbond 86bb3426ef test(ui): explore provisional branch pause and reopen 2026-08-20 09:00:01 +01:00
robbond df0e3b5a9b test(ui): explore branch notebook composition 2026-08-20 08:42:43 +01:00
robbond e7a1bc689c test(ui): consolidate branch scoped workspace 2026-08-20 08:01:40 +01:00
robbond 36060faf16 test(ui): verify branch scoped workspace data path 2026-08-20 07:56:26 +01:00
robbond 49c4b904df test(ui): checkpoint branch scoped fixture 2026-08-20 07:43:18 +01:00
robbond 09eeed5a9e test(ui): isolate branch scoped workspace experiment 2026-08-20 07:35:08 +01:00
robbond 47cd0c7d7b test(experiment): checkpoint branch scoped reasoning retrieval 2026-08-20 06:48:44 +01:00
robbond 6bf9e7e710 test(ui): explore branch as workspace context 2026-08-20 06:26:35 +01:00
robbond 8163c5d014 test(ui): clarify branch provenance and hierarchy 2026-08-20 06:15:44 +01:00
robbond 0ac2e05c40 test(ui): clarify branch focus and passive updates 2026-08-20 06:01:32 +01:00
robbond ad42f67800 test(ui): explore passive branch result indication 2026-08-20 05:55:25 +01:00
robbond 058ad2326f test(experiment): checkpoint nonlinear branch continuity 2026-08-20 05:45:29 +01:00
robbond 9fb9735c6e test(experiment): checkpoint semantic relationship inference result 2026-08-19 19:14:46 +01:00
robbond a00adfabf4 test(experiment): simplify relationship inference apparatus 2026-08-19 18:53:10 +01:00
robbond 601e46e4b7 docs: preserve domain-independent facilitator principles 2026-08-19 18:39:35 +01:00
robbond 85204f96ac test(experiment): checkpoint borderline relationship control 2026-08-19 17:35:51 +01:00
robbond e1a18e27e7 test(experiment): checkpoint relationship negative-control result 2026-08-19 17:27:52 +01:00
robbond a73f125f8d test(experiment): checkpoint relationship false-positive apparatus 2026-08-19 17:21:13 +01:00
robbond c97f07ba65 test(experiment): checkpoint fragment relationship discovery result 2026-08-19 16:59:22 +01:00
robbond 67103fa8d4 test(experiment): repair relationship discovery env loading 2026-08-19 16:35:58 +01:00
robbond 4687226bd8 test(experiment): repair relationship discovery result handling 2026-08-19 16:14:50 +01:00
robbond 8fb284c374 test(experiment): checkpoint fragment relationship discovery apparatus 2026-08-19 15:43:06 +01:00
robbond 8c52939c02 test(experiment): checkpoint derived focused current-view result 2026-08-19 15:34:41 +01:00
robbond 644108db71 test(experiment): checkpoint derived focused current-view apparatus 2026-08-19 15:27:14 +01:00
robbond 0b0d5594fe test(experiment): checkpoint granular answer fragment result 2026-08-19 14:41:57 +01:00
robbond fbeaf01f90 docs: archive verified historical experiment families 2026-08-19 14:25:43 +01:00
robbond e6d0327641 docs: archive historical Confidence Engine evidence 2026-08-19 12:07:17 +01:00
robbond a12f9555af docs: clarify Confidence Engine context authority 2026-08-19 11:49:30 +01:00
robbond 5b43c1b8f9 docs: preserve Confidence Engine methodology continuity 2026-08-19 10:46:05 +01:00
robbond e1b54e4073 test(experiment): checkpoint granular answer fragment apparatus 2026-08-19 10:14:46 +01:00
robbond 6ed3415220 test(experiment): checkpoint three-turn separated reasoning result 2026-08-19 09:51:31 +01:00
robbond 98889039c2 test(experiment): checkpoint three-turn separated reasoning apparatus 2026-08-19 09:35:48 +01:00
robbond 9c715161b0 test(experiment): checkpoint separated reasoning layers result 2026-08-19 09:15:25 +01:00
robbond 56de4a7ef3 test(experiment): checkpoint separated reasoning layers apparatus 2026-08-19 08:53:33 +01:00
robbond b20707c447 test(experiment): checkpoint focused context boundary result 2026-08-19 08:33:41 +01:00
robbond 6ed4d60029 test(experiment): checkpoint focused context boundary apparatus 2026-08-19 07:59:41 +01:00
robbond 153bbee85e test(experiment): checkpoint two-turn focused refinement result 2026-08-19 07:53:26 +01:00
robbond 8520f2195d test(experiment): expose two-turn live refinement route 2026-08-19 07:44:55 +01:00
robbond ebcf1d9306 test(experiment): checkpoint two-turn focused refinement apparatus 2026-08-19 07:35:32 +01:00
robbond dafc020f66 feat(experiment): checkpoint one-turn focused investigation UI 2026-08-19 07:07:10 +01:00
robbond 7add85d8d2 feat(experiment): checkpoint focused investigation boundaries 2026-08-19 05:41:32 +01:00
robbond c0b963973f test(experiment): checkpoint focused vs global result 2026-08-19 05:15:25 +01:00
robbond 913dfec507 test(experiment): checkpoint comparison observability 2026-08-18 19:28:30 +01:00
robbond 952cb442b5 test(experiment): checkpoint focused vs global comparison apparatus 2026-08-18 18:34:08 +01:00
robbond 2f6c90b027 test(experiment): checkpoint focused answer deconstruction 2026-08-18 18:25:35 +01:00
robbond 648e1c7a29 test(experiment): checkpoint explicit-node formulation 2026-08-18 17:29:07 +01:00
robbond fd98cda8ba feat(experiment): checkpoint RTO question lifecycle 2026-08-18 16:38:15 +01:00
robbond 6b25100f9a feat(experiment): checkpoint RTO open-question workspace 2026-08-18 15:25:27 +01:00
robbond db5016c138 feat(experiment): checkpoint RTO case workspace lifecycle 2026-08-18 14:59:52 +01:00
robbond 4a34dcc361 test(experiment): ground RTO apparatus in real fixture 2026-08-18 12:39:19 +01:00
robbond 25a88c5fc3 feat: multi-thread experimental apparatus (RTO.A1)
Add fixture-only apparatus for representing multiple concurrent open
investigation items within a fixed case context.

New scenario 'multi-thread' exposes:
- A fixed central situation statement and case summary (product-launch
  timing decision, drawn from existing pre-anchored-product-launch
  data)
- Three open investigation items — none compulsory: enterprise customer
  signing probability, competitor timing, financial viability comparison
- One engine recommendation (mt-ent-customer-signing, ordered first)
- User selection of any item; chosen item becomes visually primary while
  others remain visible as context
- Experimental state isolated in _experimental / _experimentalState —
  never aliases production graph fields
2026-08-18 10:39:16 +01:00
387 changed files with 50833 additions and 10764 deletions
+37 -10
View File
@@ -19,6 +19,18 @@ It:
6. updates the graph from the answer;
7. repeats until action is justified or the remaining uncertainty is clear.
> **NOTE:** The flow above describes historical/current implementation mechanics.
> It does not represent current Confidence Engine methodology direction.
> See `docs/Confidence_Engine_Return_to_Origin_Methodology_Context_2026-08-18.md`
> for the current working hypothesis (granular answer-fragment inquiry).
The linear selector-led flow described above is a **historical capability**, not
an automatic architecture to continue. Under Return-to-Origin:
- The Engine facilitates inquiry; it does not compel a single-question route.
- The user owns which unresolved investigation/question to pursue.
- Accumulated reasoning memory does not necessarily belong inside repeated LLM calls.
A chatbot remembers the conversation.
The Confidence Engine preserves the state of the reasoning.
@@ -50,15 +62,23 @@ The engine should help a user reach one of these states:
## Current development stage
The deterministic reasoning architecture reached a stable alpha checkpoint.
> **Version lineage note:** The Confidence Engine uses two distinct version
> lineages that must not be conflated:
> - **Reasoning-engine experimental lineage** (v0.8+): reasoning-fidelity,
> investigation-state assessment, semantic selectors — under RTO pause.
> - **UX/product development lineage** (v0.7): workspace layout, user views,
> loading feedback — also paused.
> These are independent tracks; do not assume they describe one product version.
Current work is primarily improving:
The deterministic reasoning architecture reached a stable alpha checkpoint
(reasoning-engine v0.8). UI/product work reached v0.7 staging. Both have
paused under Return to Origin while the granular answer-fragment hypothesis
is evaluated as working methodology context.
- usability;
- presentation;
- loading feedback;
- plain-language explanations;
- separation of user and developer views.
Current work is paused. The next step begins from the methodology question:
given the useful investigation structure the Engine can already derive, how
should that structure be surfaced so a person can see, choose, defer, and
return to open questions while the Engine continues to guide their thinking?
Do not resume broad reasoning architecture work unless a repeated observed
failure clearly requires it.
@@ -93,7 +113,11 @@ The interface should minimise cognitive load by presenting the current state fir
The engine may contain hundreds of reasoning nodes; the user should only see the information required to take the next meaningful action.
## Why workspace layout matters (v0.7)
## Why workspace layout matters (v0.7 — UX/product lineage)
> **This section documents paused UX design intent.** It belongs to the v0.7
> product development lineage, not the reasoning-engine lineage. UI work is
> currently paused under Return to Origin.
This phase optimises for simultaneous visibility instead of sequential scrolling.
Related panels — Understanding alongside Investigation Map, Situation alongside History — can appear side-by-side on wide screens while mobile continues to stack everything vertically. The reasoning engine is completely unaware of these changes; only the presentation layer is affected.
@@ -104,7 +128,10 @@ Read `docs/current-working-principles.md` for current guidance. Treat `docs/arch
For UI mock work, read `docs/ui-mock-reference.md`. Do not load
`docs/archive/deferred-ux-backlog.md` unless a named past UX idea is being reviewed.
Engine and UI experiments are paused. First file to inspect when resuming:
`docs/current-project-state.md`, then `docs/project-knowledge-inventory.md`.
Engine and UI experiments are paused under Return to Origin. First file to inspect when resuming:
**`docs/current-handoff.md`** (methodology continuity anchor), then `docs/current-project-state.md`, then `docs/project-knowledge-inventory.md`.
> After reading `docs/current-project-state.md`, choose the relevant minimal pack from `docs/task-context-packs.md`. Do not combine packs unless a specific task genuinely crosses boundaries.
>
> **Historical experiment families are evidence to load only when a specific question requires them; they are not default architecture context.**
+15
View File
@@ -515,6 +515,21 @@ Three tiers, applied top to bottom:
- Omit items too verbose to scan; do not synthesise rewritten claims.
- Never invent facts absent from the graph.
### Provenance and attribution
Preserve authorship and provenance in every user-facing presentation.
When displaying a user's previous input, keep it visibly distinct from system-generated interpretation. If the original user wording is available, present it as the user's response rather than rewriting it into system prose. Derived Findings, summaries, uncertainties, assumptions, or follow-up questions must not be styled or worded in a way that implies the user said them.
The distinction should be:
```text
User response → user-authored (verbatim)
What we learned → Engine-derived
```
Exact labels are subject to UX refinement; the durable rule is separating provenance, not prescribing specific copy.
## Investigation Narrative
The reasoning graph is the machine representation of the investigation.
+55
View File
@@ -123,3 +123,58 @@ Stop after reporting. Do not begin the next task automatically.
When a task is interrupted by output limits, resume with a narrowly scoped repair prompt rather than restating the entire original brief.
User interfaces communicate reasoning, not implementation. If a piece of information exists only because the engine tracks it internally (graph nodes, unresolved counts, edge totals, confidence scores), it should remain in Developer Details unless it directly helps the user make their next decision.
## Playwright MCP — canonical dev server ownership
- Assume `http://localhost:3000` is already running when a task names it.
- Never start / stop / kill / restart / replace / port-probe the dev server.
- Never reinterpret "do not start/restart/kill/probe" as "start normally" or "use npm run dev".
- If the canonical dev server is unavailable: **BLOCKED** — do not proceed.
## Playwright MCP — known controls and semantic locators
For known UI controls, use **Run Playwright code** with exact semantic locators:
```js
await page.getByRole('button', { name: 'Review current understanding' }).click();
```
Do NOT first try MCP Click. Do NOT use snapshot refs (`[ref=...]`) for actions — they are observational only.
Semantic scoping is allowed and encouraged where names repeat, e.g.:
```js
page.getByRole('dialog').getByRole('button', { name: 'Restart investigation' });
```
## Playwright MCP — semantic waits
For known async/hydration states, use `waitFor` with a semantic state — not arbitrary sleeps:
```js
await page.getByRole(...).waitFor({ state: 'visible', timeout: ... });
```
Client hydration is real product behaviour. Always await before classifying localStorage-backed UI state.
## Playwright MCP — selector failure
If the prescribed semantic locator cannot find its expected control: **STOP**.
Do NOT fall back to snapshot refs, CSS selectors, XPath, DOM traversal, `page.evaluate`, aria-label guessing, or locator archaeology.
## Playwright MCP — browser state and live freeze
During live verification do not inspect / inject / mutate browser storage merely to manufacture expected test state (unless storage manipulation itself is the explicit experiment).
Once live Playwright verification begins: **NO PRODUCTION FILE EDITS**. First visible discrepancy is evidence to capture and stop on.
## Deterministic test rules — apparatus ownership
**Tests are instruments, not product truth.**
At the first deterministic failure classify: **PRODUCT FAILURE** or **APPARATUS FAILURE**, then stop.
For APPARATUS FAILURE: do not turn the product task into test-harness development. Do not enter repeated vi.mock / dynamic re-import / module-cache manipulation / duplicate render / global mutation repair loops. Route apparatus correction separately.
If a lower-layer function is mocked, test the value crossing the mocked seam — do NOT require the mock to reproduce its real implementation. Storage-layer tests own storage writes.
+37
View File
@@ -0,0 +1,37 @@
node_modules
.next
out
dist
coverage
*.lcov
test-results
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
evaluation-results
provider-debug-results
tests-results
.playwright-mcp/
.evidence-temp/
# Git
.git
.gitignore
# Environment files with secrets (never bake into image)
.env
.env.local
.env.*.local
# Documentation / handoff (not needed for build)
docs
*.md
# IDE
.vscode
.idea
# OS generated files
.DS_Store
Thumbs.db
+6 -9
View File
@@ -1,15 +1,12 @@
# Local Ollama server address
OLLAMA_BASE_URL=http://192.168.x.x:11434
# ── Supabase Auth (public browser configuration only) ────────────────
NEXT_PUBLIC_SUPABASE_URL=https://supabase.rdbcloud.co.uk
NEXT_PUBLIC_SUPABASE_ANON_KEY=replace-with-supabase-anon-key
# Model name (e.g., llama3, mistral, codellama, etc.)
# ── Ollama provider (runtime, server-only) ────────────────────────────
OLLAMA_BASE_URL=http://192.168.x.x:11434
OLLAMA_MODEL=replace-with-model-name
# ── Mock / Demo Mode (UI development only) ──────────────────
# Set to "true" to use pre-recorded scenario fixtures instead of Ollama.
# ── Mock / Demo Mode (UI development only) ────────────────────────────
NEXT_PUBLIC_CONFIDENCE_ENGINE_MOCKS=true
# Mock delay mode: "instant" | "normal" (default, 700ms) | "slow" (2500ms)
NEXT_PUBLIC_CONFIDENCE_MOCK_DELAY=normal
# Scenario to replay: "complete" (jump to end after start) | "error" | "" (default sequential turns)
NEXT_PUBLIC_CONFIDENCE_ENGINE_MOCK_SCENARIO=complete
+7
View File
@@ -39,3 +39,10 @@ yarn-error.log*
evaluation-results/
provider-debug-results/
tests-results/
# Local Playwright MCP runtime output
.playwright-mcp/
# Evidence/temp directories from live experiments
.evidence-temp/
+45
View File
@@ -0,0 +1,45 @@
# ── Stage 1: Build ─────────────────────────────────────────────────────
FROM node:22-alpine AS builder
WORKDIR /app
ENV NEXT_PUBLIC_SUPABASE_URL="" \
NEXT_PUBLIC_SUPABASE_ANON_KEY=""
COPY package.json package-lock.json* yarn.lock* pnpm-lock.yaml* ./
RUN corepack enable && \
if [ -f pnpm-lock.yaml ]; then \
corepack prepare pnpm@latest --activate; \
pnpm install --frozen-lockfile; \
elif [ -f yarn.lock ]; then \
yarn install --frozen-lockfile; \
else \
npm ci; \
fi
COPY . .
RUN NEXT_PUBLIC_SUPABASE_URL=${NEXT_PUBLIC_SUPABASE_URL} \
NEXT_PUBLIC_SUPABASE_ANON_KEY=${NEXT_PUBLIC_SUPABASE_ANON_KEY} \
next build
# ── Stage 2: Production runtime ────────────────────────────────────────
FROM node:22-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production \
NEXT_TELEMETRY_DISABLED=1 \
PORT=3000 \
HOSTNAME="0.0.0.0"
COPY --from=builder /app/public ./public
COPY --from=builder --chown=node:node /app/.next/standalone ./
COPY --from=builder --chown=node:node /app/.next/static ./.next/static
USER node
EXPOSE 3000
CMD ["node", "server.js"]
+4 -1
View File
@@ -3,8 +3,9 @@ import {
PROMPT_VERSIONS,
DEFAULT_PROMPT_VERSION,
} from "@/lib/analysis";
import { withAuthenticatedApi } from "@/lib/supabase/api-auth.js";
export async function POST(request) {
async function post(request) {
try {
const body = await request.json();
@@ -47,3 +48,5 @@ export async function POST(request) {
);
}
}
export const POST = withAuthenticatedApi(post);
+71
View File
@@ -0,0 +1,71 @@
/**
* Investigation Overview synthesis API route.
*
* Route: POST /api/cases/overview
*
* Thin route pattern — no overview business logic here.
*/
import { getProvider, getProviderModelName } from "@/lib/llm/provider.js";
import { synthesizeInvestigationOverview } from "@/lib/graph/investigation-overview-synthesis.js";
import { withAuthenticatedApi } from "@/lib/supabase/api-auth.js";
async function post(request) {
try {
const body = await request.json();
if (!body || typeof body !== "object") {
return Response.json(
{ success: false, stage: "request_validation", error: "Invalid request body" },
{ status: 400 }
);
}
const { situationGraph, findings, plausibleInterpretations } = body;
if (!situationGraph) {
return Response.json(
{ success: false, stage: "request_validation", error: "Missing situationGraph" },
{ status: 400 }
);
}
const result = await synthesizeInvestigationOverview(
{ situationGraph, findings, plausibleInterpretations },
{
provider: getProvider(),
modelName: getProviderModelName(),
}
);
return Response.json(
{ success: true, understanding: result.understanding, plausibleInterpretations: result.plausibleInterpretations },
{ status: 200 }
);
} catch (error) {
if (error instanceof SyntaxError) {
return Response.json(
{ success: false, stage: "request_validation", error: "Invalid JSON request body" },
{ status: 400 }
);
}
if (error.statusCode) {
return Response.json(
{
success: false,
stage: error.statusCode === 400 ? "request_validation" : "provider",
error: error.message ?? "Overview synthesis failed",
},
{ status: error.statusCode }
);
}
return Response.json(
{ success: false, stage: "internal", error: "Internal server error" },
{ status: 500 }
);
}
}
export const POST = withAuthenticatedApi(post);
+32 -3
View File
@@ -1,6 +1,7 @@
import { startCase } from "@/lib/graph/orchestrator.js";
import { withAuthenticatedApi } from "@/lib/supabase/api-auth.js";
export async function POST(request) {
async function post(request) {
try {
const body = await request.json();
const result = await startCase(body);
@@ -16,17 +17,43 @@ export async function POST(request) {
? result.statusCode
: 500;
const diagnostics = {
status,
error: result.error ?? "Start case failed",
validationErrors: result.validationErrors,
analysisErrors: result.analysisErrors,
validationIssues: result.validationIssues,
providerApiPath: result.providerApiPath,
providerExecution: result.providerExecution,
rawResponse: result.rawResponse ?? undefined,
};
if (status >= 500) {
console.error("[api/cases/start] error response", diagnostics);
} else {
console.warn("[api/cases/start] error response", diagnostics);
}
return Response.json(
{
success: false,
error: result.error ?? "Start case failed",
error: diagnostics.error,
validationErrors: result.validationErrors,
diagnostics: result.diagnostics,
analysisErrors: result.analysisErrors,
validationIssues: result.validationIssues,
providerApiPath: result.providerApiPath,
providerExecution: result.providerExecution,
rawResponse: result.rawResponse ?? undefined,
},
{ status },
);
} catch {
} catch (error) {
console.error("[api/cases/start] unhandled exception", {
message: error instanceof Error ? error.message : String(error),
stack: error instanceof Error ? error.stack : undefined,
error,
});
return Response.json(
{
success: false,
@@ -36,3 +63,5 @@ export async function POST(request) {
);
}
}
export const POST = withAuthenticatedApi(post);
+74
View File
@@ -0,0 +1,74 @@
/**
* Dedicated Current Understanding synthesis API route.
*
* Route: POST /api/cases/synthesis
*
* Follows the thin route pattern established by cases/start and cases/update routes:
* parse request → invoke domain seam → return validated result → map failure status
*
* No synthesis business logic belongs in this file.
*/
import { getProvider, getProviderModelName } from "@/lib/llm/provider.js";
import { synthesizeCurrentUnderstanding } from "@/lib/graph/current-understanding-synthesis.js";
import { withAuthenticatedApi } from "@/lib/supabase/api-auth.js";
async function post(request) {
try {
const body = await request.json();
// ── Parse / validate input contract ───────────────────────
if (!body || typeof body !== "object") {
return Response.json(
{ success: false, stage: "request_validation", error: "Invalid request body" },
{ status: 400 }
);
}
const { situationGraph, findings } = body;
if (!situationGraph) {
return Response.json(
{ success: false, stage: "request_validation", error: "Missing situationGraph" },
{ status: 400 }
);
}
// ── Invoke domain seam with configured model ──────────────
const result = await synthesizeCurrentUnderstanding(
{ situationGraph, findings },
{
provider: getProvider(),
modelName: getProviderModelName(),
}
);
return Response.json({ success: true, currentUnderstanding: result.currentUnderstanding }, { status: 200 });
} catch (error) {
if (error instanceof SyntaxError) {
return Response.json(
{ success: false, stage: "request_validation", error: "Invalid JSON request body" },
{ status: 400 }
);
}
if (error.statusCode) {
return Response.json(
{
success: false,
stage: error.statusCode === 400 ? "request_validation" : "provider",
error: error.message ?? "Synthesis failed",
},
{ status: error.statusCode }
);
}
// Unexpected error
return Response.json(
{ success: false, stage: "internal", error: "Internal server error" },
{ status: 500 }
);
}
}
export const POST = withAuthenticatedApi(post);
+81 -2
View File
@@ -1,4 +1,8 @@
import { updateCase } from "@/lib/graph/orchestrator.js";
import { updateCase, reconsiderCompletedEpisode } from "@/lib/graph/orchestrator.js";
import { applyValidatedProposal } from "@/lib/graph/apply-proposal.js";
import { prepareCompletedEpisode } from "@/lib/graph/episode-preparation.js";
import { updateCaseEpisodeRequestSchema } from "@/lib/graph/schema.js";
import { withAuthenticatedApi } from "@/lib/supabase/api-auth.js";
function mapFailureStatus(result) {
switch (result?.stage) {
@@ -32,9 +36,28 @@ function buildFailureResponse(result) {
};
}
export async function POST(request) {
async function post(request) {
try {
const body = await request.json();
const isEpisodeMode = body?.episodeMode === true;
if (isEpisodeMode) {
const parsed = updateCaseEpisodeRequestSchema.safeParse(body);
if (!parsed.success) {
return Response.json(
{
success: false,
stage: "request_validation",
error: "Invalid episode request",
validationErrors: parsed.error.issues,
},
{ status: 400 },
);
}
return await handleEpisodeMode(body.situationGraph, body);
}
const result = await updateCase(body, { applyProposal: true });
if (result.success) {
@@ -66,3 +89,59 @@ export async function POST(request) {
);
}
}
export const POST = withAuthenticatedApi(post);
/** Server-side completed-episode reconsideration flow. */
async function handleEpisodeMode(situationGraph, body) {
const prepared = prepareCompletedEpisode({
situationGraph,
targetNodeId: body.targetNodeId,
contributions: body.contributions ?? [],
findings: body.findings,
});
if (!prepared?.turns?.length && !prepared?.eligibleCanonicalFindings?.length) {
return Response.json(
{ success: false, stage: "preparation", error: "no_episodic_content" },
{ status: 400 },
);
}
const reasoning = await reconsiderCompletedEpisode(prepared);
if (!reasoning.success) {
return Response.json(
buildFailureResponse(reasoning),
{ status: mapFailureStatus(reasoning) },
);
}
const application = await applyValidatedProposal({
situationGraph,
proposal: reasoning.proposal,
evidenceContext: {
isCompletedEpisode: true,
episodeEvidence: prepared,
},
});
if (!application.success) {
return Response.json(
buildFailureResponse(application),
{ status: mapFailureStatus(application) },
);
}
const resolvedNodeIds = new Set(application.updatedSituationGraph.resolvedNodeIds ?? []);
resolvedNodeIds.add(body.targetNodeId);
const updatedSituationGraph = {
...application.updatedSituationGraph,
resolvedNodeIds: [...resolvedNodeIds],
};
return Response.json({
success: true,
updatedSituationGraph,
proposal: reasoning.proposal,
}, { status: 200 });
}
@@ -0,0 +1,157 @@
import { getProvider, getProviderModelName } from "@/lib/llm/provider";
import {
buildFocusedDeconstructPrompt,
focusedDeconstructJsonSchema,
validateFocusedDeconstructSchema,
} from "@/lib/graph/focused-investigation";
import { withAuthenticatedApi } from "@/lib/supabase/api-auth.js";
async function post(request) {
let targetNodeId = null;
let startedAt = null;
try {
const body = await request.json();
targetNodeId = body.targetNodeId ?? null;
if (!body.targetNodeId || typeof body.targetNodeId !== "string") {
return Response.json(
{ error: "Request must include a 'targetNodeId' string field" },
{ status: 400 },
);
}
if (!body.targetLabel || typeof body.targetLabel !== "string") {
return Response.json(
{ error: "Request must include a 'targetLabel' string field" },
{ status: 400 },
);
}
if (!body.targetDescription || typeof body.targetDescription !== "string") {
return Response.json(
{ error: "Request must include a 'targetDescription' string field" },
{ status: 400 },
);
}
if (!body.centralStatement || typeof body.centralStatement !== "string") {
return Response.json(
{ error: "Request must include a 'centralStatement' string field" },
{ status: 400 },
);
}
if (!body.question || typeof body.question !== "string") {
return Response.json(
{ error: "Request must include a 'question' string field" },
{ status: 400 },
);
}
if (!body.answer || typeof body.answer !== "string") {
return Response.json(
{ error: "Request must include an 'answer' string field" },
{ status: 400 },
);
}
const prompt = buildFocusedDeconstructPrompt({
targetLabel: body.targetLabel,
targetDescription: body.targetDescription,
centralStatement: body.centralStatement,
question: body.question,
answer: body.answer,
});
const provider = getProvider();
const modelName = getProviderModelName();
startedAt = Date.now();
console.info("[api/focused-investigation/deconstruct] start", {
targetNodeId,
modelName,
providerMode: process.env.CONFIDENCE_ENGINE_EXPERIMENT_PROVIDER ?? "ollama",
startedAt,
});
const wrapper = await provider.generateReconstruction(
prompt,
modelName,
focusedDeconstructJsonSchema,
);
const elapsedMs = Date.now() - startedAt;
// Unwrap the semantic deconstruction from the provider envelope.
const deconstruction = wrapper.response;
console.info("[api/focused-investigation/deconstruct] provider success", {
targetNodeId,
elapsedMs,
providerApiPath: wrapper.providerApiPath ?? null,
responsePresent: Boolean(deconstruction),
responseKeys: deconstruction && typeof deconstruction === "object"
? Object.keys(deconstruction)
: [],
});
// Validate schema (required fields present, no graph-mutation fields)
const validationErrors = validateFocusedDeconstructSchema(deconstruction);
console.info("[api/focused-investigation/deconstruct] validation", {
targetNodeId,
schemaValid: validationErrors.length === 0,
responseKeys: deconstruction && typeof deconstruction === "object"
? Object.keys(deconstruction)
: [],
validationErrors,
});
if (validationErrors.length > 0) {
console.info("[api/focused-investigation/deconstruct] end", {
targetNodeId,
status: 502,
elapsedMs,
});
return Response.json(
{
success: false,
error: "Focused deconstruction result did not match expected schema",
validationErrors,
targetNodeId: body.targetNodeId,
elapsedMs,
},
{ status: 502 },
);
}
const response = Response.json({
success: true,
targetNodeId: body.targetNodeId,
observations: deconstruction.observations,
uncertainties: deconstruction.uncertainties,
assumptions: deconstruction.assumptions,
relationships: deconstruction.relationships,
possibleFollowUpQuestions: deconstruction.possibleFollowUpQuestions,
elapsedMs,
});
console.info("[api/focused-investigation/deconstruct] end", {
targetNodeId,
status: 200,
elapsedMs,
});
return response;
} catch (e) {
const elapsedMs = startedAt == null ? null : Date.now() - startedAt;
console.error("[api/focused-investigation/deconstruct] provider failure", {
targetNodeId,
elapsedMs,
errorName: e?.name ?? "Error",
errorMessage: e?.message ?? "Unknown server error",
statusCode: e?.statusCode ?? e?.status ?? null,
providerApiPath: e?.providerApiPath ?? null,
errorCode: e?.code ?? null,
errorParam: e?.param ?? null,
});
console.info("[api/focused-investigation/deconstruct] end", {
targetNodeId,
status: 500,
elapsedMs,
});
return Response.json(
{ error: e.message || "Unknown server error" },
{ status: 500 },
);
}
}
export const POST = withAuthenticatedApi(post);
@@ -0,0 +1,55 @@
import { formulateQuestionForTarget } from "@/lib/graph/focused-investigation";
import { withAuthenticatedApi } from "@/lib/supabase/api-auth.js";
async function post(request) {
try {
const body = await request.json();
if (!body.targetNodeId || typeof body.targetNodeId !== "string") {
return Response.json(
{ error: "Request must include a 'targetNodeId' string field" },
{ status: 400 },
);
}
if (!body.situationGraph || typeof body.situationGraph !== "object") {
return Response.json(
{ error: "Request must include a 'situationGraph' object field" },
{ status: 400 },
);
}
const result = formulateQuestionForTarget({
situationGraph: body.situationGraph,
targetNodeId: body.targetNodeId,
});
if (!result.success) {
return Response.json(
{ success: false, error: result.error },
{ status: 400 },
);
}
return Response.json({
success: true,
targetNodeId: result.targetNodeId,
question: result.question,
strategy: result.strategy,
reasoningPattern: result.reasoningPattern,
reasoningPatternReason: result.reasoningPatternReason,
reason: result.reason,
questionFamily: result.questionFamily,
selectedQuestionTemplate: result.selectedQuestionTemplate,
allowedQuestionFamilies: result.allowedQuestionFamilies,
rejectedQuestionFamilies: result.rejectedQuestionFamilies,
});
} catch (e) {
return Response.json(
{ error: e.message || "Unknown server error" },
{ status: 500 },
);
}
}
export const POST = withAuthenticatedApi(post);
@@ -0,0 +1,14 @@
import { restartInvestigation } from "@/lib/storage/server-investigation-persistence.js";
import { withAuthenticatedApi } from "@/lib/supabase/api-auth.js";
async function post(_request, { params }) {
try {
const snapshot = await restartInvestigation(params.id);
if (!snapshot) return Response.json({ error: "Investigation not found" }, { status: 404 });
return Response.json({ snapshot });
} catch {
return Response.json({ error: "Investigation persistence request failed" }, { status: 500 });
}
}
export const POST = withAuthenticatedApi(post);
+14
View File
@@ -0,0 +1,14 @@
import { loadInvestigation } from "@/lib/storage/server-investigation-persistence.js";
import { withAuthenticatedApi } from "@/lib/supabase/api-auth.js";
async function get(_request, { params }) {
try {
const snapshot = await loadInvestigation(params.id);
if (!snapshot) return Response.json({ error: "Investigation not found" }, { status: 404 });
return Response.json({ snapshot });
} catch {
return Response.json({ error: "Investigation persistence request failed" }, { status: 500 });
}
}
export const GET = withAuthenticatedApi(get);
+29
View File
@@ -0,0 +1,29 @@
import {
listInvestigations,
saveInvestigation,
} from "@/lib/storage/server-investigation-persistence.js";
import { withAuthenticatedApi } from "@/lib/supabase/api-auth.js";
async function get() {
try {
return Response.json({ investigations: await listInvestigations() });
} catch {
return Response.json({ error: "Investigation persistence request failed" }, { status: 500 });
}
}
async function post(request) {
try {
const { id, snapshot } = await request.json();
if (!id || !snapshot || typeof snapshot !== "object") {
return Response.json({ error: "An investigation id and snapshot are required" }, { status: 400 });
}
const savedSnapshot = await saveInvestigation(snapshot, id);
return Response.json({ snapshot: savedSnapshot }, { status: 200 });
} catch {
return Response.json({ error: "Investigation persistence request failed" }, { status: 500 });
}
}
export const GET = withAuthenticatedApi(get);
export const POST = withAuthenticatedApi(post);
+26
View File
@@ -0,0 +1,26 @@
import { createServerClient } from "@supabase/ssr";
import { NextResponse } from "next/server";
export async function GET(request) {
const requestUrl = new URL(request.url);
const code = requestUrl.searchParams.get("code");
const response = NextResponse.redirect(new URL("/", requestUrl.origin));
if (code) {
const supabase = createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY,
{
cookies: {
getAll: () => request.cookies.getAll(),
setAll(cookiesToSet) {
cookiesToSet.forEach(({ name, value, options }) => response.cookies.set(name, value, options));
},
},
},
);
await supabase.auth.exchangeCodeForSession(code);
}
return response;
}
+114
View File
@@ -2,6 +2,72 @@
@tailwind components;
@tailwind utilities;
:root {
--ce-page: #f9fafb;
--ce-surface: #ffffff;
--ce-surface-muted: #f9fafb;
--ce-surface-elevated: #ffffff;
--ce-text: #111827;
--ce-text-muted: #6b7280;
--ce-border: #d1d5db;
--ce-teal: #0f766e;
--ce-teal-surface: #f0fdfa;
--ce-focus: #14b8a6;
--ce-skeleton: #e5e7eb;
}
html[data-theme="dark"] {
--ce-page: #172128;
--ce-surface: #202c34;
--ce-surface-muted: #1b262e;
--ce-surface-elevated: #293740;
--ce-text: #edf2f3;
--ce-text-muted: #b4c0c5;
--ce-border: #40515a;
--ce-teal: #62d3c5;
--ce-teal-surface: #203a3c;
--ce-focus: #78ded2;
--ce-skeleton: #3a4a53;
}
html[data-theme="dark"] body { background-color: var(--ce-page) !important; color: var(--ce-text) !important; }
html[data-theme="dark"] .app-chrome { background-color: var(--ce-surface-muted); border-color: var(--ce-border); }
html[data-theme="dark"] .theme-toggle { background-color: var(--ce-surface-elevated); border-color: var(--ce-border); color: var(--ce-text); }
html[data-theme="dark"] .theme-toggle:hover { background-color: #33444d; }
html[data-theme="dark"] .bg-white,
html[data-theme="dark"] [class*="bg-white"] { background-color: var(--ce-surface) !important; }
html[data-theme="dark"] [class*="bg-gray-50"],
html[data-theme="dark"] [class*="bg-gray-100"] { background-color: var(--ce-surface-muted) !important; }
html[data-theme="dark"] [class*="bg-gradient-to"] { background-image: none !important; background-color: var(--ce-surface) !important; }
html[data-theme="dark"] [class*="bg-teal-50"] { background-color: var(--ce-teal-surface) !important; }
html[data-theme="dark"] [class*="bg-amber-50"],
html[data-theme="dark"] [class*="bg-orange-50"],
html[data-theme="dark"] [class*="bg-yellow-50"] { background-color: #3a3324 !important; }
html[data-theme="dark"] [class*="bg-green-50"] { background-color: #20392f !important; }
html[data-theme="dark"] [class*="bg-blue-50"] { background-color: #243540 !important; }
html[data-theme="dark"] [class*="border-gray"],
html[data-theme="dark"] [class*="border-teal"],
html[data-theme="dark"] [class*="border-amber"],
html[data-theme="dark"] [class*="border-orange"],
html[data-theme="dark"] [class*="border-green"],
html[data-theme="dark"] [class*="border-blue"] { border-color: var(--ce-border) !important; }
html[data-theme="dark"] :is(.text-gray-900, .text-gray-800, .text-gray-700, .text-gray-600) { color: var(--ce-text) !important; }
html[data-theme="dark"] :is(.text-gray-500, .text-gray-400) { color: var(--ce-text-muted) !important; }
html[data-theme="dark"] :is(.text-teal-700, .text-teal-800) { color: var(--ce-teal) !important; }
html[data-theme="dark"] :is(.text-amber-700, .text-amber-800, .text-orange-700, .text-orange-800, .text-yellow-700, .text-yellow-800) { color: #f2c879 !important; }
html[data-theme="dark"] :is(.text-green-700, .text-green-800) { color: #8bd8a8 !important; }
html[data-theme="dark"] input,
html[data-theme="dark"] textarea,
html[data-theme="dark"] select { background-color: var(--ce-surface-elevated); color: var(--ce-text); border-color: var(--ce-border); }
html[data-theme="dark"] input::placeholder,
html[data-theme="dark"] textarea::placeholder { color: #94a3ab; }
html[data-theme="dark"] details { background-color: var(--ce-surface-muted) !important; }
html[data-theme="dark"] button:focus-visible,
html[data-theme="dark"] a:focus-visible,
html[data-theme="dark"] input:focus-visible,
html[data-theme="dark"] textarea:focus-visible,
html[data-theme="dark"] select:focus-visible { outline: 2px solid var(--ce-focus); outline-offset: 2px; }
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
@@ -24,6 +90,50 @@
animation-delay: 0.16s;
}
/* ── CU skeleton overlay during synthesis refresh ─────────────── */
.cu-skeleton-overlay {
pointer-events: none;
}
.cu-skeleton-lines {
display: flex;
flex-direction: column;
align-items: center;
width: 100%;
margin-top: auto;
}
.cu-skeleton-line {
height: 16px;
border-radius: 8px;
background-color: var(--ce-skeleton);
position: relative;
overflow: hidden;
}
/* Striped shimmer that travels left → right through each bar */
.cu-skeleton-line::after {
content: "";
position: absolute;
inset: 0;
background: repeating-linear-gradient(
105deg,
transparent 0%,
transparent 8px,
rgba(255, 255, 255, 0.45) 8px,
rgba(255, 255, 255, 0.45) 16px,
transparent 16px,
transparent 24px
);
animation: cuSkeletonShimmer 1.6s linear infinite;
}
@keyframes cuSkeletonShimmer {
0% { transform: translateX(-100%); }
100% { transform: translateX(100%); }
}
@media (prefers-reduced-motion: reduce) {
[style*="animation:spin"] {
animation: none !important;
@@ -32,4 +142,8 @@
.investigation-card {
animation: none;
}
.cu-skeleton-line::after {
animation: none !important;
}
}
+65
View File
@@ -0,0 +1,65 @@
"use client";
import React from "react";
import { loadInvestigation } from "@/lib/storage/investigation-storage";
import ScenarioForm from "@/components/scenario-form";
import Link from "next/link";
import { useParams, useRouter } from "next/navigation";
import { useEffect, useState } from "react";
export default function InvestigationPage({ params }) {
const router = useRouter();
const routeId = typeof params?.id === "string" ? params.id : "";
const [existing, setExisting] = useState(null);
const [hydrated, setHydrated] = useState(false);
useEffect(() => {
let active = true;
setHydrated(false);
if (!routeId) { setHydrated(true); return; }
(async () => {
try {
const snapshot = await loadInvestigation(routeId);
if (active) setExisting(snapshot);
} finally {
if (active) setHydrated(true);
}
})();
return () => { active = false; };
}, [routeId]);
return (
<main className="mx-auto max-w-[1600px] px-6 py-12">
{/* Page-level navigation — owned by route, not ReasoningWorkspace */}
<nav className="mb-4 flex gap-3">
<Link
href="/"
className="rounded-lg border border-teal-600 bg-white px-4 py-2 text-sm font-medium text-teal-700 hover:bg-teal-50 transition"
>
Back to portfolio
</Link>
</nav>
<h1 className="mb-2 text-3xl font-bold tracking-tight">Confidence Engine</h1>
<p className="mb-8 text-sm text-gray-500">
Experimental prototype: enter a scenario and send it to a local LLM for
evidence-based structured reconstruction. This is a technical vertical
slice not a production system.
</p>
{!hydrated ? (
<p className="text-sm text-gray-500">Loading investigation</p>
) : existing ? (
<ScenarioForm
investigationId={routeId}
existingSnapshot={existing}
onNavigateToReport={() => router.push(`/investigations/${routeId}/report`)}
/>
) : (
<ScenarioForm
investigationId={routeId}
onNavigateToReport={() => router.push(`/investigations/${routeId}/report`)}
/>
)}
</main>
);
}
+248
View File
@@ -0,0 +1,248 @@
"use client";
import React, { useEffect, useRef, useState } from "react";
import { loadInvestigation, saveInvestigation } from "@/lib/storage/investigation-storage";
import Link from "next/link";
export default function ReportPage({ params }) {
const routeId = (typeof params === "object" && params?.id != null) ? String(params.id) : "";
const [existing, setExisting] = useState(null);
const [hydrated, setHydrated] = useState(false);
const [generationLoading, setGenerationLoading] = useState(false);
const [generationError, setGenerationError] = useState(false);
const [updateLoading, setUpdateLoading] = useState(false);
const generationAttempted = useRef(false);
useEffect(() => {
let active = true;
setHydrated(false);
(async () => {
try {
const snapshot = await loadInvestigation(routeId);
if (active) setExisting(snapshot);
} catch {
if (active) setGenerationError(true);
} finally {
if (active) setHydrated(true);
}
})();
return () => { active = false; };
}, [routeId]);
// First-generation: create report when none persists (v0.58)
useEffect(() => {
if (!hydrated) return;
if (existing?.investigationReport) return;
if (generationAttempted.current) return;
generationAttempted.current = true;
const situationGraph = existing?.situationGraph;
const findings = existing?.findings ?? [];
if (!situationGraph) {
setGenerationError(true);
return;
}
(async () => {
setGenerationLoading(true);
try {
const res = await fetch("/api/cases/overview", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ situationGraph, findings }),
});
if (!res.ok) {
setGenerationError(true);
return;
}
const data = await res.json();
if (data.success) {
/* ── v0.59a — provenance: record generation revision (does NOT change Investigation revision) ── */
const rev = existing?.investigationRevision ?? 0;
const reportData = { understanding: data.understanding, plausibleInterpretations: data.plausibleInterpretations, hasPlausibleInterpretations: true, generatedFromRevision: rev };
setExisting((p) => {
void saveInvestigation({ ...p, investigationReport: reportData })
.catch((error) => console.error("Investigation report save failed", error));
return { ...p, investigationReport: reportData };
});
} else {
setGenerationError(true);
}
} catch {
setGenerationError(true);
} finally {
setGenerationLoading(false);
}
})();
}, [hydrated, existing]);
// Manual Report update (v0.59b — freshness manual update)
const handleUpdateReport = async () => {
if (updateLoading) return;
setUpdateLoading(true);
const snap = await loadInvestigation(routeId);
const situationGraph = snap?.situationGraph;
const findings = snap?.findings ?? [];
const rev = snap?.investigationRevision ?? 0;
if (!situationGraph) {
setUpdateLoading(false);
return;
}
try {
const res = await fetch("/api/cases/overview", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ situationGraph, findings }),
});
if (!res.ok) {
setUpdateLoading(false);
return;
}
const data = await res.json();
if (data.success) {
const reportData = { understanding: data.understanding, plausibleInterpretations: data.plausibleInterpretations, hasPlausibleInterpretations: true, generatedFromRevision: rev };
setExisting((p) => {
void saveInvestigation({ ...p, investigationReport: reportData })
.catch((error) => console.error("Investigation report save failed", error));
return { ...p, investigationReport: reportData };
});
}
} catch {
/* failure: retain existing Report and updateAvailable state */
} finally {
setUpdateLoading(false);
}
};
const report = existing?.investigationReport || null;
const scenario = hydrated ? (existing?.scenario || "") : null;
const paragraphs = (report?.understanding || "")
.split("\n")
.filter(Boolean);
return (
<main className="mx-auto max-w-[800px] px-6 py-16">
<h1 className="mb-2 text-[15px] font-bold tracking-[.2em] uppercase text-teal-700/90">
Investigation Report
</h1>
{/* Report freshness — only when a Report exists */}
{report ? (
<div className="mt-6 flex items-center gap-3">
{existing?.investigationRevision === report.generatedFromRevision ? (
<span className="text-[11px] font-semibold tracking-wider uppercase text-teal-700/70">Current</span>
) : (
<div className="flex items-center gap-3">
<span className="text-[11px] font-semibold tracking-wider uppercase text-gray-500">Update available</span>
<span className="text-xs text-gray-400">The investigation has changed since this report was generated.</span>
<button
type="button"
onClick={handleUpdateReport}
disabled={updateLoading}
className="rounded-lg border border-teal-600 bg-white px-3 py-1.5 text-[11px] font-semibold tracking-wider uppercase text-teal-700 hover:bg-teal-50 transition disabled:opacity-40"
>
{updateLoading ? "Updating&#8230;" : "Update report"}
</button>
</div>
)}
</div>
) : null}
{/* Situation */}
{scenario && (
<div className="mt-8 rounded-xl border-[2.5px] border-teal-300/70 bg-gradient-to-b from-teal-50/60 to-white px-8 pt-6 pb-7 shadow-sm">
<h2 className="mb-3 text-[11px] font-bold tracking-[.18em] uppercase text-teal-700/70">
Situation
</h2>
<p className="text-base leading-relaxed text-gray-800 whitespace-pre-wrap">
{scenario}
</p>
</div>
)}
{/* What we understand */}
{report ? (
<>
{paragraphs.length > 0 ? (
paragraphs.map((p, i) => (
<div key={i} className="mt-6 rounded-xl border-[2.5px] border-teal-300/70 bg-gradient-to-b from-teal-50/60 to-white px-8 pt-6 pb-7 shadow-sm">
<h2 className="mb-3 text-[11px] font-bold tracking-[.18em] uppercase text-teal-700/70">
What we understand
</h2>
<p className="text-base leading-relaxed text-gray-800">{p}</p>
</div>
))
) : (
<div className="mt-6 rounded-xl border-[2.5px] border-teal-300/70 bg-gradient-to-b from-teal-50/60 to-white px-8 pt-6 pb-7 shadow-sm">
<h2 className="mb-3 text-[11px] font-bold tracking-[.18em] uppercase text-teal-700/70">
What we understand
</h2>
<p className="text-base leading-relaxed text-gray-800">{report.understanding || ""}</p>
</div>
)}
{/* What remains plausible — conditional */}
{report.hasPlausibleInterpretations && report.plausibleInterpretations ? (
<div className="mt-6 rounded-xl border-[2.5px] border-blue-300/70 bg-gradient-to-b from-blue-50/60 to-white px-8 pt-6 pb-7 shadow-sm">
<h2 className="mb-3 text-[11px] font-bold tracking-[.18em] uppercase text-blue-700/70">
What remains plausible
</h2>
<p className="text-base leading-relaxed text-gray-800 italic">
{report.plausibleInterpretations}
</p>
</div>
) : null}
</>
) : (
/* Skeleton / loading state when no persisted report exists */
<>
<div className="mt-8 rounded-xl border-[2.5px] border-gray-200 bg-gray-50/50 px-8 pt-6 pb-7 shadow-sm">
<h2 className="mb-3 text-[11px] font-bold tracking-[.18em] uppercase text-gray-400">
What we understand
</h2>
{generationLoading ? (
<div className="flex flex-col gap-3 py-2" aria-live="polite">
<span className="text-[11px] font-semibold tracking-wider text-gray-400 uppercase">Generating report&#8230;</span>
{[0, 1, 2].map((i) => (
<div key={i} className="h-4 w-full rounded animate-pulse" style={{ backgroundColor: "rgb(229 231 235)", animationDelay: `${i * 150}ms`, width: i === 1 ? "80%" : i === 2 ? "65%" : "90%" }} />
))}
</div>
) : generationError ? (
<p className="text-sm text-red-600">Report generation failed. You may try again from the Investigation page.</p>
) : null}
</div>
</>
)}
{/* Back to investigation */}
<div className="mt-10">
<Link
href={`/investigations/${routeId}`}
className="rounded-lg border border-teal-600 bg-white px-4 py-2 text-sm font-medium text-teal-700 hover:bg-teal-50 transition"
>
Back to investigation
</Link>
</div>
{/* Back to portfolio */}
<div className="mt-3">
<Link
href="/"
className="rounded-lg border border-teal-600 bg-white px-4 py-2 text-sm font-medium text-teal-700 hover:bg-teal-50 transition"
>
Back to portfolio
</Link>
</div>
</main>
);
}
+16
View File
@@ -1,4 +1,6 @@
import "./globals.css";
import ThemeToggle from "@/components/theme-toggle";
import LogoutButton from "@/components/logout-button";
export const metadata = {
title: "Confidence Engine",
@@ -9,6 +11,20 @@ export default function RootLayout({ children }) {
return (
<html lang="en">
<body className="min-h-screen bg-gray-50 text-gray-900">
<script
dangerouslySetInnerHTML={{
__html: `try { const saved = localStorage.getItem('confidence-engine-theme'); const theme = saved === 'dark' || saved === 'light' ? saved : (matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'); document.documentElement.dataset.theme = theme; document.documentElement.style.colorScheme = theme; } catch (_) {}`,
}}
/>
<header className="app-chrome border-b border-gray-200/80">
<div className="mx-auto flex max-w-[1600px] items-center justify-between px-6 py-3">
<span className="text-sm font-semibold tracking-wide text-teal-700">Confidence Engine</span>
<div className="flex items-center gap-2">
<ThemeToggle />
<LogoutButton />
</div>
</div>
</header>
{children}
</body>
</html>
+5
View File
@@ -0,0 +1,5 @@
import LoginForm from "@/components/login-form";
export default function LoginPage() {
return <LoginForm />;
}
+157 -7
View File
@@ -1,15 +1,165 @@
import ScenarioForm from "@/components/scenario-form";
"use client";
import React from "react";
import { listInvestigations, restartInvestigation } from "@/lib/storage/investigation-storage";
import Link from "next/link";
import { useRouter } from "next/navigation";
function Portfolio() {
const router = useRouter();
const [summaries, setSummaries] = React.useState([]);
const [hydrated, setHydrated] = React.useState(false);
const [loadError, setLoadError] = React.useState(null);
const [showRestartConfirm, setShowRestartConfirm] = React.useState(false);
React.useEffect(() => {
let active = true;
(async () => {
try {
const investigations = await listInvestigations();
if (active) setSummaries(investigations);
} catch (error) {
if (active) setLoadError(error);
} finally {
if (active) setHydrated(true);
}
})();
return () => { active = false; };
}, []);
export default function Home() {
return (
<main className="mx-auto max-w-[1600px] px-6 py-12">
<main className="mx-auto max-w-[640px] px-6 py-16">
<h1 className="mb-2 text-3xl font-bold tracking-tight">Confidence Engine</h1>
<p className="mb-8 text-sm text-gray-500">
Experimental prototype: enter a scenario and send it to a local LLM for
evidence-based structured reconstruction. This is a technical vertical
slice not a production system.
Investigator&apos;s notebook index of persisted investigations.
</p>
<ScenarioForm />
{/* Investigation collection */}
{summaries.length > 0 && (
<section className="mb-10">
<h2 className="mb-4 text-[13px] font-bold tracking-[.18em] uppercase text-teal-700/80">
Investigations
</h2>
{summaries.map((summary) => (
<div key={summary.id} className="rounded-xl border-[2.5px] border-teal-300/70 bg-gradient-to-b from-teal-50/60 to-white px-8 py-6 shadow-sm">
<p className="text-sm text-gray-700">
{summary.scenario || "Untitled investigation"}
</p>
<div className="mt-4 flex items-start gap-3 text-sm">
{summary.reportExists ? (
<div className="flex flex-col gap-1">
<Link
href={`/investigations/${summary.id}/report`}
className="rounded-lg border border-teal-600 bg-white px-4 py-2 font-medium text-teal-700 hover:bg-teal-50 transition"
>
View report
</Link>
{summary.reportGeneratedFromRevision === summary.investigationRevision ? (
<span className="text-[11px] font-semibold tracking-wider uppercase text-teal-700/70">
Current
</span>
) : (
<span className="text-[11px] font-semibold tracking-wider uppercase text-gray-500">
Update available
</span>
)}
</div>
) : null}
<Link
href={`/investigations/${summary.id}`}
className="self-start rounded-lg border border-teal-600 bg-white px-4 py-2 font-medium text-teal-700 hover:bg-teal-50 transition"
>
Continue investigation
</Link>
<button
onClick={() => setShowRestartConfirm(summary.id)}
className="self-start rounded-lg border border-red-400 bg-white px-4 py-2 font-medium text-red-700 hover:bg-red-50 transition"
>
Restart investigation
</button>
{showRestartConfirm === summary.id && (
<div
role="dialog"
aria-modal="true"
aria-labelledby={`restart-title-${summary.id}`}
className="fixed inset-0 z-50 flex items-center justify-center bg-black/40"
onClick={() => setShowRestartConfirm(null)}
>
<div
className="w-[420px] rounded-xl border border-gray-200 bg-white p-6 shadow-lg"
onClick={(e) => e.stopPropagation()}
>
<h2 id={`restart-title-${summary.id}`} className="mb-3 text-lg font-semibold">
Restart this investigation?
</h2>
<p className="mb-5 text-sm text-gray-600">
Your current investigation, findings, clarified questions, and report will be lost. Are you sure you want to continue?
</p>
<div className="flex justify-end gap-3">
<button
onClick={() => setShowRestartConfirm(null)}
className="rounded-lg border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 transition"
>
Cancel
</button>
<button
onClick={async () => {
setShowRestartConfirm(null);
try {
await restartInvestigation(summary.id);
setSummaries(await listInvestigations());
} catch (error) {
setLoadError(error);
}
}}
className="rounded-lg border border-red-400 bg-white px-4 py-2 text-sm font-medium text-red-700 hover:bg-red-50 transition"
>
Restart investigation
</button>
</div>
</div>
</div>
)}
</div>
</div>
))}
</section>
)}
{/* No investigations */}
{!hydrated && (
<section className="mb-10"><h2 className="mb-4 text-[13px] font-bold tracking-[.18em] uppercase text-teal-700/80">Investigations</h2><p className="text-sm text-gray-500 italic">Loading investigations</p></section>
)}
{hydrated && loadError && (
<section className="mb-10"><h2 className="mb-4 text-[13px] font-bold tracking-[.18em] uppercase text-teal-700/80">Investigations</h2><p className="text-sm text-red-600">Unable to load investigations.</p></section>
)}
{hydrated && !loadError && summaries.length === 0 && (
<section className="mb-10">
<h2 className="mb-4 text-[13px] font-bold tracking-[.18em] uppercase text-teal-700/80">
Investigations
</h2>
<p className="text-sm text-gray-500 italic">No investigations yet.</p>
</section>
)}
<button
onClick={(e) => {
e.preventDefault();
const id = crypto.randomUUID();
router.push(`/investigations/${id}`);
}}
className="rounded-lg border-[2.5px] border-dashed border-teal-400 px-6 py-3 text-sm font-medium text-teal-700 hover:bg-teal-50 transition"
>
+ Create new investigation
</button>
</main>
);
}
export default Portfolio;
+143
View File
@@ -0,0 +1,143 @@
/**
* Experimental branch switcher — RTO.25A
*
* Smallest branch representation needed to test passive late-result indication.
* Does NOT replace production branch navigation. Temporary fixture only.
*/
"use client";
import React, { useState, useEffect } from "react";
/* ── Keyframes (injected once via <style> at render) ───── */
const PulseStyle = () => (
<style>{`
@keyframes rto-pulse {
0%, 100% { opacity: 0.6; }
50% { opacity: 1; }
}
`}</style>
);
/* ── Status dot (passive new-result indicator) ─────────── */
function NewIndicator({ visible }) {
if (!visible) return null;
return (
<span
className="ml-2 inline-flex items-center"
title="Something new is available here"
aria-label="New result available"
>
<span
className="relative inline-block h-[8px] w-[8px]"
style={{ animation: "rto-pulse 3s ease-in-out infinite" }}
>
<span
className="absolute inset-0 rounded-full bg-blue-400/70"
aria-hidden="true"
/>
</span>
</span>
);
}
/* ── Single branch row ─────────────────────────────────── */
function BranchRow({ id, label, active, isNew, isPaused, origin, onClick }) {
const isActive = Boolean(active);
return (
<button
onClick={onClick}
disabled={isActive}
aria-current={isActive ? "page" : undefined}
className={`w-full flex items-start gap-2 rounded-md px-3 py-2 text-left transition text-sm ${
isActive
? "bg-blue-50/80 border border-blue-200/60 text-blue-900 font-medium"
: "text-gray-600 hover:bg-gray-100/70 hover:text-gray-800 border border-transparent"
} ${!isActive ? "cursor-pointer" : "cursor-default"}`}
>
{/* Active indicator — ● vs ○ */}
<span
className={`flex-none leading-none text-base ${
isActive ? "text-blue-500" : "text-gray-400"
}`}
aria-hidden="true"
>
{isActive ? "●" : "○"}
</span>
{/* Branch label + origin */}
<span className="flex-1 min-w-0">
<span className="truncate block">{label}</span>
{origin && (
<span className="block text-[11px] leading-tight text-gray-500/80 truncate" title={origin}>
{origin}
</span>
)}
</span>
{/* Passive indicators: pause + new */}
<span className="flex items-center gap-1.5 flex-none">
{!isActive && isPaused && (
<span
className="text-[10px] text-gray-400"
title="Done for now"
>
Paused
</span>
)}
{!isActive && <NewIndicator visible={isNew} />}
</span>
</button>
);
}
/* ── Card wrapper ────────────────────────────────────────── */
export default function ExperimentalBranchSwitcher({
branches = [],
activeBranchId,
branchNewResults = {},
branchPauseState = [],
onBranchSelect,
}) {
if (!branches.length) return null;
return (
<div
className="rounded-lg border border-gray-200/60 bg-gray-50/30 p-4"
role="radiogroup"
aria-label="Experimental branch switcher — RTO.25A"
>
{/* Label — clearly experimental */}
<h2 className="mb-1 text-[10px] font-semibold tracking-widest uppercase text-gray-600">
Branches{" "}
<span className="font-normal text-gray-500">(exp)</span>
</h2>
<p className="mb-3 text-[11px] font-medium leading-tight text-gray-500/80">
Browse branches. Current focus is preserved.
</p>
<div className="space-y-1" role="list" aria-label="Available branches">
{branches.map((branch) => (
<BranchRow
key={branch.id}
id={branch.id}
label={branch.label}
active={activeBranchId === branch.id}
isNew={Boolean(branchNewResults[branch.id])}
isPaused={branchPauseState.includes(branch.id)}
origin={branch.origin}
onClick={() => onBranchSelect?.(branch.id)}
/>
))}
</div>
</div>
);
}
export { PulseStyle };
+2 -2
View File
@@ -84,10 +84,10 @@ export default function InvestigationMap({ turnCount = 0 }) {
return (
<div className="rounded-lg border border-gray-200/60 bg-gray-50/30 p-4" role="region" aria-label="Investigation map preview">
<h2 className="mb-1 text-[11px] font-medium tracking-widest uppercase text-gray-300">
<h2 className="mb-1 text-[11px] font-semibold tracking-widest uppercase text-gray-500">
Investigation Map
</h2>
<p className="mb-3 text-xs text-gray-400/70">
<p className="mb-3 text-xs font-medium leading-tight text-gray-500/80">
Active investigation topics and their status.
</p>
@@ -196,7 +196,7 @@ function InvestigationSummaryPanelV2({ graph, selectedQuestion, result, updateSt
<div>
{stillInvestigating.length > 1 ? (
<>
<h3 className="mb-2 text-xs font-medium text-gray-400">Still investigating</h3>
<h3 className="mb-2 text-xs font-medium text-gray-500">Still investigating</h3>
<ul className="space-y-1.5">
{Object.entries(investigatingByGroup).map(([group, items]) => (
<li key={group}>
@@ -227,7 +227,7 @@ function InvestigationSummaryPanelV2({ graph, selectedQuestion, result, updateSt
{/* ── What we have learned ────────────────────────── */}
{known.length > 0 && (
<div>
<h3 className="mb-2 text-xs font-medium text-gray-400">What we know</h3>
<h3 className="mb-2 text-xs font-medium text-gray-500">What we know</h3>
<ul className="space-y-1.5">
{known.map((item, i) => (
<li key={i} className="flex items-start gap-2">
@@ -243,8 +243,8 @@ function InvestigationSummaryPanelV2({ graph, selectedQuestion, result, updateSt
{/* ── Quiet reasoning summary — secondary ─────────── */}
<div className="pt-2 border-t border-gray-200/40">
<p className="text-[10px] font-medium tracking-widest uppercase text-gray-300 mb-1.5">Reasoning</p>
<div className="flex flex-wrap gap-x-4 gap-y-1 text-xs text-gray-400">
<p className="text-[10px] font-semibold tracking-widest uppercase text-gray-500 mb-1.5">Reasoning</p>
<div className="flex flex-wrap gap-x-4 gap-y-1 text-xs text-gray-500">
{reasonEntries.map(([label, count]) => (
<span key={label}>
{count} {label}
@@ -47,7 +47,7 @@ function KnownSection({ title, items }) {
return (
<div>
<h3 className="mb-2 text-[11px] font-medium tracking-widest uppercase text-gray-400">
<h3 className="mb-2 text-[11px] font-medium tracking-widest uppercase text-gray-500">
{title}
</h3>
<ul className="space-y-1.5">
@@ -67,7 +67,7 @@ function InvestigatingSection({ title, items }) {
return (
<div>
<h3 className="mb-2 text-[11px] font-medium tracking-widest uppercase text-gray-400">
<h3 className="mb-2 text-[11px] font-medium tracking-widest uppercase text-gray-500">
{title}
</h3>
<ul className="space-y-1.5">
@@ -87,7 +87,7 @@ function ExplanationSection({ items }) {
return (
<div>
<h3 className="mb-2 text-[11px] font-medium tracking-widest uppercase text-gray-400">
<h3 className="mb-2 text-[11px] font-medium tracking-widest uppercase text-gray-500">
Possible explanations
</h3>
<ul className="space-y-1.5">
@@ -102,10 +102,10 @@ function QuietSummary({ text }) {
return (
<div className="pt-2 border-t border-gray-200/40">
<p className="text-[10px] font-medium tracking-widest uppercase text-gray-300 mb-1.5">
<p className="text-[10px] font-semibold tracking-widest uppercase text-gray-500 mb-1.5">
Investigation state
</p>
<p className="text-xs text-gray-400">{text}</p>
<p className="text-xs text-gray-500">{text}</p>
</div>
);
}
+1 -1
View File
@@ -127,7 +127,7 @@ function InvestigationSummaryPanel({ graph, selectedQuestion, result, updateStat
{/* Current understanding */}
{currentUnderstanding && (
<div>
<h3 className="mb-1 text-[11px] font-medium tracking-widest uppercase text-gray-400/70">
<h3 className="mb-1 text-[11px] font-semibold tracking-widest uppercase text-gray-500">
What we understand so far
</h3>
<p className="text-sm leading-relaxed text-gray-600">{currentUnderstanding}</p>
+45
View File
@@ -0,0 +1,45 @@
"use client";
import { useState } from "react";
import { createClient, magicLinkRedirectTo } from "@/lib/supabase/browser.js";
export default function LoginForm() {
const [email, setEmail] = useState("");
const [status, setStatus] = useState("idle");
const [error, setError] = useState("");
async function sendMagicLink(event) {
event.preventDefault();
setStatus("pending");
setError("");
const { error: signInError } = await createClient().auth.signInWithOtp({
email,
options: { emailRedirectTo: magicLinkRedirectTo(window.location.origin) },
});
if (signInError) {
setError("We could not send a magic link. Please try again.");
setStatus("idle");
return;
}
setStatus("sent");
}
return (
<main className="mx-auto flex min-h-[calc(100vh-57px)] max-w-[640px] items-center px-6 py-16">
<section className="w-full rounded-xl border-[2.5px] border-teal-300/70 bg-gradient-to-b from-teal-50/60 to-white px-8 py-9 shadow-sm">
<p className="mb-3 text-[11px] font-bold uppercase tracking-[.18em] text-teal-700/70">Welcome</p>
<h1 className="text-3xl font-bold tracking-tight">Confidence Engine</h1>
<p className="mt-3 text-sm leading-relaxed text-gray-600">Enter your email and we will send you a secure link to continue.</p>
<form className="mt-7 space-y-4" onSubmit={sendMagicLink}>
<label className="block text-sm font-medium text-gray-700" htmlFor="email">Email address</label>
<input id="email" type="email" autoComplete="email" required value={email} onChange={(event) => setEmail(event.target.value)} className="w-full rounded-lg border border-gray-300 px-4 py-3 text-sm focus:border-teal-600 focus:outline-none focus:ring-2 focus:ring-teal-400" />
<button type="submit" disabled={status === "pending"} className="rounded-lg bg-teal-700 px-5 py-2.5 text-sm font-medium text-white transition hover:bg-teal-600 disabled:cursor-wait disabled:opacity-60">
{status === "pending" ? "Sending magic link…" : "Send magic link"}
</button>
</form>
{status === "sent" && <p className="mt-5 text-sm text-green-700" role="status">Check your email for your magic link.</p>}
{error && <p className="mt-5 text-sm text-red-700" role="alert">{error}</p>}
</section>
</main>
);
}
+61
View File
@@ -0,0 +1,61 @@
"use client";
import { useEffect, useState } from "react";
import { createClient } from "@/lib/supabase/browser.js";
export default function LogoutButton() {
const [visible, setVisible] = useState(false);
const [error, setError] = useState("");
const [loggingOut, setLoggingOut] = useState(false);
useEffect(() => {
const client = createClient();
async function checkSession() {
const { data: { session } } = await client.auth.getSession();
setVisible(!!session);
}
checkSession();
const { data: { subscription } } = client.auth.onAuthStateChange((_event, session) => {
setVisible(!!session);
});
return () => subscription.unsubscribe();
}, []);
async function handleLogout() {
setLoggingOut(true);
setError("");
try {
const client = createClient();
await client.auth.signOut();
window.location.href = "/login";
} catch (err) {
setError("Could not logout. Try again.");
setLoggingOut(false);
}
}
if (!visible) return null;
return (
<div className="flex items-center gap-2">
<button
type="button"
onClick={handleLogout}
disabled={loggingOut}
aria-label="Logout"
className="rounded-lg border border-gray-300 px-3 py-2 text-sm font-medium text-gray-600 transition hover:bg-gray-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-teal-500 focus-visible:ring-offset-2 disabled:cursor-wait disabled:opacity-60"
>
{loggingOut ? "Logging out…" : "Logout"}
</button>
{error && (
<p className="text-xs text-red-600" role="alert">
{error}
</p>
)}
</div>
);
}
File diff suppressed because it is too large Load Diff
+486 -37
View File
@@ -5,6 +5,8 @@ import { useState, useRef, useMemo } from "react";
import DiagnosticsView from "@/components/diagnostics-view";
import ReasoningWorkspace, { LoadingOverlay, ContinueLaterBanner } from "@/components/reasoning-workspace";
import { mockFetch, AVAILABLE_SCENARIOS } from "@/lib/mocks/confidence-engine/mock-client";
import { deriveFindingsFromContributions, normalizeFindings } from "@/lib/graph/finding-helpers";
import { loadInvestigation, saveInvestigation, restartInvestigation } from "@/lib/storage/investigation-storage";
/* Compile-time env resolution — NEXT_PUBLIC_ vars are injected by Next.js at build */
const MOCK_ENABLED = process.env.NEXT_PUBLIC_CONFIDENCE_ENGINE_MOCKS === "true";
@@ -33,7 +35,7 @@ export async function submitScenarioForStartCase(fetchImpl, scenario) {
export async function submitAnswerForUpdateCase(
fetchImpl,
{ situationGraph, previousQuestion, answer },
{ situationGraph, previousQuestion, answer, findings },
) {
if (!answer?.trim()) {
return {
@@ -47,10 +49,15 @@ export async function submitAnswerForUpdateCase(
};
}
const body = { situationGraph, previousQuestion, answer };
if (findings && findings.length > 0) {
body.findings = findings;
}
const response = await fetchImpl("/api/cases/update", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ situationGraph, previousQuestion, answer }),
body: JSON.stringify(body),
});
return {
@@ -60,6 +67,19 @@ export async function submitAnswerForUpdateCase(
};
}
export async function synthesizeFromFindings(fetchImpl, { situationGraph, findings }) {
const response = await fetchImpl("/api/cases/synthesis", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ situationGraph, findings }),
});
return {
ok: response.ok,
data: await response.json(),
};
}
function normaliseStartResult(data) {
return {
...data,
@@ -186,28 +206,72 @@ export function UpdateErrorPanel({ updateError }) {
export { INITIAL_MESSAGES, UPDATE_MESSAGES, useLoadingStatus };
// ── Session key ────────────────────────────────────────────────
const SESSION_KEY = "confidence-engine-session";
function getSession() {
if (typeof sessionStorage === "undefined") return null;
try {
const raw = sessionStorage.getItem(SESSION_KEY);
return raw ? JSON.parse(raw) : null;
} catch (_) { return null; }
/**
* Derives whether the current component state represents a valid investigation
* context sufficient to render a workspace surface.
*
* Valid only when:
* - result carries a situationGraph (renderable graph), OR
* - status is "success" AND there is a non-empty scenario
* (from session restoration with real data).
*
* This predicate is the single source of truth for all render-gate decisions.
* showExperimentView, fixture availability, or sessionStorage keys alone are
* NOT sufficient to constitute valid context.
*/
export function hasValidInvestigationContext(result, status, scenario) {
return Boolean(result?.situationGraph) ||
(status === "success" && Boolean(scenario?.trim()));
}
function saveSession(state) {
if (typeof sessionStorage === "undefined") return;
try { sessionStorage.setItem(SESSION_KEY, JSON.stringify(state)); } catch (_) {}
/**
* Derives the primary surface that must render for the given state tuple.
* Enforces exactly-one-primary-surface invariant: no zero, no two.
*/
export function derivePrimarySurface(result, status, _showExperimentView, scenario, activeBranchId) {
if (status === "loading") return "LOADING";
if (status === "error") return "ERROR_SURFACE";
const valid = hasValidInvestigationContext(result, status, scenario);
if (valid) return "NORMAL_WORKSPACE";
return "SCENARIO_ENTRY";
}
function clearSession() {
if (typeof sessionStorage === "undefined") return;
try { sessionStorage.removeItem(SESSION_KEY); } catch (_) {}
/**
* Orchestrate the authoritative episode reconsideration flow.
* Exported for deterministic testing — domain functions and server endpoint accepted as parameters.
*/
export async function executeEpisodeDone({
resultSituationGraph,
targetNodeId,
focusedContributions,
findings,
episodeDoneServer,
synthesizeFn,
setResult: setAppState,
}) {
const serverResult = await episodeDoneServer({
situationGraph: resultSituationGraph,
targetNodeId,
contributions: focusedContributions ?? [],
findings,
});
if (!serverResult.success) {
return { success: false, stage: "episode_done", error: serverResult.error };
}
export default function ScenarioForm() {
const nextGraph = serverResult.updatedSituationGraph;
setAppState(prev => ({ ...(prev ?? {}), situationGraph: nextGraph }));
const synthesisResult = await synthesizeFn(nextGraph, findings);
return { success: true, nextGraph, synthesisResult };
}
export default function ScenarioForm({ investigationId, onNavigateToReport }) {
const [scenario, setScenario] = useState("");
const [status, setStatus] = useState("idle"); // idle | loading | error | success
const [result, setResult] = useState(null);
@@ -217,20 +281,345 @@ export default function ScenarioForm() {
const [updateResult, setUpdateResult] = useState(null);
const [lastSubmittedAnswer, setLastSubmittedAnswer] = useState("");
const [currentUnderstanding, setCurrentUnderstanding] = useState(null);
const [cuSynthesisLoading, setCuSynthesisLoading] = useState(false);
const [mockScenario, setMockScenario] = useState("");
const [hideFacilitatorOnLanding, setHideFacilitatorOnLanding] = useState(false);
/* ── v0.54b — investigation overview transient state ────── */
const [overviewState, setOverviewState] = useState(null);
const [overviewLoading, setOverviewLoading] = useState(false);
/* ── v0.55 — persisted investigation report (derived artefact) ── */
const [investigationReport, setInvestigationReport] = useState(null);
/* ── v0.59a — provenance: Investigation revision tracking ── */
const [investigationRevision, setInvestigationRevision] = useState(0);
/* ── in-flight gate for episode reconsideration on Done ──── */
const doneInProgressRef = useRef(false);
/* ── RTO.31: focused contributions ownership ─────────────── */
const [focusedContributions, setFocusedContributions] = useState([]);
/* ── v2 findings from focused contributions ─────────────── */
const [findings, setFindings] = useState([]);
const [hydrated, setHydrated] = useState(false);
function persist(snapshot) {
void saveInvestigation(snapshot).catch((error) => console.error("Investigation autosave failed", error));
}
function restartPersistedInvestigation() {
void restartInvestigation(investigationId)
.catch((error) => console.error("Investigation restart failed", error));
}
function appendFinding(finding) {
setFindings((prev) => {
return [...prev, finding];
});
}
function updateFindingDisposition(findingId, newDisposition) {
// Derive explicit next state — not a React-state reread.
const nextFindings = (findings ?? []).map((f) =>
f.id === findingId ? { ...f, userDisposition: newDisposition } : f,
);
setFindings(() => nextFindings);
// ── Synthesis trigger: completed canonical eligibility transition ──
const prevFinding = (findings ?? []).find((f) => f.id === findingId);
const previousDisposition = prevFinding?.userDisposition;
const notRelevantTransition =
previousDisposition !== "not_relevant" && newDisposition === "not_relevant";
const restoreTransition =
previousDisposition === "not_relevant" && newDisposition === null;
if (!notRelevantTransition && !restoreTransition) return;
/* ── v0.59a — provenance: eligible evidence set changed ── */
setInvestigationRevision((prev) => (prev ?? 0) + 1);
const currentGraph = result?.situationGraph;
if (!currentGraph) return;
void synthesizeFromFindings(fetch, {
situationGraph: currentGraph,
findings: normalizeFindings(nextFindings),
}).then((res) => {
if (res.ok && res.data?.currentUnderstanding) {
setCurrentUnderstanding(res.data.currentUnderstanding);
}
});
}
function updateFindingProposition(findingId, newProposition) {
// Derive explicit next state — not a React-state reread.
const nextFindings = (findings ?? []).map((f) =>
f.id === findingId ? { ...f, proposition: newProposition, userDisposition: null } : f,
);
/* ── v0.59a — provenance: no-op guard ── */
const prevFinding = (findings ?? []).find((f) => f.id === findingId);
if (prevFinding?.proposition === newProposition) return; // no semantic change
setFindings(() => nextFindings);
/* ── v0.59a — provenance: corrected Finding changes evidence ── */
setInvestigationRevision((prev) => (prev ?? 0) + 1);
// ── Synthesis trigger: corrected Finding → one reconstruction ──
const currentGraph = result?.situationGraph;
if (!currentGraph) return;
void synthesizeFromFindings(fetch, {
situationGraph: currentGraph,
findings: normalizeFindings(nextFindings),
}).then((res) => {
if (res.ok && res.data?.currentUnderstanding) {
setCurrentUnderstanding(res.data.currentUnderstanding);
}
});
}
/**
* Authoritative graph reconsideration triggered by "Done for now"
* activity boundary. Delegates to the exported executeEpisodeDone pipeline.
*/
async function handleDoneForNowPromotion(targetNodeId, onImmediateGraphUpdate) {
if (!targetNodeId) return;
// Gate: only invoke episode processing when the active target has focused contributions.
// Scenario-wide findings no longer determine whether an empty target enters episode processing.
const hasActiveTargetContent = (focusedContributions ?? []).some(
(c) => c.targetNodeId === targetNodeId || c.originatingTargetNodeId === targetNodeId,
);
if (!hasActiveTargetContent) return;
// In-flight guard: exactly-once enforcement
if (doneInProgressRef.current) return;
doneInProgressRef.current = true;
/* ── Immediate client transition — before awaiting async work ── */
const preDoneGraph = result?.situationGraph;
if (preDoneGraph && onImmediateGraphUpdate) {
const immediateResolvedIds = new Set(preDoneGraph.resolvedNodeIds || []);
immediateResolvedIds.add(targetNodeId);
const immediateGraph = {
...preDoneGraph,
resolvedNodeIds: Array.from(immediateResolvedIds),
};
onImmediateGraphUpdate(immediateGraph);
}
setCuSynthesisLoading(true);
try {
const doneResult = await executeEpisodeDone({
resultSituationGraph: preDoneGraph,
targetNodeId,
focusedContributions: focusedContributions ?? [],
findings,
episodeDoneServer: (payload) =>
fetch("/api/cases/update", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ ...payload, episodeMode: true }),
}).then((res) => res.json()),
synthesizeFn: (graph, fn) => synthesizeFromFindings(fetch, { situationGraph: graph, findings: fn }),
setResult,
});
/* ── v0.59a — provenance: episode done is meaningful evidence change ── */
const nextRev = (investigationRevision ?? 0) + 1;
setInvestigationRevision(nextRev);
/* CU synthesis — install only on success */
if (doneResult?.synthesisResult?.ok && doneResult.synthesisResult.data?.currentUnderstanding) {
setCurrentUnderstanding(doneResult.synthesisResult.data.currentUnderstanding);
}
/* On synthesis failure: KEEP nextGraph, KEEP Findings, KEEP existing CU. Do NOT rollback. */
} finally {
doneInProgressRef.current = false;
setCuSynthesisLoading(false);
}
}
/**
* v0.54b/v0.55 — request investigation overview via the established POST /api/cases/overview seam.
* Produces a distinct Investigation Report: a derived artefact, not canonical reasoning state.
*/
async function handleRequestOverview() {
if (overviewLoading || !result?.situationGraph) return;
setOverviewLoading(true);
setOverviewState(null); // clear any previous overview before new request
const plausibleInput = (result.situationGraph?.reconstruction || {}).plausibleInterpretations ?? [];
const hasPlausibleInput = Array.isArray(plausibleInput) && plausibleInput.length > 0;
try {
const res = await fetch("/api/cases/overview", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({
situationGraph: result.situationGraph,
findings,
plausibleInterpretations: plausibleInput,
}),
}).then((r) => r.json());
if (res?.success && res?.understanding != null) {
setOverviewState(res);
// Persist as a derived artefact of this investigation
const rev = investigationRevision ?? 0;
const report = {
understanding: res.understanding,
plausibleInterpretations: hasPlausibleInput ? res.plausibleInterpretations ?? "" : "",
hasPlausibleInterpretations: hasPlausibleInput,
generatedFromRevision: rev,
};
setInvestigationReport(report);
// Trigger autosave to persist the report
void saveInvestigation({
id: investigationId,
scenario,
situationGraph: result.situationGraph,
selectedQuestion: result.selectedQuestion,
summary: currentUnderstanding,
updatedAt: new Date().toISOString(),
focusedContributions,
findings,
investigationReport: report,
investigationRevision: rev,
});
}
// On failure: do not clear existing CU, do not block further attempts
} finally {
setOverviewLoading(false);
}
}
function appendFocusedContribution(contribution) {
// Derive a single stored contribution object and use it for BOTH
// contribution storage AND Finding derivation so the same identity
// appears in focusedContributions[] and Finding.contributionId.
setFocusedContributions((prev) => {
const seq = prev.length + 1;
const storedContribution = { ...contribution, sequence: seq, id: `contrib-${String(seq).padStart(4, "0")}` };
// Derive Findings from the exact stored Contribution (not a separate approximation)
setFindings((prevFindings) => {
const newFindings = deriveFindingsFromContributions([storedContribution]).findings;
return normalizeFindings([...prevFindings, ...newFindings]);
});
return [...prev, storedContribution];
});
// ── Synthesis trigger: once per completed Finding transition ──
const newFindingsDelta = deriveFindingsFromContributions([
{
...contribution,
sequence: (focusedContributions?.length ?? 0) + 1,
id: `contrib-${String((focusedContributions?.length ?? 0) + 1).padStart(4, "0")}`,
},
]).findings;
if (newFindingsDelta.length === 0) return;
const currentGraph = result?.situationGraph;
if (!currentGraph) return;
void synthesizeFromFindings(fetch, {
situationGraph: currentGraph,
findings: normalizeFindings([...(findings ?? []), ...newFindingsDelta]),
}).then((res) => {
if (res.ok && res.data?.currentUnderstanding) {
setCurrentUnderstanding(res.data.currentUnderstanding);
}
});
}
const textareaRef = useRef(null);
/* Restore persisted session on mount (Phase 3) ─────────── */
/* ── Valid investigation predicate ─────────────────────── */
// Delegated to the exported utility below.
const validCtx = hasValidInvestigationContext(result, status, scenario);
/* Restore persisted session on mount ─────────── */
useEffect(() => {
if (typeof window === "undefined") return;
const saved = getSession();
if (!saved) return;
let active = true;
(async () => {
try {
const saved = investigationId ? await loadInvestigation(investigationId) : null;
if (!active || !saved) return;
const hasGraph = Boolean(saved.situationGraph);
setScenario(saved.scenario || "");
setResult(saved.situationGraph ? { ...saved, situationGraph: saved.situationGraph } : null);
setResult(hasGraph ? { ...saved, situationGraph: saved.situationGraph } : null);
setCurrentUnderstanding(saved.summary || null);
setFocusedContributions(saved.focusedContributions || []);
setFindings(saved.findings || []);
/* ── v0.55 — hydrate persisted investigation report ─── */
if (saved.investigationReport) {
setInvestigationReport(saved.investigationReport);
}
/* ── v0.59a — hydrate provenance revision ─────────── */
setInvestigationRevision(saved.investigationRevision ?? 0);
// Partial sessions (present but no graph) must NOT suppress the
// scenario-entry form. Only promote to success when there is actual
// investigation data to render.
if (hasGraph) {
setStatus("success");
}, []);
}
} finally {
if (active) setHydrated(true);
}
})();
return () => { active = false; };
}, [investigationId]);
/* ── Canonical autosave — persist whenever state changes (Phase 2) ── */
useEffect(() => {
if (typeof window === "undefined") return;
// Guard: no valid investigation yet → skip autosave during idle/start flows.
// Also prevents overwriting an existing saved investigation with the initial
// empty state of a fresh ScenarioForm instance (hydration race guard).
if (!hydrated || !result?.situationGraph) return;
persist({
id: investigationId,
scenario,
situationGraph: result.situationGraph,
selectedQuestion: result.selectedQuestion,
summary: currentUnderstanding,
updatedAt: new Date().toISOString(),
focusedContributions,
findings,
investigationReport,
investigationRevision,
});
}, [
scenario,
result?.situationGraph,
result?.selectedQuestion,
currentUnderstanding,
focusedContributions,
findings,
investigationReport,
investigationRevision, hydrated,
]);
/* Restore facilitator dismiss preference (Experiment 05) ─── */
useEffect(() => {
@@ -307,7 +696,9 @@ export default function ScenarioForm() {
setCurrentUnderstanding(data.summary ?? null);
const normalised = normaliseStartResult(data);
setResult(normalised);
saveSession({ scenario, situationGraph: normalised.situationGraph, selectedQuestion: normalised.selectedQuestion, summary: data.summary ?? null, updatedAt: new Date().toISOString() });
/* ── v0.59a — provenance: first meaningful change sets revision to 1 ── */
setInvestigationRevision(1);
persist({ id: investigationId, scenario, situationGraph: normalised.situationGraph, selectedQuestion: normalised.selectedQuestion, summary: data.summary ?? null, updatedAt: new Date().toISOString(), focusedContributions, findings: [], investigationReport, investigationRevision: 1 });
} else {
setStatus("error");
setCurrentUnderstanding(data.summary ?? null);
@@ -340,6 +731,7 @@ export default function ScenarioForm() {
situationGraph: result?.situationGraph,
previousQuestion: result?.selectedQuestion,
answer,
findings,
});
if (submission.skipped) {
@@ -352,17 +744,21 @@ export default function ScenarioForm() {
const outcome = submission.data;
if (submission.ok && outcome.success) {
// ── Derive explicit next canonical state (no React-state reread) ──
const nextGraph = outcome.updatedSituationGraph;
let nextFindings = [...findings];
if (outcome.appendedFindings && Array.isArray(outcome.appendedFindings)) {
nextFindings = [...nextFindings, ...outcome.appendedFindings];
}
setUpdateStatus("success");
setCurrentUnderstanding(
outcome.summary ? outcome.summary : currentUnderstanding,
);
setUpdateResult({
...outcome,
previousSituationGraph: result?.situationGraph ?? null,
});
setResult((current) => ({
...current,
situationGraph: outcome.updatedSituationGraph,
situationGraph: nextGraph,
selectedQuestion: normaliseUpdateSelectedQuestion(
outcome.selectedQuestion,
),
@@ -371,9 +767,25 @@ export default function ScenarioForm() {
.map((node) => node.id),
diagnostics: outcome.diagnostics,
}));
setFindings(nextFindings);
// ── Coalesced transition: one synthesis per successful update ──
void synthesizeFromFindings(fetch, {
situationGraph: nextGraph,
findings: normalizeFindings(nextFindings),
}).then((res) => {
if (res.ok && res.data?.currentUnderstanding) {
setCurrentUnderstanding(res.data.currentUnderstanding);
}
// On synthesis failure: graph/Findings already persisted, CU preserved, no retry.
});
setAnswer("");
// Persist after successful update turn
saveSession({ scenario, situationGraph: outcome.updatedSituationGraph, selectedQuestion: normaliseUpdateSelectedQuestion(outcome.selectedQuestion), summary: outcome.summary ?? currentUnderstanding, updatedAt: new Date().toISOString() });
// Persist after successful update turn — include explicit next state
/* ── v0.59a — provenance: meaningful change advances revision ── */
const nextRev = (investigationRevision ?? 0) + 1;
setInvestigationRevision(nextRev);
persist({ id: investigationId, scenario, situationGraph: nextGraph, selectedQuestion: normaliseUpdateSelectedQuestion(outcome.selectedQuestion), summary: currentUnderstanding, updatedAt: new Date().toISOString(), focusedContributions, findings: nextFindings, investigationReport, investigationRevision: nextRev });
} else {
setUpdateStatus("error");
setUpdateError(outcome);
@@ -386,7 +798,8 @@ export default function ScenarioForm() {
return (
<div className="space-y-6">
{status === "idle" && (
{/* ── Idle form for scenario input ─ */}
{!result?.situationGraph && status === "idle" && (
<form onSubmit={handleSubmit} className="space-y-6">
{/* Two-column landing workspace */}
@@ -419,7 +832,7 @@ export default function ScenarioForm() {
className="h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
/>
<label htmlFor="dismiss-facilitator" className="text-xs text-gray-500">
Don't show this introduction again
{`Dismiss this introduction permanently`}
</label>
</div>
</div>
@@ -428,7 +841,7 @@ export default function ScenarioForm() {
{/* Right panel — Workspace (2/3 on desktop) */}
<div className={hideFacilitatorOnLanding ? "md:col-span-3" : "md:col-span-2"}>
<h2 className="mb-4 text-xs font-bold tracking-widest uppercase text-gray-400">Tell me what's happening</h2>
<h2 className="mb-4 text-xs font-bold tracking-widest uppercase text-gray-400">What&#39;s the situation</h2>
<textarea
ref={textareaRef}
value={scenario}
@@ -505,13 +918,24 @@ export default function ScenarioForm() {
/>
)}
{/* ── Main result workspace ─────────────────────── */}
{((status === "success" || status === "error") && status !== "loading") && (
{/* ── Main result workspace ─── */}
{(status === "success" || status === "error") && (
<>
<div className="grid grid-cols-1 gap-6 lg:grid-cols-3">
{/* Workspace — uses result from Analyse or Update only */}
<div className="lg:col-span-2">
<ReasoningWorkspace
scenario={scenario}
status={status}
updateStatus={updateStatus}
cuSynthesisLoading={cuSynthesisLoading}
currentUnderstanding={currentUnderstanding}
/* v0.54b investigation overview transient state */
overviewState={overviewState}
setOverviewState={setOverviewState}
overviewLoading={overviewLoading}
handleRequestOverview={handleRequestOverview}
onNavigateToReport={onNavigateToReport}
result={{
...(result || {}),
situationGraph: updateResult?.updatedSituationGraph ?? result?.situationGraph,
@@ -524,8 +948,25 @@ export default function ScenarioForm() {
setAnswer={setAnswer}
onAnswerSubmit={handleUpdate}
lastSubmittedAnswer={lastSubmittedAnswer}
focusedContributions={focusedContributions}
onFocusedContribution={appendFocusedContribution}
findings={findings}
onUpdateFindingDisposition={updateFindingDisposition}
onUpdateFindingProposition={updateFindingProposition}
/* v0.49 done-for-now promotion seam */
onSummaryUpdate={handleDoneForNowPromotion}
/* immediate graph transition (Done acknowledged before async) */
onImmediateGraphChange={(nextGraph) => setResult((prev) => ({ ...(prev ?? {}), situationGraph: nextGraph }))}
/* v0.59a provenance tracking */
investigationRevision={investigationRevision}
onSituationGraphChange={(nextGraph) => {
const nextRev = (investigationRevision ?? 0) + 1;
setInvestigationRevision(nextRev);
setResult((prev) => ({ ...(prev ?? {}), situationGraph: nextGraph }));
}}
onRestart={() => {
clearSession();
restartPersistedInvestigation();
setInvestigationRevision(0);
setStatus("idle");
setResult(null);
setAnswer("");
@@ -534,13 +975,18 @@ export default function ScenarioForm() {
setLastSubmittedAnswer("");
setCurrentUnderstanding(null);
setUpdateError(null);
setFocusedContributions([]);
setFindings([]);
}}
/>
</div>
</div>
</>
)}
{/* ── Continue later banner when session was restored ── */}
{status === "success" && result?.updatedAt && (
<ContinueLaterBanner onRestart={() => { clearSession(); setStatus("idle"); setResult(null); setAnswer(""); setUpdateStatus("idle"); setCurrentUnderstanding(null); }} />
<ContinueLaterBanner onRestart={() => { restartPersistedInvestigation(); setInvestigationRevision(0); setStatus("idle"); setResult(null); setAnswer(""); setUpdateStatus("idle"); setCurrentUnderstanding(null); setFocusedContributions([]); setFindings([]); }} />
)}
{/* Reset button after successful analysis */}
@@ -548,7 +994,8 @@ export default function ScenarioForm() {
<div className="text-center">
<button
onClick={() => {
clearSession();
restartPersistedInvestigation();
setInvestigationRevision(0);
setScenario("");
setStatus("idle");
setResult(null);
@@ -558,6 +1005,8 @@ export default function ScenarioForm() {
setLastSubmittedAnswer("");
setCurrentUnderstanding(null);
setUpdateError(null);
setFocusedContributions([]);
setFindings([]);
}}
className="rounded-lg border border-gray-200/60 px-4 py-2 text-sm font-medium text-gray-500 transition hover:bg-gray-50/80"
>
+47
View File
@@ -0,0 +1,47 @@
"use client";
import { useEffect, useState } from "react";
import {
readThemePreference,
resolveTheme,
saveThemePreference,
toggleTheme,
} from "@/lib/theme-preference.js";
function applyTheme(theme) {
document.documentElement.dataset.theme = theme;
document.documentElement.style.colorScheme = theme;
}
export default function ThemeToggle() {
const [theme, setTheme] = useState("light");
useEffect(() => {
const nextTheme = resolveTheme({
savedTheme: readThemePreference(window.localStorage),
systemPrefersDark: window.matchMedia?.("(prefers-color-scheme: dark)").matches,
});
setTheme(nextTheme);
applyTheme(nextTheme);
}, []);
const switchTheme = () => {
const nextTheme = toggleTheme(theme);
setTheme(nextTheme);
saveThemePreference(nextTheme, window.localStorage);
applyTheme(nextTheme);
};
const isDark = theme === "dark";
return (
<button
type="button"
onClick={switchTheme}
aria-label={isDark ? "Switch to light mode" : "Switch to dark mode"}
className="theme-toggle rounded-lg border px-3 py-2 text-sm font-medium transition focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-teal-500 focus-visible:ring-offset-2"
>
<span aria-hidden="true" className="mr-1.5">{isDark ? "☀" : "☾"}</span>
{isDark ? "Light mode" : "Dark mode"}
</button>
);
}
@@ -211,6 +211,160 @@ Given the useful investigation structure the Engine can already derive, how shou
The next phase should begin from this methodology question, not from a preselected technical solution.
## Granular Answer-Fragment Learning (RTO.1417)
Recent experiments explored what happens when further answers are made inside the same focused investigation (RTO.1417).
### What RTO.1417 proved
The experiments demonstrated that an LLM can:
- Retain prior focused knowledge across turns
- Revise uncertainty in response to new information
- Separate focused understanding from decision significance
- Carry coherent reasoning across several turns inside a single investigation
This learning was valuable and should be preserved as experimental evidence. The apparatus created during RTO.1417 remains available and relevant.
### What RTO.1417 began recreating
Pushing that design further exposed that we had reproduced the original structural assumption at a lower level:
- **Original global pattern:**
```text
whole case state + new answer → LLM rewrites whole case state
```
- **Focused version (RTO.1417):**
```text
whole focused-investigation state + new answer → LLM rewrites whole focused-investigation state
```
The second version is much smaller and technically better, but it is still the same cumulative reconstruction pattern — just at a lower scope. Prompt growth from later RTO experiments helped expose this.
**Learning:** Do not immediately respond by optimising or compressing the cumulative focused-state implementation. Reconsider whether accumulated state needs to be sent back through the LLM at all.
### The granular answer-fragment hypothesis (working hypothesis — not yet architecture)
The natural reasoning unit appears to be:
> **one question → one answer → one interpretation/capture**
Granularity's purpose is not merely token or latency optimisation. The small cycle is how the methodology makes a large problem manageable for the user. A difficult scenario is progressively decomposed into pieces small enough to reason about confidently.
The working hypothesis is:
```text
user chooses a question
→ user provides an answer
→ Engine deconstructs that answer
→ Engine captures the granular contribution
→ resulting uncertainties/questions are exposed
→ user chooses what to investigate next
→ repeat
```
Each accepted answer can produce a small evidence-bearing reasoning fragment. Those fragments are remembered outside the LLM call. The larger investigation understanding and eventual graph emerge from composing those pieces over time. Only directly relevant prior knowledge may need to be supplied when a specific earlier fragment is being qualified, contradicted or refined.
A software implementation may eventually represent granular contributions as things such as:
- observations
- uncertainties
- assumptions
- relationships
- questions raised
linked to the question/investigation that produced them. This illustrative list is not a production schema — it exists here only as a design hint.
### Memory / graph principle
The LLM does not necessarily need to own accumulated reasoning memory. The graph/state/notebook layer can remember the reasoning fragments. The LLM may be used to interpret a new answer, but a software implementation should not assume every new answer requires sending all accumulated investigation state back through the model and asking it to regenerate the whole current understanding.
### Optional capability: "Help me answer" / "Answer for me"
A software implementation may optionally offer something like:
> **Help me answer** or **Answer for me**
where the LLM proposes an answer. This is an optional application capability — not part of the core method. The methodology works without it.
**Ownership rule:** A generated answer is a proposal, not gospel and not automatically evidence. The user must be able to accept it, edit it or reject it. Only an accepted contribution enters the normal reasoning/deconstruction flow. Where practical, provenance should remain distinguishable between:
- user-supplied answer
- LLM-proposed answer accepted/edited by user
### Development principle reaffirmed: BUILD → BREAK → LEARN → STOP
When an experiment exposes that an architectural assumption is breaking:
```text
do not immediately optimise the broken assumption
do not add complexity to preserve it
capture what was learned
return to the methodology
design the next smallest experiment from that learning
```
RTO.1417 should therefore remain valuable evidence, not be deleted or described as mistakes. They helped reveal the next underlying assumption.
## Methodology test for future development
> **Could this reasoning operation be described in the Confidence Engine methodology and performed by a trained human facilitator without an LLM?**
- If YES: the application may use an LLM to automate, accelerate or scale it
- If NO: stop and ask whether the work is developing the Confidence Engine methodology or merely exploiting an LLM capability
This does not apply to implementation mechanics such as JSON, APIs or databases. It applies to the underlying reasoning behaviour.
## Recent experimental evidence supporting methodological principles (2026-08-18/19)
The following experiments provide specific evidence for the durable methodology principles
documented in `docs/current-working-principles.md`. Each is recorded as one data point, not generalisation.
### RTO.18 — Independent granular question/answer deconstruction (without accumulated state)
Independent per-turn question and answer deconstruction worked when each turn received only its own
question + answer, without any accumulated focused state from previous turns. This supports:
- **A3** (reasoning on meaning, not accumulated vocabulary)
- **A6** (non-linear investigation via independent fragments)
- **The granular answer-fragment hypothesis** as a working direction
### RTO.20 — Narrow derived current view from selected fragments + known relationship
A narrow, derived current understanding state worked when computed from selected fragments combined with known structural relationships rather than full-graph reconstruction. This supports:
- **A5** (deterministic structure for identity/storage; semantic interpretation only where needed)
- **A10** (progressive disclosure of relevant reasoning to the user)
### RTO.21 — Semantic relationship discovery: one genuine positive case
Semantic interpretation found one genuine cross-fragment relationship from two fragments alone. The operation correctly identified that two contributions meaningfully related without prior keyword dictionary matching. This supports:
- **A4** (semantic interpretation as a suitable facilitation capability)
- **A3** (meaning-based over vocabulary-based reasoning)
### RTO.22 — Semantic relationship discovery: one obvious negative case (control)
The same semantic operation correctly returned no relationship for one obviously unrelated pair of contributions. This supports:
- **A4** (semantic interpretation is useful but produces proposals, not decisions)
- **A3** (meaning-based reasoning does not produce false positives at high rates on obvious cases)
### RTO.23 — Current apparatus work
RTO.23 apparatus development is ongoing. No live experimental evidence exists for RTO.23 yet.
---
## Methodology principles reinforced by this evidence
The experiments above support (without proving) the following durable methodology boundaries:
- **Delivery-platform independence** (A1): all results were observed through a software delivery path, but the reasoning operations described (question deconstruction, relationship inference, fragment composition) are equally performable by a human facilitator.
- **Meaning over dictionary** (A3/A4): RTO.21 and RTO.22 together suggest semantic interpretation can produce both true-positive and true-negative relationship proposals without keyword scoring — but two data points do not establish reliability. The guardrail remains: treat all inferred relationships as proposals until handled per the delivery method.
- **Non-linear investigation** (A6/A7): independent fragment processing validates that reasoning can proceed asynchronously across branches without blocking the user.
- **Progressive disclosure** (A10): RTO.20 demonstrates that a derived narrow view from relevant fragments is more useful to the user than a full-graph reconstruction of everything known.
## Source basis
- `01_Confidence_Engine_Founding_Principles`
@@ -224,4 +378,11 @@ The next phase should begin from this methodology question, not from a preselect
- `Confidence_Engine_Project_Context_Update_2026-08-17`
- `Confidence_Engine_Current_Handoff_2026-08-17`
This context update distinguishes established project principles from current implementation learning. The workspace/user-directed investigation model is recorded as the current hypothesis to test, not as a completed replacement architecture.
> **Provenance note:** Some source-basis documents listed above were external
> project/session context supplied during the methodology work and are not
> repository-managed files. They informed this document's content but cannot be
> verified as originating from the Git history of this repository. Their role
> is to document where the methodology context came from, not to assert Git
> provenance for those external documents.
This context update distinguishes established project principles from current implementation learning. The workspace/user-directed investigation model is recorded as the current hypothesis to test, not as a completed replacement architecture. The granular answer-fragment hypothesis (RTO.1417) is recorded as working hypothesis, not yet accepted architecture.
+24
View File
@@ -15,6 +15,30 @@ All files below were moved from `docs/` on 2026-08-06 by Experiment 29 to reduce
| `docs/v0.7-observation-report.md` (136 lines) | `docs/archive/v0.7-observation-report.md` | Experimental observation snapshot from v0.7 UX work. | Useful as a reference but not a current working document. UX work is paused. | When reviewing past UX observations that may inform future interface design decisions. |
| `docs/archive/deferred-ux-backlog.md` (376 lines) | `docs/archive/deferred-ux-backlog.md` | Deferred and exploratory UX ideas from original `docs/backlog info.md` (lines 21390). Retained for historical reference. Not commitments, priorities or active tasks. | Superseded `docs/backlog info.md`. Deferred UX planning separated from mock reference in Experiment 31. | When a named past UX idea from the deferred backlog is being reviewed; not loaded by default. |
## Phase 2B Experiment Archives (2026-08-19)
All files below were classified `HISTORICAL_EVIDENCE + SAFE` during the Phase 1B/2B context audit and moved to reduce default reading burden while preserving full traceability. They are preserved evidence — not discarded, obsolete, or invalidated. Load only when a specific historical question requires them.
| Subdirectory | What Was Moved | Count |
|---|---|---|
| `docs/archive/experiments/reasoning-fidelity-v0.8/` | Experiment 56 family (reasoning-fidelity v0.8 pass) | 11 files (experiment-56am, excluding c) |
| `docs/archive/experiments/semantic-action-contract/` | Experiment 58 family (semantic action contract) | 8 files (experiment-58a1a6, b1b2) |
| `docs/archive/experiments/question-formulation/` | Experiment 59 family (question formulation) | 7 files (experiment-59a1a3, b1b4) |
| `docs/archive/experiments/decision-options/` | Experiment 60A family (decision options analysis) | 7 files (experiment-60a18, excluding a3) |
| `docs/archive/experiments/decision-closure-integration/` | Experiment 60B subfamilies {1015}, {5582}, {95,97,100} | 35 files (experiment-60b{10-15}, {55-56,58-82}, {95,97,100}) |
| `docs/archive/experiments/knowledge-mgmt/` | Cold-start validation historical evidence | 1 file (cold-start-validation.md) |
| `docs/archive/experiments/context-routing/` | Document-role review (classification/routing analysis) | 1 file (document-role-review.md) |
| `docs/archive/experiments/pre-RTO/` | Pre-Return-to-Origin experiments and version-specific docs: v0.5v0.7 | 7 files (pre-RTO experiments + release notes/UX pass) |
**Not moved in Phase 2B:** checkpoint-60b93.md, docs/design-evolution-log.md, docs/investigation-state-assessment*.md, architectural-principles.md, v0.6-reasoning-architecture.md, success-signals.md, failure-modes.md, investigation-narrative.md, behaviour-selection.md, orchestrator-contract.md, reasoning-contract-backlog.md, reasoning-refinement-requirements.md, reasoning-production-path-map.md.
**Phase 2D experiment archives (2026-08-19):** After Phase 2C carry-forward verification confirmed all three families SAFE for archival:
| Subdirectory | What Was Moved | Count |
|---|---|---|
| `docs/archive/experiments/post-v0.8-investigation/` | Experiment 57 family (post-v0.8 investigation) | 69 files (experiment-57* family) |
| `docs/archive/experiments/decision-closure-integration/` | Experiment 60B subfamilies {18}, {1948} | 37 files (experiment-60b{1-8}, experiment-60b{19-48}) |
## Superseded Files
The following files were superseded by a structured split in Experiment 31 and are no longer in use. Their contents remain fully represented in the documents below.

Some files were not shown because too many files have changed in this diff Show More