diff --git a/.claude/architecture-guardrails.md b/.claude/architecture-guardrails.md new file mode 100644 index 0000000..abd9c92 --- /dev/null +++ b/.claude/architecture-guardrails.md @@ -0,0 +1,77 @@ +# Architecture Guardrails + +## Hard boundary for UX tasks + +When a task is described as UI, UX, layout, styling, loading feedback or +presentation work, do not modify: + +- reasoning algorithms; +- unknown selection; +- reasoning-pattern selection; +- question formulation; +- atomicity or answerability assessment; +- graph mutation; +- graph schemas; +- API request or response contracts; +- reconstruction prompts; +- provider configuration; +- confidence propagation; +- compatibility validation. + +If a UX request appears to require one of those changes, stop and report the +dependency rather than changing it silently. + +## Reasoning invariants + +Preserve these invariants: + +- The LLM proposes information; deterministic code owns graph mutation. +- Every user-facing question comes from an explicit unresolved graph node. +- Questions contain one primary concept and seek one coherent answer. +- Unknowns must be atomic or decomposed. +- Atomic wording alone is insufficient; a selected unknown must be independently + answerable. +- Question family must match the active reasoning pattern. +- Active investigation nodes must be compatible with the reasoning pattern. +- Relationship classification cannot outrun comparability assessment. +- Ambiguity remains explicit rather than being resolved alphabetically. +- Parent unknowns do not resolve before their completion rule is satisfied. +- Confidence must not outrun evidence or completeness. +- Duplicate evidence must not increase confidence. +- Conflicting evidence caps conclusion confidence. +- A successful update must rerun deterministic next-question selection when + eligible unknowns remain. +- No question is preferable to an unjustified question. + +## Current architecture, simplified + +Scenario +→ reconstruction +→ situation graph +→ unknown selection +→ atomicity +→ answerability +→ reasoning pattern +→ investigation strategy +→ question family +→ question formulation +→ complexity validation +→ user answer +→ proposed graph update +→ deterministic validation/application +→ propagation +→ confidence/completeness update +→ next unknown + +## Compatibility discipline + +Do not expand schemas merely because a model emits a synonym. + +Prefer: + +1. identify the source; +2. determine whether it is a synonym; +3. normalise deterministically when justified; +4. retain strict validation. + +Do not weaken validation globally to fix a single malformed response. diff --git a/.claude/project-context.md b/.claude/project-context.md new file mode 100644 index 0000000..a21f8c2 --- /dev/null +++ b/.claude/project-context.md @@ -0,0 +1,110 @@ +# Project Context + +> **Start every resumed session with `docs/current-handoff.md`, then read `docs/current-project-state.md` and choose the relevant pack from `docs/task-context-packs.md`.** Use `docs/project-knowledge-inventory.md` to locate task-specific or historical context. Do not read the full design-evolution log unless a named experiment is required. Do not load `docs/archive/` by default; use `docs/archive/README.md` to locate historical evidence when specifically required. + +## What the Confidence Engine is + +The Confidence Engine is a structured reasoning tool intended to help people +decide whether they have enough justified confidence to act. + +It does not simply answer the user's original question. + +It: + +1. reconstructs the situation; +2. separates observations, assumptions, relationships and unknowns; +3. creates a structured reasoning graph; +4. selects the most useful unresolved uncertainty; +5. asks one simple question; +6. updates the graph from the answer; +7. repeats until action is justified or the remaining uncertainty is clear. + +A chatbot remembers the conversation. + +The Confidence Engine preserves the state of the reasoning. + +## Product direction + +The eventual product should feel like a calm, capable investigator helping the +user think one step at a time. + +The user should not need to understand: + +- graph theory; +- node IDs; +- internal enums; +- schemas; +- prompt versions; +- proposal validation; +- model-provider details. + +Those remain available through developer/debug views. + +## Core product promise + +The engine should help a user reach one of these states: + +- I have enough justified confidence to act. +- I do not yet have enough confidence, but I know what to investigate next. +- I have discovered that my original question needs reframing. + +## Current development stage + +The deterministic reasoning architecture reached a stable alpha checkpoint. + +Current work is primarily improving: + +- usability; +- presentation; +- loading feedback; +- plain-language explanations; +- separation of user and developer views. + +Do not resume broad reasoning architecture work unless a repeated observed +failure clearly requires it. + +## Important philosophy + +Complicated situations are made from smaller parts. + +Each part may influence the whole, but parts do not necessarily carry equal +weight. + +Previous cases may suggest where to investigate, but they must never determine +the outcome of a new case. + +Every case begins with no accepted evidence from previous cases. + +## Product Principle: TL;DR First + +The Confidence Workspace is not a document viewer or chat transcript. It is an active investigation workspace. + +At any point, the interface should allow a user returning after seconds, minutes or hours to understand where they are within a few seconds. + +The workspace should always answer: + +1. What is the situation? +2. What have we established? +3. What is the single most important thing to determine next? +4. Why does that matter? +5. How close are we to having sufficient confidence? + +The interface should minimise cognitive load by presenting the current state first and allowing progressively deeper exploration only when requested. + +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) + +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. + +## Routing Notes + +Read `docs/current-working-principles.md` for current guidance. Treat `docs/architectural-principles.md` as a broader task-specific reference, not a statement of current implementation. + +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`. + +> 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. diff --git a/.claude/ux-guidelines.md b/.claude/ux-guidelines.md new file mode 100644 index 0000000..823edfe --- /dev/null +++ b/.claude/ux-guidelines.md @@ -0,0 +1,601 @@ +# UX Guidelines + +## Main principle + +The user should see the next useful step clearly. + +The system may retain considerable complexity underneath, but the primary +workspace should remain calm and understandable. + +## Main user view + +Prioritise: + +1. Your situation +2. Current understanding +3. What we are working out +4. Why it matters +5. Next question +6. Answer field +7. Reasoning progress + +## Developer view + +Keep technical details behind a collapsed `Developer details` disclosure. + +This may contain: + +- complete situation graph; +- graph counts; +- nodes and edges; +- affected and resolved nodes; +- diagnostics; +- proposal details; +- raw JSON; +- prompt and model details; +- technical confidence data. + +Do not remove the developer view. It remains important while the product is +being tested. + +## Language + +Use plain language. + +Prefer: + +- `areas that still need investigation` +- `what we are working out` +- `why this matters` +- `what we understand so far` +- `next question` + +Avoid in the main view: + +- unknown nodes; +- unresolved candidates; +- activeUnknownNodeId; +- graph references; +- proposal compatibility; +- candidate count; +- internal enum values; +- raw IDs. + +Never display an unexplained count such as: + +`3 remaining` + +Explain what the count represents, or omit it. + +Do not imply that one unresolved graph node always equals one remaining user +question. + +## Loading experience + +Analysis and update requests can take around a minute with the current local +model. + +A disabled button is not sufficient feedback. + +Show a visible processing card immediately. + +Recommended initial-analysis messages: + +- 0–10 seconds: `Reading your situation` +- 10–25 seconds: `Building a structured understanding` +- 25–45 seconds: `Identifying what is known and still unclear` +- 45+ seconds: `Selecting the next useful question` + +Recommended update messages: + +- 0–10 seconds: `Considering your answer` +- 10–25 seconds: `Updating the situation` +- 25–45 seconds: `Checking what changed` +- 45+ seconds: `Choosing the next question` + +These messages are time-based reassurance only. + +Do not claim that a backend stage has completed unless the backend explicitly +reports it. + +Show elapsed time. + +Do not show fake progress percentages. + +Disable duplicate submission while a request is active. + +## Visual character + +Aim for: + +- calm; +- professional; +- spacious; +- accessible; +- suitable for business, consultancy and government users. + +Prefer: + +- clear hierarchy; +- restrained colour; +- generous whitespace; +- readable line lengths; +- consistent cards; +- accessible contrast; +- responsive layouts. + +Avoid: + +- visual clutter; +- excessive badges; +- neon colour; +- unnecessary gradients; +- glassmorphism; +- distracting animation; +- dashboard-style density. + +The next question should be the strongest visual element. + +## Workspace Layout Philosophy + +The Confidence Engine is a workspace, not a document. + +Documents optimise for reading from top to bottom. + +Workspaces optimise for allowing related information to be visible simultaneously. + +As investigations become larger, users should not be forced into unnecessary +vertical scrolling simply because horizontal space is available. + +Layout decisions should always ask: + +> "How much useful investigation context can be seen at one time?" + +rather than: + +> "How narrow can the content column be?" + +### Principles + +- **Active investigation remains the primary focus.** The current question and response form are always fully visible first. +- **Frequently referenced information should remain visible.** Understanding and Investigation Map should be scannable without scrolling away from the active question. +- **Reference material may share horizontal space on larger displays.** Situation and History can sit side-by-side when there is room. +- **Layout should adapt to available space without changing the investigation flow.** The same information is always present; only its arrangement changes. +- **Mobile and tablet continue to use a stacked single-column layout.** No progressive disclosure at small sizes — every section remains accessible by scrolling, just as it always has been. +- **Desktop progressively exposes more simultaneous context.** Instead of simply adding whitespace, wider screens reveal horizontal relationships between related panels. + +### Desktop layout model (wide screens) + +``` +┌───────────────────── full-width ─────────────────────┐ +│ Investigation Summary │ +├───────────────────────────────────────────────────────┤ +│ Active Workspace │ Working Memory │ +│ (full width) │ Understanding Map │ +│ Current Investigation │ │ +│ Response └─────────────────────────────────┘ +├───────────────────────────────────────────────────────┤ +│ Reference: Situation │ History │ +├───────────────────────────────────────────────────────┤ +│ Developer Details (always below) │ +└───────────────────────────────────────────────────────┘ +``` + +### Visual goal + +The page should feel less like a long report and more like an investigator's +workspace. The eye should be able to compare Understanding alongside Investigation Map without scrolling, and Situation alongside History in the same way. + +### What this phase does NOT include + +- No card redesigns. +- No new navigation. +- No account management or top bar. +- No tabs, collapsing layouts, resizable panes, floating panels, or masonry. +- No typography or colour changes. + +This is a layout-only phase. The reasoning engine should remain completely unaware of presentation decisions. + +## TL;DR Workspace Rules + +The newest state is the most important state. + +The primary focus of every screen should be the user's next action, not the history of how they arrived there. + +### Information hierarchy + +1. Current investigation +2. Why this matters +3. Response +4. Current understanding +5. Investigation history +6. Original situation +7. Developer details + +### Progressive disclosure + +Show only the information needed for the current decision. + +Everything else should be collapsible or secondary. + +### Cognitive load + +The user should never need to scan an entire page to discover: + +- what is happening +- what they need to do next +- why they are being asked + +These should always be immediately visible. + +### Investigation history + +History exists to provide confidence and traceability, not to compete with the current investigation. + +History should remain collapsed unless the user chooses to inspect previous reasoning. + +### Original situation + +Once an investigation has started, the original scenario becomes reference material rather than the primary focus. + +## Interaction Modes + +The Confidence Engine operates in two distinct modes. + +### Workspace Mode + +The user is reading, thinking, and providing information. + +The interface should: + +- present the current investigation +- allow the user to answer +- show the current understanding +- provide investigation history + +The workspace is interactive. + +--- + +### Reasoning Mode (initial analysis) + +The engine is constructing the first investigation from nothing. + +A full primary loading state appears: + +- prominent overlay with spinner, rotating status messages, elapsed timer; +- the entire workspace is replaced until reasoning completes; +- no partial or changing content is visible during processing. + +--- + +### Reasoning Mode (subsequent answers — localised) + +The investigation already exists. + +Only the active response panel is replaced by the loading card: + +- Current investigation question remains visible for context; +- Current understanding, Original situation, and Investigation history persist; +- Terminal state cards are suppressed during loading; +- The workspace layout remains stable and recognisable; +- Recovery states appear in place of the loading card if reasoning fails. + +The interface should: + +- clearly indicate that reasoning is in progress via the response-panel overlay; +- reassure the user that their answer has been accepted; +- avoid displaying partial or changing reasoning outside the response panel. + +--- + +### Transition + +Every submission follows the same lifecycle: + +User submits information +↓ +Loading card appears (full-page for initial analysis, localised for updates) +↓ +Updated workspace returns + +The interaction is consistent in intent — both modes confirm input acceptance and pause the active response area — but the page-level behaviour differs because one constructs from nothing while the other refines existing context. + +Users should never wonder whether their input has been accepted or whether the engine is still reasoning. + +## Workspace Polish (v0.7) + +The workspace should feel calm. Every visible element must justify its presence. + +Unknown values should usually be hidden rather than represented with placeholders. + +Whitespace is preferred over decorative UI. + +Prefer removing over adding. Prefer consistency over cleverness. + +Every section group should feel visually connected — spacing within a group is tighter than between groups. + +Labels should be brief. "Investigation History" → "History". "Your response" → "Response". The context already makes the meaning clear. + +Headings should be clean. Remove unnecessary subheadings that duplicate context. Remove uppercase labels from headings where they add visual noise without adding information. + +Cards should have consistent border radius, padding, and heading treatment across the workspace. + +An Investigation Map Preview should look provisional — lighter borders, muted text, subtle background — so the user knows it is a preview rather than completed content. + +## Entry Experience + +The landing page is not the investigation workspace. + +The landing page welcomes the user. + +The landing page explains what will happen. + +Complexity appears progressively. + +Users begin with observations rather than conclusions. + +The Confidence Engine behaves like a facilitator introducing a workshop — calm, patient, and focused on understanding before acting. + +## Facilitator Behaviour + +Orientation should support work, not interrupt it. + +The facilitator is present by invitation, not obligation. + +Returning users should control repeated guidance. + +The workspace should remain the primary visual focus. + +Information should naturally flow from left to right. + +## Attention Hierarchy + +The current task always owns the user's attention. + +Supporting information should remain available without competing. + +Visual emphasis should come primarily from hierarchy rather than colour. + +Reduce distraction before adding decoration. + +Calm interfaces improve reasoning. + +Hierarchy flows from strongest to quietest: + +1. The current investigation question (strongest visual element) +2. The response area (interactive, clear action) +3. Supporting context (visible but restrained) +4. Reference material (available, low priority) + +The workspace should feel like an active desk — the work in progress is prominent, supporting tools are within reach but not shouting for attention. + +## Input Expectations + +Input size communicates expected effort. + +Do not visually ask for more information than the engine currently needs. + +The initial situation is a starting observation, not a completed report. + +The engine should gather detail progressively through justified questions. + +Short inputs should feel valid. + +Users may still paste longer content when necessary. + +Meaning and state must never depend on colour alone. + +Similar interactions should look similar. + +Every investigation answer is a single observation. + +Response controls should communicate concise input unless the engine explicitly requests otherwise. + +Consistency reduces cognitive load. + +## Investigation Rhythm + +Principles: + +Every interaction should feel like the next natural step. + +The interface should never appear to stop thinking. + +Users should always know what just happened. + +Users should always know what happens next. + +The investigation should feel continuous rather than page-based. + +The conversation should flow naturally. + +## Conversation and Reference Lanes + +On desktop, the workspace splits into two persistent lanes: + +- The left lane (approximately two-thirds) is the active conversation area. +- The right lane (approximately one-third) holds supporting reference artefacts. + +The active conversation has a stable spatial home. Question, Response, and History form one continuous interaction lane. History grows downward beneath the active response. Each turn stays part of the same notebook within that lane. + +Supporting artefacts should remain spatially stable while the conversation grows. Desktop width should be used to preserve context, not merely enlarge cards. Text should not be truncated when sufficient readable space exists. + +Mobile remains a natural stacked flow with no horizontal split. + +## Facilitator Translation Layer (Experiment 11 — Emerging) + +The reasoning engine produces a rich graph with structured concepts (observations, unknowns, assumptions, relationships, metrics, states). The UI should increasingly become a translation layer over this graph rather than maintaining separate duplicated summaries. + +For end users, present the same data as: + +- **Known** — resolved nodes and established observations +- **Still investigating** — unresolved unknowns and assumptions to validate +- **Quiet reasoning summary** — raw counts (nodes, edges, etc.) visually secondary + +Internal graph concepts should remain available for developers (Developer Details) but should not dominate the primary view. The panel should feel like a facilitator's notebook: someone looking at it should immediately understand where the investigation stands, what has been learned, and what remains uncertain — without needing to understand graph theory. + +## Graph Projection + +The reasoning engine produces a rich graph with structured concepts (observations, unknowns, assumptions, relationships, metrics, states). The UI increasingly becomes a translation layer over this graph rather than maintaining separate duplicated summaries. + +This section records principles for projecting graph data into human-meaningful views. + +### Translation over exposure + +- The graph is internal structure; the UI communicates human meaning. +- User-facing panels should translate graph state rather than expose graph terminology. +- Display only the amount of graph information useful for the current task. + +### Epistemic clarity + +- Known information, uncertainty and assumptions must remain visibly distinct. +- Assumptions must never look like facts. +- Use explicit structural labels (e.g., "Possible explanation", "Not yet established") rather than relying on colour or implicit cues. + +### Curation as explanation + +- Prioritisation and omission are part of good explanation. +- Repeated scenario text should not dominate derived summaries. +- Complete technical detail remains available through Developer Details. + +### Robustness constraints + +- Meaning must remain understandable without relying on colour. +- Displayed content must be grounded in existing graph fields — never invent facts absent from the graph. +- When nothing useful is established, show calm fallback language rather than an empty panel or a fabricated summary. + +### Label hygiene + +- Prefer labels over descriptions when labels are clearer. +- Normalise text for deduplication (lowercase, trim, collapse whitespace). +- Omit items that are too verbose to scan; do not synthesise rewritten claims that change meaning. +- Avoid displaying graph identifiers, confidence values without context, or raw enum categories in user-facing views. + +### State-aware framing + +- The same panel must remain useful during early, active and terminal investigation states. +- Terminal state content should change its framing (e.g., "What the evidence supports" rather than "Still investigating") but not invent certainty. + +## Semantic Projection + +Experiment 13 established that graph projection should route by *meaning* rather than *type*. These are the resulting principles. + +### Meaning over type + +- Classify nodes by what they *say*, not by their kind enum. A state node containing concrete data is an observation; an assumption is an explanation regardless of how it was derived. +- Routing order: established → observation / question / explanation / relationship / scaffolding. Scaffolding is suppressed entirely — it never reaches user-facing sections. + +### Suppression hierarchy + +Three tiers, applied top to bottom: + +1. **Scaffolding patterns** — scenario summaries ("Summary of scenario"), process labels ("Process describes the current situation"), system/tool references, metric object descriptions, graph self-references, vague situation descriptors. These are structural glue; the user does not need to see them. +2. **Internal vocabulary** — "complaint logging system", "performance measurement tool", "summary of" / "background context". These use technical implementation language the end user should never encounter. +3. **Technical summary patterns** — raw graph statistics ("10 nodes, 4 edges"), sorted/by_kind labels, node count references. + +### Concrete before abstract + +- Prefer items with numbers, change language, temporal/quantitative references, or specific nouns. +- Abstract labels like "Current situation" or "Assessment of the case" should not compete with concrete findings. + +### Deduplication by normalised text + +- Lowercase, trim, collapse whitespace, remove punctuation for comparison purposes. +- Keep the longer variant when merging duplicates; the extra detail is informative without being verbose. + +### Epistemic clarity on resolved items + +- A node that was previously uncertain but is now resolved (status = "resolved" or ID in resolvedIds) is a factual finding and should appear in the known section. +- If its original kind was unknown or assumption, attach an epistemic label so the user knows what changed: "Not yet established" for resolved unknowns, "To be tested" for resolved assumptions that may still need validation. + +### Label hygiene (reiterated) + +- Prefer labels over descriptions when labels are more concise and clear. +- Omit items too verbose to scan; do not synthesise rewritten claims. +- Never invent facts absent from the graph. + +## Investigation Narrative + +The reasoning graph is the machine representation of the investigation. + +The investigation narrative is the human representation. + +The UI renders projections from the narrative, not directly from the graph. + +Principles: + +- Users understand investigations, not graphs. +- The graph is an internal reasoning structure. +- The narrative is the explanation of current understanding. +- Every user-facing panel should consume narrative state where possible. +- Multiple UI layouts may share the same narrative. +- Narrative should evolve as evidence changes. +- Narrative must never invent facts absent from the graph. +- Narrative explains uncertainty rather than exposing graph mechanics. + +## Facilitator Behaviour + +The facilitator is defined by patterns of action, not by its words. +The same investigation state can produce different behaviours depending on context and history. + +### Core behavioural principles + +Every turn should reflect a behaviour selected from the following set — not a mechanically determined response: + +**Orient.** Establish shared understanding before asking anything. + +**Acknowledge.** Integrate what was learned before introducing new uncertainty. + +**Observe pattern.** Surface connections between established facts without resolving them for the user. + +**Clarify.** Target ambiguous or partially useful information with narrow, precise questions. + +**Validate.** Mark resolutions explicitly and show their consequence on the investigation. + +**Connect.** Propose exploring relationships between established findings as natural next steps. + +**Challenge assumption.** Expose premises that lack sufficient evidence without dismissing them. + +**Refine understanding.** Restate the current state more coherently when sufficient information exists — not as repetition but as evolution. + +**Expose uncertainty.** Make the disparity between known and unknown visible rather than hiding gaps behind generic language. + +**Decide direction.** Recommend a specific next step with reasoning — not enumerate all options equally. + +**Know when to pause.** Hold space after significant insight instead of immediately asking another question. + +**Avoid premature closure.** Validate partial understanding; offer deeper pathways without implying urgency to conclude. + +**Communicate confidence honestly.** Express certainty through epistemic language that matches the actual resolution state. + +**Progressively narrow focus.** Shift from breadth to synthesis to depth as the investigation matures. + +### What the facilitator does NOT do + +- Ask questions to fill graph nodes. +- Treat all unknowns equally. +- Present every available explanation as equally valid. +- Move on before integrating what was just learned. +- Summarise too often or too rarely. +- Claim certainty where none exists. +- Forget what was established earlier. + +### State-aware behaviour selection + +The facilitator selects its behavioural response from investigation state assessment, not from a fixed sequence: + +> What was resolved this turn? +> How many turns since last synthesis? +> What is the proportion of known vs unknown? +> Did recent turns explore or synthesise? +> Do newly established facts form a pattern? +> Did user information introduce clarity or ambiguity? +> What phase is the investigation in (early / active / terminal)? + +### Relationship to architecture + +The narrative layer describes *state* (what do we know?). +The behavioural model describes *action* (what should we do about it?). + +They are complementary. The engine assesses state through the narrative, then selects a behaviour, then executes through the conversation infrastructure. diff --git a/.claude/working-rules.md b/.claude/working-rules.md new file mode 100644 index 0000000..0fee519 --- /dev/null +++ b/.claude/working-rules.md @@ -0,0 +1,125 @@ +# Claude Code Working Rules + +## Mandatory command constraints + +These rules exist because previous long shell commands and streamed responses +caused tool failures. + +- Do not use heredocs. +- Do not use long `node -e` commands. +- Do not use long `python -c` commands. +- If helper code is needed, create a small script file and run it. +- Keep shell commands short and readable. +- Break complex work into several commands. +- Write large outputs to files instead of printing them. +- Do not print full JSON responses or graph objects. +- Do not paste complete large files into chat. +- Prefer: tool → file → concise summary. +- Keep final reports concise. +- Do not narrate every implementation step. + +## Change discipline + +Before editing: + +1. state the current branch; +2. inspect `git status`; +3. identify the relevant files; +4. explain the smallest intended change. + +Work on one component or concern at a time. + +Do not combine unrelated cleanup with the requested task. + +Do not reformat unrelated files. + +Do not modify production reasoning code during UX tasks. + +## Testing discipline + +Use focused tests. + +Do not run the full test suite unless requested or genuinely necessary. + +Do not call Ollama in unit tests. + +Do not run live multi-scenario evaluations for ordinary UI changes. + +Do not run Playwright unless the task specifically requires it. + +Do not weaken existing reasoning tests to make UI changes pass. + +## Git discipline + +Before committing: + +- inspect the diff; +- confirm no secrets; +- confirm no internal IP addresses; +- confirm no raw provider responses; +- confirm no screenshots; +- confirm no temporary scripts; +- confirm no generated test outputs; +- confirm only intended files changed. + +Use a focused commit message. + +Do not merge or tag unless explicitly requested. + +## Non-narration rule + +Claude Code must act as an implementation agent, not narrate its internal +debugging process. + +When tests fail: + +1. inspect the focused failure; +2. make the smallest justified edit; +3. rerun the focused test; +4. repeat until passing or genuinely blocked. + +Do not print or explain intermediate reasoning. + +Never print: + +- rendered HTML; +- full JSON; +- full graph objects; +- large diffs; +- long stack traces; +- repeated interpretations of the same failure. + +Prefer: + +tool → edit → focused test → concise report + +The final chat response must be under 1,000 words and normally contain only: + +- branch; +- commit hash; +- files changed; +- behaviour changed; +- tests; +- lint/build; +- remaining limitation; +- git status. + +## Response discipline + +At the end of a task, normally report only: + +- branch; +- commit hash, when committed; +- files changed; +- behaviour changed; +- tests; +- lint/build; +- manual result, if performed; +- remaining limitation; +- git status. + +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. diff --git a/.env.example b/.env.example index aa72cca..2e352ca 100644 --- a/.env.example +++ b/.env.example @@ -3,3 +3,13 @@ OLLAMA_BASE_URL=http://192.168.x.x:11434 # Model name (e.g., llama3, mistral, codellama, etc.) OLLAMA_MODEL=replace-with-model-name + +# ── Mock / Demo Mode (UI development only) ────────────────── +# Set to "true" to use pre-recorded scenario fixtures instead of Ollama. +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 diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..34ebb5a --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,51 @@ +# Confidence Engine + +Read these project instructions before making changes: + +- @.claude/project-context.md +- @.claude/architecture-guardrails.md +- @.claude/ux-guidelines.md +- @.claude/working-rules.md + +## Current working principle + +The Confidence Engine helps a person move from uncertainty towards justified +confidence by asking one simple, useful question at a time. + +The graph preserves the state of the reasoning. The conversation is the primary +user experience. + +## Before changing anything + +1. Inspect the current branch and working tree. +2. Read the relevant implementation and tests. +3. Identify whether the request concerns: + - reasoning behaviour; + - API/data contracts; + - or presentation only. +4. Respect the boundaries in the imported instructions. +5. Make the smallest change that satisfies the task. + +Do not assume an architectural redesign is wanted. + +## Live experiment harness rule + +When running reasoning experiments, use the canonical harness at +`tests/graph/live-update-experiment-helper.cjs`. Never create a new harness, +enumerate `/api/tags`, probe localhost, or discover/substitute models during +normal reasoning experiments. + +## Standard validation + +For UI-only work, normally run: + +```bash +npm test -- --run tests/ui/scenario-form.test.jsx +npm run lint +npm run build +``` + +Run additional focused tests only when relevant files are affected. + +Do not run Ollama, Playwright, the full test suite, or evaluator suites unless the +task explicitly requires them. diff --git a/app/globals.css b/app/globals.css index b5c61c9..dbb837d 100644 --- a/app/globals.css +++ b/app/globals.css @@ -1,3 +1,35 @@ @tailwind base; @tailwind components; @tailwind utilities; + +@keyframes spin { + from { transform: rotate(0deg); } + to { transform: rotate(360deg); } +} + +@keyframes fadeIn { + from { opacity: 0; transform: translateY(4px); } + to { opacity: 1; transform: translateY(0); } +} + +.investigation-card { + animation: fadeIn 0.4s ease-out both; +} + +.investigation-card:nth-child(2) { + animation-delay: 0.08s; +} + +.investigation-card:nth-child(3) { + animation-delay: 0.16s; +} + +@media (prefers-reduced-motion: reduce) { + [style*="animation:spin"] { + animation: none !important; + } + + .investigation-card { + animation: none; + } +} diff --git a/app/page.jsx b/app/page.jsx index 8c44694..fd3b208 100644 --- a/app/page.jsx +++ b/app/page.jsx @@ -2,7 +2,7 @@ import ScenarioForm from "@/components/scenario-form"; export default function Home() { return ( -
+

Confidence Engine

Experimental prototype: enter a scenario and send it to a local LLM for diff --git a/components/investigation-map.jsx b/components/investigation-map.jsx new file mode 100644 index 0000000..210afbc --- /dev/null +++ b/components/investigation-map.jsx @@ -0,0 +1,100 @@ +/** + * Investigation Map — user-facing workspace card. + * + * Shows the progress of reasoning as a set of investigation topics with + * simple status indicators. Does NOT expose graph internals. + * + * Design principles: + * - Calm, spacious, accessible + * - No percentages, no progress bars, no confidence scores + * - Topics evolve naturally across turns + */ + +import getInvestigationMapTopics from "@/lib/map/investigation-map-adapter"; + +/* ── Status icons (unicode — no icon library dependency) ─── */ + +const STATUS_ICONS = { + established: "✓", + current: "●", + unknown: "○", +}; + +function topicRowColor(status) { + switch (status) { + case "established": + return "text-gray-900"; + case "current": + return "text-blue-800"; + default: + return "text-gray-400"; + } +} + +function topicIconColor(status) { + switch (status) { + case "established": + return "text-green-600"; + case "current": + return "text-blue-500"; + default: + return "text-gray-300"; + } +} + +/* ── Single topic row ───────────────────────────────────── */ + +function TopicRow({ title, status }) { + const icon = STATUS_ICONS[status]; + const colorClass = topicRowColor(status); + const iconColor = topicIconColor(status); + const ariaLabel = `${status === "established" ? "Established" : status === "current" ? "Currently investigating" : "Still to explore"}: ${title}`; + + return ( +

+ + {title} +
+ ); +} + +/* ── Card wrapper ────────────────────────────────────────── */ + +export default function InvestigationMap({ turnCount = 0 }) { + const topics = getInvestigationMapTopics(turnCount); + + // Group topics by status for cleaner rendering + const groups = { + established: topics.filter((t) => t.status === "established"), + current: topics.filter((t) => t.status === "current"), + unknown: topics.filter((t) => t.status === "unknown"), + }; + + // Only render the card if there are non-established topics (during active investigation) + const hasActiveTopics = groups.current.length > 0 || groups.unknown.length > 0; + if (!hasActiveTopics && groups.established.length === 0) return null; + + return ( +
+

+ Investigation Map +

+

+ Active investigation topics and their status. +

+ +
+ {topics.map((topic, i) => ( + + ))} +
+
+ ); +} \ No newline at end of file diff --git a/components/investigation-summary-panel-v2.jsx b/components/investigation-summary-panel-v2.jsx new file mode 100644 index 0000000..1a87d5f --- /dev/null +++ b/components/investigation-summary-panel-v2.jsx @@ -0,0 +1,257 @@ +/** + * InvestigationSummaryPanelV2 — Phase 4, Experiment 11 + * A facilitator-style progress panel that translates the reasoning graph + * into a human-friendly "what is known / what remains" view. + * + * Design principle: + * The UI should progressively become a translation layer over the + * reasoning graph rather than maintaining separate duplicated summaries. + * Internal graph concepts remain available for developers, while end + * users see a facilitator-style explanation of what is currently understood + * and what remains uncertain. + * + * This component uses exactly the same graph data as InvestigationSummaryPanel + * (Version A). No new backend fields or API contracts are required. + */ + +/* ── Helpers ──────────────────────────────────────────────── */ + +function formatTimestamp(iso) { + if (!iso) return "—"; + try { + const d = new Date(iso); + if (isNaN(d)) return iso; + const pad = (n) => String(n).padStart(2, "0"); + return `${d.getFullYear()}-${pad(d.getMonth()+1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`; + } catch { + return iso; + } +} + +function humaniseDuration(seconds) { + if (!seconds || seconds < 0) return "—"; + const mins = Math.floor(seconds / 60); + const secs = seconds % 60; + if (mins === 0) return `${secs}s`; + return `${mins}m ${secs}s`; +} + +/* ── Data extraction helpers ─────────────────────────────── */ + +/** + * Classify nodes into "known" (resolved / observations with values) and + * "still investigating" (unresolved unknowns and assumptions needing validation). + */ +function classifyNodes(graph, resolvedIds) { + if (!graph?.nodes) return { known: [], stillInvestigating: [] }; + + const resolved = new Set(resolvedIds || []); + + const known = []; + const stillInvestigating = []; + + for (const node of graph.nodes) { + const isResolved = resolved.has(node.id) || node.status === "resolved"; + + // Resolved nodes become known facts + if (isResolved) { + known.push({ + label: node.label, + description: node.description, + kind: node.kind, + confidence: node.confidence, + }); + } else { + // Unresolved unknowns and assumptions go into "still investigating" + stillInvestigating.push({ + label: node.label, + description: node.description, + kind: node.kind, + confidence: node.confidence, + }); + } + } + + return { known, stillInvestigating }; +} + +/** + * Map graph node kinds to end-user-friendly group labels. + */ +function groupLabelForKind(kind) { + const map = { + unknown: "Still investigating", + assumption: "Assumptions to validate", + observation: "Observations", + state: "Current states", + metric: "Metrics", + conclusion: "Conclusions", + }; + return map[kind] || kind.replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase()); +} + +/* ── Rendering helpers ───────────────────────────────────── */ + +/** + * Render a single item from the known or still-investigating lists. + * Show only meaningful content — hide labels that duplicate description. + */ +function renderListItem(item) { + // Prefer description if it adds something beyond the label + const text = (item.description && item.description !== item.label) + ? item.description + : item.label; + + return text; +} + +/* ── Component ────────────────────────────────────────────── */ + +function InvestigationSummaryPanelV2({ graph, selectedQuestion, result, updateStatus }) { + // ── Status (same derivation logic as Version A) ────────── + const isInvestigating = Boolean(selectedQuestion); + const hasGraph = Boolean(graph); + + let currentStatus; + if (updateStatus === "loading") { + currentStatus = { label: "Reasoning", level: "investigating" }; + } else if (!hasGraph) { + currentStatus = { label: "Not started", level: "idle" }; + } else if (isInvestigating) { + currentStatus = { label: "Investigation in progress", level: "investigating" }; + } else if (graph.resolvedNodeIds?.length > 0 && graph.nodes) { + const unresolvedUnknowns = graph.nodes.filter( + (n) => n.kind === "unknown" && !graph.resolvedNodeIds.includes(n.id) + ); + if (unresolvedUnknowns.length === 0) { + currentStatus = { label: "Investigation complete", level: "complete" }; + } else { + currentStatus = { label: "Current evidence limit reached", level: "limit" }; + } + } else { + currentStatus = { label: "Analysis complete", level: "complete" }; + } + + const statusColors = { + idle: { border: "border-gray-200/60", bg: "bg-gray-50/40", text: "text-gray-400" }, + investigating: { border: "border-blue-200/60", bg: "bg-blue-50/30", text: "text-blue-600" }, + complete: { border: "border-green-200/60", bg: "bg-green-50/30", text: "text-green-600" }, + limit: { border: "border-gray-200", bg: "bg-gray-50/40", text: "text-gray-400" }, + }; + + const colors = statusColors[currentStatus.level] || statusColors.idle; + + // ── Current understanding (same source as Version A) ──── + const currentUnderstanding = + result?.summary || + result?.updatedSituationGraph?.currentSummary || + graph?.currentSummary || + null; + + // ── Classify graph data ───────────────────────────────── + const resolvedIds = new Set(graph?.resolvedNodeIds || []); + const { known, stillInvestigating } = classifyNodes(graph, resolvedIds); + + // Group still-investigating items by kind for a cleaner view + const investigatingByGroup = {}; + for (const item of stillInvestigating) { + const key = groupLabelForKind(item.kind); + if (!investigatingByGroup[key]) investigatingByGroup[key] = []; + investigatingByGroup[key].push(item); + } + + // ── Reasoning summary counts (quiet, at bottom) ───────── + const reasonCounts = { + observations: graph?.nodes?.filter((n) => n.kind === "observation").length || 0, + unknowns: stillInvestigating.filter((n) => n.kind === "unknown").length || 0, + assumptions: graph?.nodes?.filter((n) => n.kind === "assumption" && !resolvedIds.has(n.id)).length || 0, + relationships: graph?.edges?.length || 0, + metrics: graph?.nodes?.filter((n) => n.kind === "metric").length || 0, + states: graph?.nodes?.filter((n) => n.kind === "state").length || 0, + conclusions: graph?.nodes?.filter((n) => n.kind === "conclusion").length || 0, + }; + + // Only show non-zero counts in the reasoning summary + const reasonEntries = Object.entries(reasonCounts).filter(([_, v]) => v > 0); + + return ( +
+ {/* Status — minimal indicator */} +
+ + {currentStatus.label} +
+ + {/* ── Current understanding (if any) ──────────────── */} + {currentUnderstanding && ( +
+

{currentUnderstanding}

+
+ )} + + {/* ── Still investigating — primary focus ─────────── */} + {(stillInvestigating.length > 0 || known.length === 0) && ( +
+ {stillInvestigating.length > 1 ? ( + <> +

Still investigating

+
    + {Object.entries(investigatingByGroup).map(([group, items]) => ( +
  • + {group} +
      + {items.map((item, i) => ( +
    • + + + {renderListItem(item)} + +
    • + ))} +
    +
  • + ))} +
+ + ) : stillInvestigating.length === 1 ? ( +
+ +

{renderListItem(stillInvestigating[0])}

+
+ ) : null} +
+ )} + + {/* ── What we have learned ────────────────────────── */} + {known.length > 0 && ( +
+

What we know

+
    + {known.map((item, i) => ( +
  • + + + {renderListItem(item)} + +
  • + ))} +
+
+ )} + + {/* ── Quiet reasoning summary — secondary ─────────── */} +
+

Reasoning

+
+ {reasonEntries.map(([label, count]) => ( + + {count} {label} + + ))} +
+
+
+ ); +} + +export default InvestigationSummaryPanelV2; diff --git a/components/investigation-summary-panel-v3.jsx b/components/investigation-summary-panel-v3.jsx new file mode 100644 index 0000000..7836f68 --- /dev/null +++ b/components/investigation-summary-panel-v3.jsx @@ -0,0 +1,172 @@ +/** + * InvestigationSummaryPanelV3 — Phase 4, Experiment 12 + * A user-facing facilitator view that translates the reasoning graph into + * a concise, human-meaningful presentation. + * + * Design principles: + * - The panel shows up to four sections: what we know, still investigating, + * possible explanations, and a quiet summary. + * - All content is grounded in existing graph fields. No invented facts. + * - Epistemic labels are explicit (structural), not colour-dependent. + * - The same panel remains useful during early, active and terminal states. + */ + +import { buildFacilitatorViewModel } from "@/lib/presentation/facilitator-view-adapter"; + +/* ── Item rendering ─────────────────────────────────────────────── */ + +/** + * Render a single item with its structural label where applicable. + */ +function renderItem(item, isExplanation) { + if (isExplanation && typeof item === "object") { + return ( +
  • + + {item.text} + + {item.label} + +
  • + ); + } + + return ( +
  • + + {item} +
  • + ); +} + +/* ── Section components ──────────────────────────────────────────── */ + +function KnownSection({ title, items }) { + if (!items || items.length === 0) return null; + + return ( +
    +

    + {title} +

    +
      + {items.map((item, i) => ( +
    • + + {item} +
    • + ))} +
    +
    + ); +} + +function InvestigatingSection({ title, items }) { + if (!items || items.length === 0) return null; + + return ( +
    +

    + {title} +

    +
      + {items.map((item, i) => ( +
    • + + {item} +
    • + ))} +
    +
    + ); +} + +function ExplanationSection({ items }) { + if (!items || items.length === 0) return null; + + return ( +
    +

    + Possible explanations +

    +
      + {items.map((item, i) => renderItem(item, true))} +
    +
    + ); +} + +function QuietSummary({ text }) { + if (!text) return null; + + return ( +
    +

    + Investigation state +

    +

    {text}

    +
    + ); +} + +/* ── Empty-state fallback ──────────────────────────────────────── */ + +function EmptyState() { + return ( +
    + + + {/* Intentionally no Possible explanations section when empty */} + +
    + +

    We are still establishing the basic facts.

    +
    +
    + ); +} + +/* ── Main component ────────────────────────────────────────────── */ + +function InvestigationSummaryPanelV3({ graph, selectedQuestion, result }) { + // Build the view model from the adapter + const resolvedIds = new Set(graph?.resolvedNodeIds || []); + + const viewModel = buildFacilitatorViewModel({ + nodes: graph?.nodes || [], + resolvedIds, + activeUnknownNodeId: graph?.activeUnknownNodeId || null, + edges: graph?.edges || [], + selectedQuestion, + }); + + // Early state fallback + if (!viewModel.known.hasItems && !viewModel.investigating.hasItems) { + return ; + } + + return ( +
    + {/* What we know */} + + + {/* Still investigating — or "Remaining cautions" in terminal state */} + {!viewModel.investigating.shouldOmit && ( + + )} + + {/* Possible explanations */} + {viewModel.explanations.hasItems && ( + + )} + + {/* Quiet reasoning summary */} + +
    + ); +} + +export default InvestigationSummaryPanelV3; diff --git a/components/investigation-summary-panel.jsx b/components/investigation-summary-panel.jsx new file mode 100644 index 0000000..09ec9ae --- /dev/null +++ b/components/investigation-summary-panel.jsx @@ -0,0 +1,152 @@ +/** + * InvestigationSummaryPanel — Phase 4 + * Displays key investigation metrics in a compact card. + * Some fields are currently mocked; TODO comments identify what the + * reasoning engine must eventually provide. + */ + +/* ── Helpers ──────────────────────────────────────────────── */ + +function formatTimestamp(iso) { + if (!iso) return "—"; + try { + const d = new Date(iso); + if (isNaN(d)) return iso; + const pad = (n) => String(n).padStart(2, "0"); + return `${d.getFullYear()}-${pad(d.getMonth()+1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`; + } catch { + return iso; + } +} + +function humaniseDuration(seconds) { + if (!seconds || seconds < 0) return "—"; + const mins = Math.floor(seconds / 60); + const secs = seconds % 60; + if (mins === 0) return `${secs}s`; + return `${mins}m ${secs}s`; +} + +/* ── Component ────────────────────────────────────────────── */ + +function InvestigationSummaryPanel({ graph, selectedQuestion, result, updateStatus }) { + // ── Current status ──────────────────────────────────────────── + // TODO: reasoning should emit an explicit status field such as + // "investigating", "evidence_limit_reached", "resolution_achieved". + // Currently derived heuristically from graph state. + const isInvestigating = Boolean(selectedQuestion); + const hasGraph = Boolean(graph); + + let currentStatus; + if (updateStatus === "loading") { + currentStatus = { label: "Reasoning", level: "investigating" }; + } else if (!hasGraph) { + currentStatus = { label: "Not started", level: "idle" }; + } else if (isInvestigating) { + currentStatus = { label: "Investigation in progress", level: "investigating" }; + } else if (graph.resolvedNodeIds?.length > 0 && graph.nodes) { + const unresolvedUnknowns = graph.nodes.filter( + (n) => n.kind === "unknown" && !graph.resolvedNodeIds.includes(n.id) + ); + if (unresolvedUnknowns.length === 0) { + currentStatus = { label: "Investigation complete", level: "complete" }; + } else { + // TODO: reasoning should emit a terminal "evidence_limit_reached" + // status when it stops selecting questions because no unknown has + // sufficient upstream evidence. Currently we infer this from the + // absence of an active question combined with unresolved unknowns. + currentStatus = { label: "Current evidence limit reached", level: "limit" }; + } + } else { + currentStatus = { label: "Analysis complete", level: "complete" }; + } + + const statusColors = { + idle: { border: "border-gray-200/60", bg: "bg-gray-50/40", text: "text-gray-400" }, + investigating: { border: "border-blue-200/60", bg: "bg-blue-50/30", text: "text-blue-600" }, + complete: { border: "border-green-200/60", bg: "bg-green-50/30", text: "text-green-600" }, + limit: { border: "border-gray-200", bg: "bg-gray-50/40", text: "text-gray-400" }, + }; + + const colors = statusColors[currentStatus.level] || statusColors.idle; + + // ── Current understanding ──────────────────────────────── + // TODO: reasoning should provide a durable summary field that is + // guaranteed to be the latest plain-language synthesis. + // Currently falls back to graph.currentSummary which may not exist + // in all mock scenarios. + const currentUnderstanding = + result?.summary || + result?.updatedSituationGraph?.currentSummary || + graph?.currentSummary || + null; + + // ── Questions answered / remaining ─────────────────────── + // TODO: reasoning should emit a list of resolved unknown node IDs + // and the total set of unknown nodes it identified at start. + // Currently we count from the graph snapshot: every unknown whose + // status is "resolved" (or whose ID appears in resolvedNodeIds). + let questionsAnswered = 0; + let questionsRemaining = 0; + + if (graph?.nodes) { + const allUnknowns = graph.nodes.filter((n) => n.kind === "unknown"); + const resolvedCount = allUnknowns.filter( + (n) => n.status === "resolved" || (graph.resolvedNodeIds && graph.resolvedNodeIds.includes(n.id)) + ).length; + questionsAnswered = resolvedCount; + // TODO: this is a rough heuristic — the reasoning engine should + // explicitly track which unknowns were proposed for questioning. + questionsRemaining = allUnknowns.length - resolvedCount; + } + + // ── Timestamps ─────────────────────────────────────────── + // TODO: reasoning should provide investigationStartedAt and + // lastUpdatedAt as part of the start/update contract. + // Currently we use the session updatedAt timestamp (persisted by + // the UI layer) as a best-effort approximation. + const investigationStartTime = result?.updatedAt || null; + const lastUpdatedAt = result?.updatedAt || null; + + // Derive elapsed time since last update + let elapsedSeconds = 0; + if (lastUpdatedAt) { + elapsedSeconds = Math.floor((Date.now() - new Date(lastUpdatedAt).getTime()) / 1000); + } + + return ( +
    + {/* Status */} +
    + + {currentStatus.label} +
    + + {/* Current understanding */} + {currentUnderstanding && ( +
    +

    + What we understand so far +

    +

    {currentUnderstanding}

    +
    + )} + + {/* Questions — hidden when no meaningful value to show */} + {isInvestigating && questionsRemaining > 0 && ( +
    +
    + Questions answered + {questionsAnswered} +
    +
    + Still working on + {questionsRemaining + " items"} +
    +
    + )} +
    + ); +} + +export default InvestigationSummaryPanel; diff --git a/components/reasoning-workspace.jsx b/components/reasoning-workspace.jsx new file mode 100644 index 0000000..d6ff539 --- /dev/null +++ b/components/reasoning-workspace.jsx @@ -0,0 +1,821 @@ +"use client"; + +import React, { useState, useRef, useEffect, useMemo } from "react"; +import DiagnosticsView from "@/components/diagnostics-view"; +import GraphUpdateView from "@/components/graph-update-view"; +import SituationGraphView from "@/components/situation-graph-view"; +import InvestigationSummaryPanel from "@/components/investigation-summary-panel"; +import InvestigationSummaryPanelV2 from "@/components/investigation-summary-panel-v2"; +import InvestigationSummaryPanelV3 from "@/components/investigation-summary-panel-v3"; +import InvestigationMap from "@/components/investigation-map"; + +// ── Technical summary detector (main view filters these) ─── +const TECHNICAL_PATTERNS = [ + /nodes?\s*[:\d]/i, + /edges?\s*[:\d]/i, + /\b(?:unknown|observation|conclusion)\b\s/i, + /\bsorted\b/i, + /by_kind/i, + /\b(?:node|edge|unknown|state)\s+count/i, +]; + +function isTechnicalSummary(summary) { + if (!summary || typeof summary !== "string") return false; + const trimmed = summary.trim(); + if (!trimmed) return false; + for (const p of TECHNICAL_PATTERNS) { + if (p.test(trimmed)) return true; + } + return false; +} + +// ── Recovery state components (Phase 2) ─────────────────────── + +function ProviderUnavailableCard({ onRestart }) { + return ( +
    +

    Provider unavailable

    +

    + The reasoning service could not be reached. This is usually temporary — check that the local model is running and try again. +

    + {onRestart && ( + + )} +
    + ); +} + +function MalformedResponseCard({ onRestart }) { + return ( +
    +

    Unexpected response

    +

    + The reasoning service returned a response we could not interpret. This may indicate a temporary issue with the model output format. +

    + {onRestart && ( + + )} +
    + ); +} + +function UnexpectedStateCard({ stateName, onRetry, onRestart }) { + return ( +
    +

    Unexpected state

    +

    + {stateName ? `The system is in an unexpected state (${stateName}).` : "An unexpected internal error occurred."} + Please restart the investigation to continue. +

    +
    + {onRetry && ( + + )} + {onRestart && ( + + )} +
    +
    + ); +} + +function ContinueLaterBanner({ onRestart }) { + return ( +
    +

    + Your previous investigation state is still saved. You can continue where you left off or start fresh. +

    + {onRestart && ( + + )} +
    + ); +} + +// ── Session persistence hook (Phase 3) ──────────────────────── + +function useSessionPersistence() { + const [sessionReady, setSessionReady] = useState(false); + const sessionKey = "confidence-engine-session"; + + function saveSession(state) { + if (typeof sessionStorage === "undefined") return; + try { + sessionStorage.setItem(sessionKey, JSON.stringify(state)); + } catch (_) { /* quota or disabled — ignore silently */ } + } + + function loadSession() { + if (typeof sessionStorage === "undefined") return null; + try { + const raw = sessionStorage.getItem(sessionKey); + return raw ? JSON.parse(raw) : null; + } catch (_) { + return null; + } + } + + function clearSession() { + if (typeof sessionStorage === "undefined") return; + try { sessionStorage.removeItem(sessionKey); } catch (_) {} + } + + return { saveSession, loadSession, clearSession, sessionReady: true }; +} + +// ── Current understanding card ──────────────────────────────── + +// Evidence-limit text that must not appear inside Current understanding +// when the terminal outcome already communicates that state. +const EVIDENCE_LIMIT_PHRASES = [ + "The available evidence has reached its current limit", + "evidence has reached its current limit", + "evidence limit reached", + "has reached its current limit", +]; + +function resolveCurrentSummary(currentSummary) { + if (!currentSummary || typeof currentSummary !== "string") return null; + const trimmed = currentSummary.trim(); + if (!trimmed) return null; + + // Filter out technical graph summaries + for (const p of TECHNICAL_PATTERNS) { + if (p.test(trimmed)) return null; + } + + // Don't show evidence-limit text in Current understanding when + // the terminal outcome card already communicates that state. + const lower = trimmed.toLowerCase(); + for (const phrase of EVIDENCE_LIMIT_PHRASES) { + if (lower.includes(phrase)) return null; + } + + return trimmed; +} + +// ── Status message pools for loading feedback ──────────────── +const INITIAL_MESSAGES = [ + { min: 0, text: "Reading your situation" }, + { min: 10, text: "Building a structured understanding" }, + { min: 25, text: "Identifying what is known and still unclear" }, + { min: 45, text: "Selecting the next useful question" }, +]; + +const UPDATE_MESSAGES = [ + { min: 0, text: "Considering your answer" }, + { min: 10, text: "Updating the situation" }, + { min: 25, text: "Checking what changed" }, + { min: 45, text: "Choosing the next question" }, +]; + +function useLoadingStatus(messages, isLoading) { + const [elapsed, setElapsed] = useState(0); + const startRef = useRef(null); + + useEffect(() => { + if (isLoading) { + startRef.current = Date.now(); + const iv = setInterval(() => { + setElapsed(Math.floor((Date.now() - startRef.current) / 1000)); + }, 1000); + return () => clearInterval(iv); + } else { + setElapsed(0); + startRef.current = null; + } + }, [isLoading]); + + const currentMessage = useMemo(() => { + if (!messages || messages.length === 0) return ""; + let msg = messages[0].text; + for (const m of messages) { + if (elapsed >= m.min) msg = m.text; + } + return msg; + }, [messages, elapsed]); + + return { elapsed, currentMessage }; +} + +// ── Spinner component ─────────────────────────────────────── +function ActivitySpinner() { + return ( + + ); +} + +// ── Current investigation card (prominent hero section) ────── +function CurrentInvestigationCard({ selectedQuestion, graph }) { + if (!selectedQuestion) return null; + + const q = typeof selectedQuestion === "string" ? selectedQuestion : selectedQuestion.question; + if (!q) return null; + + // Derive meaningful context from the active node only when it adds value + let whyMattersText = null; + if (graph?.activeUnknownNodeId && graph.nodes) { + const activeNode = graph.nodes.find((n) => n.id === graph.activeUnknownNodeId); + if (activeNode?.description && activeNode.description !== activeNode.label) { + whyMattersText = activeNode.description; + } + } + + return ( +
    +

    + Investigation +

    +

    {q}

    + {whyMattersText && ( +

    + {whyMattersText} +

    + )} +
    + ); +} + +// ── Outcome helpers ─────────────────────────────────────────── + +function hasGenuineCompletion(graph) { + if (!graph || !graph.nodes?.length) return false; + const resolvedIds = new Set(graph.resolvedNodeIds || []); + const unresolvedCount = graph.nodes.filter( + (n) => n.kind === "unknown" && n.status !== "resolved" && !resolvedIds.has(n.id), + ).length; + if (unresolvedCount > 0) return false; + if (graph.activeUnknownNodeId) { + const active = graph.nodes.find((n) => n.id === graph.activeUnknownNodeId); + if (active && active.status !== "resolved" && !resolvedIds.has(active.id)) return false; + } + return true; +} + +// ── Completion card (terminal state when all unknowns resolved) ─ +function CompletionCard({ summary }) { + return ( +
    +

    Investigation complete

    +

    The available evidence supports the following understanding.

    + {summary && ( +
    +

    {summary}

    +
    + )} +
    + ); +} + +// ── Evidence-limit card (terminal state: no next question) ─────── +function EvidenceLimitCard({ summary }) { + return ( +
    +

    Current evidence limit reached

    + {summary && ( +
    +

    {summary}

    +
    + )} +

    Further progress requires additional evidence.

    +
    + ); +} + +// ── Current understanding card ──────────────────────────────── +function CurrentUnderstandingCard({ currentSummary, plainLanguage }) { + if (plainLanguage) return ; + + const summary = resolveCurrentSummary(currentSummary); + + if (!summary) return null; + + return ( +
    +

    + Understanding +

    +

    {summary}

    +
    + ); +} + +// ── Plain-language understanding card (from pipeline summary) ── +function PlainLanguageCard({ summary }) { + if (!summary) return null; + + return ( +
    +

    + Understanding +

    +

    {summary}

    +
    + ); +} + +// ── Investigation history card (readable notebook style) ────── +function InvestigationHistoryCard({ turn }) { + const isCollapsed = turn._collapsed; + const isAnswered = Boolean(turn.answer?.trim()); + + const displayedQuestion = turn.question; + + return ( +
    + + {isAnswered && } + {displayedQuestion} + + +
    +

    {turn.answer}

    + + {turn.acknowledgement && ( +

    + {turn.acknowledgement} +

    + )} +
    +
    + ); +} + +// ── Investigation history section ───────────────────────────── +function InvestigationHistory({ turns }) { + if (!turns || turns.length === 0) return null; + + const latestId = turns[turns.length - 1].id; + + return ( +
    +

    + History +

    +
    + {turns.map((turn) => ( + + ))} +
    +
    + ); +} + +// ── Original situation (always-visible reference card) ──────── +function OriginalSituation({ scenario, centralStatement }) { + const text = scenario || centralStatement; + + if (!text) return null; + + return ( +
    +

    + Situation +

    + +

    + {text} +

    +
    + ); +} + +// ── Transient acknowledgement (auto-dismisses after 3s) ───────── + +function useAutoDismiss(duration = 3000) { + const [visible, setVisible] = useState(true); + + useEffect(() => { + if (!visible) return; + const timer = setTimeout(() => setVisible(false), duration); + return () => clearTimeout(timer); + }, [visible, duration]); + + return visible; +} + +function UpdateAcknowledgement({ updateResult }) { + const visible = useAutoDismiss(3000); + + if (!updateResult || !visible) return null; + + const summary = updateResult.summary; + + return ( +
    +
    + {summary} +
    +
    + ); +} + +// ── Developer details disclosure ────────────────────────────── +function DeveloperDetails({ graph, selectedQuestion, diagnostics, newlySurfacedNodeIds, updateResult }) { + return ( +
    + + Developer details + +
    + {graph && ( + + )} + {updateResult && ( + + )} + {diagnostics && } +
    +
    + ); +} + +// ── Loading overlay (for both start and update) ─────────────── +function LoadingOverlay({ isLoading, elapsed, currentMessage, variant }) { + if (!isLoading) return null; + + const messages = variant === "update" ? UPDATE_MESSAGES : INITIAL_MESSAGES; + let statusText = messages[0].text; + for (const m of messages) { + if (elapsed >= m.min) statusText = m.text; + } + + return ( +
    +
    + + Working through your situation +
    +

    {statusText}

    +

    + This has been running for {elapsed}s. + {variant === "initial" && elapsed >= 45 && ( + This can take around a minute with the current local model. + )} +

    +
    + ); +} + +// ── Main workspace component ────────────────────────────────── + +function getErrorType(errorStr, stage, hasGraph) { + if (!errorStr && !stage) return null; + const lower = (errorStr || "").toLowerCase(); + if (/provider|unavailable|network|timeout/.test(lower)) return "provider-unavailable"; + if (/malformed|invalid.*format|parse|structured/.test(lower)) return "malformed-response"; + if (stage === "provider") return "provider-error"; + if (stage === "unexpected") return "unexpected-state"; + if (/validation/.test(lower) && !hasGraph) return "no-graph"; + return null; +} + +export default function ReasoningWorkspace({ + scenario, + status, + updateStatus, + currentUnderstanding: propUnderstanding, + result, + answer, + setAnswer, + onAnswerSubmit, + lastSubmittedAnswer, + onRestart, +}) { + const [investigationHistory, setInvestigationHistory] = useState([]); + const turnCounter = useRef(0); + const pendingTurnRef = useRef(null); + // ── Experiment 12: toggle between progress panel versions (temporary experimental UI) ── + const [panelVariant, setPanelVariant] = useState("c"); + const { saveSession, loadSession } = useSessionPersistence(); + + // Persist workspace state on every successful update (Phase 3) + useEffect(() => { + if (updateStatus === "success" && result?.situationGraph) { + saveSession({ + scenario, + situationGraph: result.situationGraph, + selectedQuestion: result.selectedQuestion, + summary: result.summary || propUnderstanding, + updatedAt: new Date().toISOString(), + }); + } + }, [updateStatus, result]); + + // Capture the current selected question at submit time (not from a stale ref) + const capturePendingTurn = (selectedQuestion, answerText) => { + if (!selectedQuestion || !answerText?.trim()) return null; + const q = typeof selectedQuestion === "string" ? selectedQuestion : selectedQuestion.question; + if (!q) return null; + turnCounter.current += 1; + return { + id: `turn-${turnCounter.current}`, + question: q, + answer: answerText.trim(), + acknowledgement: null, + }; + }; + + // Append the captured pending turn to history after a successful update only + useEffect(() => { + const pending = pendingTurnRef.current; + if (!pending || updateStatus !== "success") return; + + setInvestigationHistory((prev) => [ + ...prev, + { ...pending, acknowledgement: result?.summary || null }, + ]); + pendingTurnRef.current = null; + }, [updateStatus, result]); + + const handleUpdateCaptureAndSubmit = async (e) => { + e.preventDefault(); + if (!answer?.trim() || !result?.selectedQuestion) return; + pendingTurnRef.current = capturePendingTurn(result.selectedQuestion, answer); + await onAnswerSubmit(e); + }; + + const graph = result?.situationGraph ?? null; + const hasGraph = Boolean(graph); + const diagnostics = result?.diagnostics ?? null; + const newlySurfacedNodeIds = result?.newlySurfacedNodeIds || []; + const genuineCompletion = hasGenuineCompletion(graph); + + const errorType = getErrorType( + result?.error || (result?.updateError ? result.updateError.error : null), + result?.stage, + Boolean(graph) + ); + + const isProviderUnavailable = + errorType === "provider-unavailable" || errorType === "provider-error"; + const isMalformedResponse = errorType === "malformed-response"; + + const { elapsed: startElapsed, currentMessage: startMsg } = useLoadingStatus( + INITIAL_MESSAGES, + status === "loading" + ); + + const { elapsed: updateElapsed, currentMessage: updateMsg } = useLoadingStatus( + UPDATE_MESSAGES, + updateStatus === "loading" + ); + + const isUpdating = updateStatus === "loading"; + const hasSelectedQuestion = Boolean(result?.selectedQuestion); + + const canAnswer = + status === "success" && + !isUpdating && + Boolean(result?.situationGraph) && + hasSelectedQuestion; + + const selectedQ = result?.selectedQuestion ?? null; + + // Determine whether the Current Understanding card should render: + // — when there is a durable plain-language understanding, or + // — when there is an actual summary from any graph snapshot, or + // — when the investigation has reached a terminal state with no active question. + const hasCurrentSummaryCondition = + Boolean(propUnderstanding || graph?.currentSummary || result?.updatedSituationGraph?.currentSummary) || !hasSelectedQuestion; + + return ( +
    + {/* ── Loading overlays ─────────────────────────────── */} + {status === "loading" && ( + + )} + + {/* ── Provider unavailable recovery (always visible) ───────────── */} + {isProviderUnavailable && ( + + )} + + {/* ── Malformed response recovery (always visible) ──────────────── */} + {isMalformedResponse && ( + + )} + + {/* ── Unexpected state recovery ──────────────────── */} + {(errorType === "unexpected-state") && result && ( + + )} + + {/* ── No graph produced after initial analysis ───────── */} + {(status === "success" || status === "error") && !graph ? ( +
    + {diagnostics?.noQuestionReason + ? "Validation failed — no structured graph output was produced." + : "The analysis completed but did not produce a structured result."} +
    + ) : ( + <> + {hasSelectedQuestion && ( +
    + {/* ── Left lane: active conversation & notebook ───────── */} +
    + + + {updateStatus === "success" && !isUpdating && ( + + )} + + {!isUpdating && canAnswer && ( +
    +
    + +