feat: Phase 2-5 UX enhancements — recovery cards, session persistence, summary panel, contract backlog

Phase 2: Recovery state components (ProviderUnavailableCard,
MalformedResponseCard, UnexpectedStateCard, ContinueLaterBanner) with
automatic error detection for provider/network/malformed/unexpected states.

Phase 3: Session persistence via sessionStorage — save after each
successful turn, restore on mount, clear on restart/reset. Continuelater banner shown when session is restored.

Phase 4: InvestigationSummaryPanel component displaying current status,
understanding summary, questions answered/remaining, investigation timestamps.

Phase 5: docs/reasoning-contract-backlog.md documenting all mocked
fields (60+ rows across 7 categories) with feature/UI need/mock/desired
output/stage/notes columns.

Also: wired onRestart through ReasoningWorkspace → ScenarioForm, fixed
getErrorType scope issues, removed broken window.__restartInvestigation.
This commit is contained in:
2026-08-05 06:48:59 +01:00
parent 28289bb4b7
commit c4f5744c30
6 changed files with 1007 additions and 113 deletions
+171
View File
@@ -0,0 +1,171 @@
/**
* 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 }) {
// ── 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 (!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", bg: "bg-gray-50", text: "text-gray-600" },
investigating: { border: "border-blue-200", bg: "bg-blue-50", text: "text-blue-700" },
complete: { border: "border-green-200", bg: "bg-green-50", text: "text-green-700" },
limit: { border: "border-gray-300", bg: "bg-gray-100", text: "text-gray-500" },
};
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 (
<div className={`rounded-lg border ${colors.border} ${colors.bg} p-5 space-y-4`}>
{/* Status */}
<div className="flex items-center gap-2">
<span className={`inline-block h-2.5 w-2.5 rounded-full bg-current ${colors.text}`} />
<span className={`text-sm font-medium ${colors.text}`}>{currentStatus.label}</span>
</div>
{/* Current understanding */}
{currentUnderstanding && (
<div>
<h3 className="mb-1 text-xs font-bold uppercase tracking-wider text-gray-400">
What we understand so far
</h3>
<p className="text-sm leading-relaxed text-gray-700">{currentUnderstanding}</p>
</div>
)}
{/* Questions */}
<div className="grid grid-cols-2 gap-4">
<div>
<span className="block text-xs text-gray-400">Questions answered</span>
<span className={`text-lg font-semibold ${colors.text}`}>{questionsAnswered}</span>
</div>
<div>
<span className="block text-xs text-gray-400">Still working on</span>
{/* TODO: avoid implying 1 unknown = 1 remaining question */}
{isInvestigating ? (
<span className={`text-lg font-semibold ${colors.text}`}>{questionsRemaining > 0 ? questionsRemaining + " items" : "—"}</span>
) : (
<span className={`text-lg font-semibold ${colors.text}`}></span>
)}
</div>
</div>
{/* Timestamps */}
<div className="space-y-1 text-xs text-gray-400">
<div className="flex justify-between">
<span>Investigation started</span>
<span>{formatTimestamp(investigationStartTime)}</span>
</div>
<div className="flex justify-between">
<span>Last updated</span>
<span>{formatTimestamp(lastUpdatedAt)}</span>
</div>
{elapsedSeconds > 0 && (
<div className="flex justify-between">
<span>Elapsed since last update</span>
<span>{humaniseDuration(elapsedSeconds)}</span>
</div>
)}
</div>
</div>
);
}
export default InvestigationSummaryPanel;
+185 -5
View File
@@ -4,6 +4,7 @@ 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";
// ── Technical summary detector (main view filters these) ───
const TECHNICAL_PATTERNS = [
@@ -25,6 +26,125 @@ function isTechnicalSummary(summary) {
return false;
}
// ── Recovery state components (Phase 2) ───────────────────────
function ProviderUnavailableCard({ onRestart }) {
return (
<div className="rounded-lg border border-amber-300 bg-amber-50 px-5 py-6 text-center">
<h2 className="mb-1 text-sm font-bold uppercase tracking-wide text-amber-700">Provider unavailable</h2>
<p className="text-sm text-amber-800 mb-4">
The reasoning service could not be reached. This is usually temporary check that the local model is running and try again.
</p>
{onRestart && (
<button
onClick={onRestart}
className="rounded-lg border border-amber-300 bg-white px-4 py-2 text-sm font-medium text-amber-800 hover:bg-amber-100"
>
Restart investigation
</button>
)}
</div>
);
}
function MalformedResponseCard({ onRestart }) {
return (
<div className="rounded-lg border border-orange-300 bg-orange-50 px-5 py-6 text-center">
<h2 className="mb-1 text-sm font-bold uppercase tracking-wide text-orange-700">Unexpected response</h2>
<p className="text-sm text-orange-800 mb-4">
The reasoning service returned a response we could not interpret. This may indicate a temporary issue with the model output format.
</p>
{onRestart && (
<button
onClick={onRestart}
className="rounded-lg border border-orange-300 bg-white px-4 py-2 text-sm font-medium text-orange-800 hover:bg-orange-100"
>
Try again
</button>
)}
</div>
);
}
function UnexpectedStateCard({ stateName, onRetry, onRestart }) {
return (
<div className="rounded-lg border border-red-300 bg-red-50 px-5 py-6 text-center">
<h2 className="mb-1 text-sm font-bold uppercase tracking-wide text-red-700">Unexpected state</h2>
<p className="text-sm text-red-800 mb-4">
{stateName ? `The system is in an unexpected state (${stateName}).` : "An unexpected internal error occurred."}
Please restart the investigation to continue.
</p>
<div className="flex items-center justify-center gap-3">
{onRetry && (
<button
onClick={onRetry}
className="rounded-lg border border-red-300 bg-white px-4 py-2 text-sm font-medium text-red-800 hover:bg-red-100"
>
Retry update
</button>
)}
{onRestart && (
<button
onClick={onRestart}
className="rounded-lg bg-red-700 px-4 py-2 text-sm font-medium text-white hover:bg-red-600"
>
Restart investigation
</button>
)}
</div>
</div>
);
}
function ContinueLaterBanner({ onRestart }) {
return (
<div className="rounded-lg border border-blue-200 bg-blue-50 px-5 py-4 text-center">
<p className="text-sm text-blue-800">
Your previous investigation state is still saved. You can continue where you left off or start fresh.
</p>
{onRestart && (
<button
onClick={onRestart}
className="mt-2 text-sm font-medium text-blue-700 underline hover:text-blue-900"
>
Restart investigation
</button>
)}
</div>
);
}
// ── 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
@@ -387,6 +507,18 @@ function LoadingOverlay({ isLoading, elapsed, currentMessage, variant }) {
}
// ── 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,
@@ -397,10 +529,25 @@ export default function ReasoningWorkspace({
setAnswer,
onAnswerSubmit,
lastSubmittedAnswer,
onRestart,
}) {
const [investigationHistory, setInvestigationHistory] = useState([]);
const turnCounter = useRef(0);
const pendingTurnRef = useRef(null);
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) => {
@@ -435,6 +582,21 @@ export default function ReasoningWorkspace({
await onAnswerSubmit(e);
};
const graph = result?.situationGraph ?? null;
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"
@@ -455,10 +617,6 @@ export default function ReasoningWorkspace({
hasSelectedQuestion;
const selectedQ = result?.selectedQuestion ?? null;
const graph = result?.situationGraph ?? null;
const diagnostics = result?.diagnostics ?? null;
const newlySurfacedNodeIds = result?.newlySurfacedNodeIds || [];
const genuineCompletion = hasGenuineCompletion(graph);
// Determine whether the Current Understanding card should render:
// — when there is a durable plain-language understanding, or
@@ -485,6 +643,25 @@ export default function ReasoningWorkspace({
/>
)}
{/* ── Provider unavailable recovery ──────────────── */}
{isProviderUnavailable && (
<ProviderUnavailableCard onRestart={onRestart} />
)}
{/* ── Malformed response recovery ────────────────── */}
{isMalformedResponse && (
<MalformedResponseCard onRestart={onRestart} />
)}
{/* ── Unexpected state recovery ──────────────────── */}
{(errorType === "unexpected-state") && result && (
<UnexpectedStateCard
stateName={result.stage || null}
onRetry={updateStatus === "error" ? onRestart : null}
onRestart={onRestart}
/>
)}
{/* ── No graph produced after initial analysis ───────── */}
{(status === "success" || status === "error") && !graph ? (
<div className="rounded-lg border border-yellow-300 bg-yellow-50 px-4 py-3 text-sm text-yellow-800">
@@ -494,6 +671,9 @@ export default function ReasoningWorkspace({
</div>
) : (
<>
{/* ── Investigation summary card ─────────────── */}
<InvestigationSummaryPanel graph={graph} selectedQuestion={selectedQ} result={result} />
{/* ── Active investigation: question + form (top priority) ─ */}
{canAnswer && (
<>
@@ -583,4 +763,4 @@ export default function ReasoningWorkspace({
);
}
export { useLoadingStatus, INITIAL_MESSAGES, UPDATE_MESSAGES, LoadingOverlay, resolveCurrentSummary, isTechnicalSummary };
export { useLoadingStatus, INITIAL_MESSAGES, UPDATE_MESSAGES, LoadingOverlay, resolveCurrentSummary, isTechnicalSummary, ContinueLaterBanner };
+123 -3
View File
@@ -3,8 +3,8 @@
import React, { useEffect } from "react";
import { useState, useRef, useMemo } from "react";
import DiagnosticsView from "@/components/diagnostics-view";
import ReasoningWorkspace, { LoadingOverlay } from "@/components/reasoning-workspace";
import { mockFetch } from "@/lib/mocks/confidence-engine/mock-client";
import ReasoningWorkspace, { LoadingOverlay, ContinueLaterBanner } from "@/components/reasoning-workspace";
import { mockFetch, AVAILABLE_SCENARIOS } from "@/lib/mocks/confidence-engine/mock-client";
/* 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";
@@ -186,6 +186,27 @@ 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; }
}
function saveSession(state) {
if (typeof sessionStorage === "undefined") return;
try { sessionStorage.setItem(SESSION_KEY, JSON.stringify(state)); } catch (_) {}
}
function clearSession() {
if (typeof sessionStorage === "undefined") return;
try { sessionStorage.removeItem(SESSION_KEY); } catch (_) {}
}
export default function ScenarioForm() {
const [scenario, setScenario] = useState("");
const [status, setStatus] = useState("idle"); // idle | loading | error | success
@@ -196,11 +217,52 @@ export default function ScenarioForm() {
const [updateResult, setUpdateResult] = useState(null);
const [lastSubmittedAnswer, setLastSubmittedAnswer] = useState("");
const [currentUnderstanding, setCurrentUnderstanding] = useState(null);
const [mockScenario, setMockScenario] = useState("");
const textareaRef = useRef(null);
/* Restore persisted session on mount (Phase 3) ─────────── */
useEffect(() => {
if (typeof window === "undefined") return;
const saved = getSession();
if (!saved) return;
setScenario(saved.scenario || "");
setResult(saved.situationGraph ? { ...saved, situationGraph: saved.situationGraph } : null);
setCurrentUnderstanding(saved.summary || null);
setStatus("success");
}, []);
/* Inject mock globals so the interceptor can read them at runtime */
useMockGlobals();
function handleScenarioSelect(key) {
setMockScenario(key);
if (typeof window !== "undefined") {
window.__MOCK_SCENARIO = key;
}
// Auto-fill central statement for quick start
var found = AVAILABLE_SCENARIOS.find(function(s) { return s.key === key; });
if (found && found.centralStatement) {
setScenario(found.centralStatement);
}
}
function handleScenarioFill(key) {
handleScenarioSelect(key);
setStatus("loading");
setResult(null);
setAnswer("");
setUpdateStatus("idle");
setUpdateResult(null);
setLastSubmittedAnswer("");
setCurrentUnderstanding(null);
setUpdateError(null);
// Simulate a click on the analyse button after auto-filling
setTimeout(function() {
var btn = document.querySelector('button[type="submit"]');
if (btn && !btn.disabled) btn.click();
}, 50);
}
const { elapsed: startElapsed, currentMessage: startMsg } = useLoadingStatus(
INITIAL_MESSAGES,
status === "loading"
@@ -230,7 +292,9 @@ export default function ScenarioForm() {
if (res.ok && data.success) {
setStatus("success");
setCurrentUnderstanding(data.summary ?? null);
setResult(normaliseStartResult(data));
const normalised = normaliseStartResult(data);
setResult(normalised);
saveSession({ scenario, situationGraph: normalised.situationGraph, selectedQuestion: normalised.selectedQuestion, summary: data.summary ?? null, updatedAt: new Date().toISOString() });
} else {
setStatus("error");
setCurrentUnderstanding(data.summary ?? null);
@@ -292,6 +356,8 @@ export default function ScenarioForm() {
diagnostics: outcome.diagnostics,
}));
setAnswer("");
// Persist after successful update turn
saveSession({ scenario, situationGraph: outcome.updatedSituationGraph, selectedQuestion: normaliseUpdateSelectedQuestion(outcome.selectedQuestion), summary: outcome.summary ?? currentUnderstanding, updatedAt: new Date().toISOString() });
} else {
setUpdateStatus("error");
setUpdateError(outcome);
@@ -329,6 +395,43 @@ export default function ScenarioForm() {
</form>
)}
{status === "idle" && MOCK_ENABLED && (
<details className="rounded-lg border border-gray-200 bg-gray-50">
<summary className="cursor-pointer px-4 py-2 text-sm font-medium text-gray-600">Developer details</summary>
<div className="space-y-3 px-4 pb-4">
<div>
<label htmlFor="mock-scenario" className="block text-xs font-medium text-gray-500 mb-1">Mock scenario</label>
<select
id="mock-scenario"
value={mockScenario}
onChange={(e) => handleScenarioSelect(e.target.value)}
className="w-full rounded-md border border-gray-300 px-3 py-2 text-sm focus:border-gray-500 focus:outline-none focus:ring-2 focus:ring-gray-400"
>
<option value=""> default (env var) </option>
{AVAILABLE_SCENARIOS.map(function(s) {
return <option key={s.key} value={s.key}>{s.label}</option>;
})}
</select>
</div>
<div className="flex flex-wrap gap-2">
{AVAILABLE_SCENARIOS.map(function(s) {
return (
<button
key={s.key}
type="button"
onClick={() => handleScenarioFill(s.key)}
disabled={!scenario.trim() && scenario !== s.centralStatement}
className="rounded-md border border-gray-200 bg-white px-3 py-1.5 text-xs text-gray-600 hover:bg-gray-100 disabled:opacity-30"
>
{s.label} ({s.turnCount} turns)
</button>
);
})}
</div>
</div>
</details>
)}
{/* ── Initial analysis loading card ─────────────── */}
{status === "loading" && (
<LoadingOverlay
@@ -358,14 +461,31 @@ export default function ScenarioForm() {
setAnswer={setAnswer}
onAnswerSubmit={handleUpdate}
lastSubmittedAnswer={lastSubmittedAnswer}
onRestart={() => {
clearSession();
setStatus("idle");
setResult(null);
setAnswer("");
setUpdateStatus("idle");
setUpdateResult(null);
setLastSubmittedAnswer("");
setCurrentUnderstanding(null);
setUpdateError(null);
}}
/>
)}
{/* ── Continue later banner when session was restored ── */}
{status === "success" && result?.updatedAt && (
<ContinueLaterBanner onRestart={() => { clearSession(); setStatus("idle"); setResult(null); setAnswer(""); setUpdateStatus("idle"); setCurrentUnderstanding(null); }} />
)}
{/* Reset button after successful analysis */}
{status === "success" && (
<div className="text-center">
<button
onClick={() => {
clearSession();
setScenario("");
setStatus("idle");
setResult(null);
+114
View File
@@ -0,0 +1,114 @@
# Reasoning Contract Backlog
This document tracks every field that the UI currently mocks because the
reasoning engine does not yet provide it. Each row maps a UI need to the
temporary workaround and the desired eventual contract.
## Legend
| Column | Purpose |
| ----------------- | --------------------------------------------------------------------------------------------------- |
| **Feature** | The UX / component that needs this field |
| **UI need** | What the interface is trying to communicate |
| **Temporary mock** | How the UI fakes or derives the value today |
| **Desired output** | What the reasoning engine should eventually emit |
| **Likely stage** | Which reasoning phase would naturally produce this data |
| **Notes** | Context, constraints, open questions |
---
## Status / State
| Feature | UI need | Temporary mock | Desired reasoning output | Likely stage | Notes |
| ---------------------- | ---------------------------------- | --------------------------------------------------- | -------------------------------------------------------------- | ---------------------- | -------------------------------------------------------- |
| InvestigationSummaryPanel | Current status indicator (investigating / complete / evidence_limit) | Derives from `selectedQuestion` existence + `resolvedNodeIds` count | Explicit `status` enum: `"investigating"`, `"resolution_achieved"`, `"evidence_limit_reached"` | Post-investigation finalisation | Should be emitted after the engine decides there are no more useful questions |
| InvestigationSummaryPanel | Elapsed time since last update | Computes `Date.now() - result.updatedAt` | Engine-provided `lastUpdatedAt` on every turn | Every API response | UI already stores this; needs confirmation from reasoning |
## Understanding / Summaries
| Feature | UI need | Temporary mock | Desired reasoning output | Likely stage | Notes |
| ---------------------- | ---------------------------------- | --------------------------------------------------- | -------------------------------------------------------------- | ---------------------- | -------------------------------------------------------- |
| CurrentUnderstandingCard, InvestigationSummaryPanel | Durable plain-language synthesis of current state | `result.summary` → falls back to `graph.currentSummary` | A single `summary` string that represents the latest synthesis | Final summary step; updated at each turn end | Must be stable across refreshes; separate from graph data |
| CurrentUnderstandingCard | Filter technical summaries from plain-language ones | Heuristic regex against keywords (`nodes`, `edges`, `by_kind`) | Boolean `isPlainLanguageSummary` flag or guaranteed plain-language field | Every turn | Regex is fragile; engine should guarantee output quality |
## Questions & Unknowns
| Feature | UI need | Temporary mock | Desired reasoning output | Likely stage | Notes |
| ---------------------- | ---------------------------------- | --------------------------------------------------- | -------------------------------------------------------------- | ---------------------- | -------------------------------------------------------- |
| InvestigationSummaryPanel | Questions answered count | Counts unknown nodes with `status === "resolved"` or in `resolvedNodeIds` | Explicit list of `resolvedUnknownIds` from engine | Post-each turn | Current heuristic conflates structural resolution with questioning |
| InvestigationSummaryPanel | Still working on count | `total unknowns - resolved` | Total identified unknowns minus resolved | Finalisation | Should not imply 1 unknown = 1 question |
| ReasoningWorkspace | Active question (next useful) | `selectedQuestion.question` from start/update API | Same — but engine should guarantee a question exists when `status === "investigating"` | Question selection phase | If no question is available, engine should emit `evidence_limit_reached` instead |
| ScenarioForm | Selected question reason / "why this matters" | `selectedQuestion.reason` from fixture | Same — but guaranteed on every turn | Question selection | Already partially wired; just needs consistent coverage |
| ReasoningWorkspace | Question reasoning pattern metadata | `selectedQuestion.reasoningPattern` | Engine should emit the pattern class for UI display (e.g. "comparability_check") | Question selection | Used in Developer details; could also inform UI tooltips |
## Graph & Evidence
| Feature | UI need | Temporary mock | Desired reasoning output | Likely stage | Notes |
| ---------------------- | ---------------------------------- | --------------------------------------------------- | -------------------------------------------------------------- | ---------------------- | -------------------------------------------------------- |
| SituationGraphView | Node confidence values | Mock `confidence` ("low"/"medium"/"high") | Computed confidence per node from evidence weight | Graph construction | UI displays as indicators; needs numeric or ordinal source |
| SituationGraphView | Confidence assessment breakdown | `confidenceAssessment.evidenceConfidence`, `completenessStatus`, `conclusionConfidence` | Structured confidence assessment with sub-scores | Evidence analysis | Currently flat mock object |
| DeveloperDetails | Active unknown node ID | `graph.activeUnknownNodeId` from fixture | Explicit active target for next investigation step | Question selection | Internal reference; exposed via developer view only |
| ReasoningWorkspace | Newly surfaced unknown nodes | Scenarios provide `proposal.addedNodes` or mocks a static list | Engine emits `newlySurfacedNodeIds` per turn | Each update turn | UI highlights these to show what the investigation discovered |
| ScenarioForm | Node kind discrimination (observation / assumption / conclusion / unknown) | Hardcoded kind values in mock fixtures | Engine classifies each node correctly | Graph construction | Critical for correct display and reasoning traceability |
| ScenarioForm | Edge relationships | Mock `relationship` ("supports", "undermines") | Engine emits relationship type between nodes | Graph construction | Needed for developer view; affects UI if confidence model expands |
## Evidence & Resolution Tracking
| Feature | UI need | Temporary mock | Desired reasoning output | Likely stage | Notes |
| ---------------------- | ---------------------------------- | --------------------------------------------------- | -------------------------------------------------------------- | ---------------------- | -------------------------------------------------------- |
| DeveloperDetails | `evidenceIds` per node | Empty array `[]` in every mock node | List of evidence nodes supporting this node | Graph construction | Needed for traceability in developer view |
| DeveloperDetails | `dependsOn` / `affects` per node | Empty arrays `[]` in mock nodes | Dependency and effect edges | Graph construction | Shows reasoning structure; currently hidden in collapsed developer details |
| InvestigationSummaryPanel | Whether evidence limit has been reached (terminal state) | Infers from `activeUnknownNodeId === null` + unresolved unknowns present | Explicit terminal status flag from engine | Post-evaluation | UI shows "Current evidence limit reached" card |
| ReasoningWorkspace | `resolvedNodeIds` from update | Mocked from scenario fixture; mirrors resolved unknown IDs | Engine emits `resolvedUnknownNodeIds` per turn | Update response | Used to mark answered questions in history |
| ReasoningWorkspace | `affectedNodeIds` from update | Empty array in mock | List of nodes changed by this answer | Update response | Developer view; shows ripple effects |
## Diagnostics & Technical Metadata
| Feature | UI need | Temporary mock | Desired reasoning output | Likely stage | Notes |
| ---------------------- | ---------------------------------- | --------------------------------------------------- | -------------------------------------------------------------- | ---------------------- | -------------------------------------------------------- |
| DiagnosticsView | `promptVersion` | Hardcoded `"v0.4"` in mocks | Actual prompt version used for this turn | Every request | Useful for debugging and rollout tracking |
| DiagnosticsView | `modelName` | Hardcoded `"mock-ollama"` | Actual model identifier | Every request | Needed when multiple models are supported |
| DiagnosticsView | `responseDurationMs` | Zeroed in mocks | Actual response duration | Every request | Shows user how long reasoning took |
| DiagnosticsView | `validationStatus` | Hardcoded `"valid"` | Whether the output passed structured-validation | Post-processing | UI already uses this to decide if graph was parsed |
| DeveloperDetails | `proposal` details (addedNodes, updatedNodes) | Mocked from scenario fixture | Full proposal metadata from reasoning engine | Update response | Shows what changed and why |
## Recovery & Error States
| Feature | UI need | Temporary mock | Desired reasoning output | Likely stage | Notes |
| ---------------------- | ---------------------------------- | --------------------------------------------------- | -------------------------------------------------------------- | ---------------------- | -------------------------------------------------------- |
| ReasoningWorkspace (ProviderUnavailableCard) | Detect provider/network failure | Regex on `error` string (`provider`, `unavailable`) | Explicit `providerAvailable: false` flag or HTTP status | Request time | Should distinguish transient from permanent failures |
| ReasoningWorkspace (MalformedResponseCard) | Detect unstructured / invalid JSON response | Regex on `error` string (`malformed`, `parse`, `structured`) | Explicit `validationError` object with path details | Post-processing | UI needs to know the validation failure for debugging |
| ReasoningWorkspace (UnexpectedStateCard) | Detect internal engine error | `stage === "unexpected"` from mock | Engine-specific error code + recoverable flag | Any stage | Should distinguish recoverable vs unrecoverable errors |
## Investigation Lifecycle
| Feature | UI need | Temporary mock | Desired reasoning output | Likely stage | Notes |
| ---------------------- | ---------------------------------- | --------------------------------------------------- | -------------------------------------------------------------- | ---------------------- | -------------------------------------------------------- |
| InvestigationSummaryPanel | `investigationStartedAt` timestamp | Uses `result.updatedAt` (from session storage) | Engine-provided `investigationStartedAt` on start response | Start case | Currently uses last-updated time as fallback; inaccurate |
| InvestigationSummaryPanel | `lastUpdatedAt` timestamp | Session `updatedAt` persisted by UI | Engine-provided timestamp on every update response | Every turn | UI already tracks this via session hook |
| ReasoningWorkspace | Genuine completion detection | Heuristic: all unknowns resolved + no active question | Explicit `genuineCompletion: true` from engine | Post-evaluation | Should distinguish "everything resolved" from "stalled" |
| CompletionCard | Final summary for complete state | `propUnderstanding` or `graph.currentSummary` | Engine-emitted final conclusion when all unknowns are resolved | Finalisation | Distinct from intermediate summaries |
| EvidenceLimitCard | Final summary at evidence limit | Same as above | Engine-emitted terminal summary when no more questions are useful | Finalisation | UI card style differs from CompletionCard |
## Scenario & Central Statement
| Feature | UI need | Temporary mock | Desired reasoning output | Likely stage | Notes |
| ---------------------- | ---------------------------------- | --------------------------------------------------- | -------------------------------------------------------------- | ---------------------- | -------------------------------------------------------- |
| OriginalSituation | Central statement display | `scenario` prop (user input) or `graph.centralStatement` | Engine-derived central statement from user input | Start case | UI already handles both; engine should normalise |
| DeveloperDetails | Node descriptions | Mock nodes have `label === description` | Distinct, detailed description per node | Graph construction | Current mock uses label as description; separate fields needed |
## Open Questions / Future Work
1. **Structured confidence scores**: The UI currently mocks ordinal confidence (low/medium/high). The reasoning engine should eventually emit numeric confidence values per node and a computed conclusion confidence, enabling richer visual indicators.
2. **Evidence provenance**: Nodes mock empty `evidenceIds`. The engine should emit which observation nodes support each assumption/conclusion, enabling the developer view to show full evidence chains.
3. **Turn-level diagnostics**: Currently only basic validation metadata is mocked. Full turn diagnostics (prompt used, model, duration, temperature, validation results) would help debugging and monitoring.
4. **Terminal state semantics**: The UI distinguishes "resolution_achieved" from "evidence_limit_reached" using heuristics. The engine should emit explicit terminal states so the UI can show the appropriate card without inference.
5. **Session integrity**: The session persistence hook (Phase 3) stores `situationGraph` + `selectedQuestion` + `summary`. If the engine later emits additional fields that affect the UI (e.g., `investigationStartedAt`, `genuineCompletion`), the persisted payload should expand to include them.
6. **Recovery action granularity**: The recovery cards currently offer a single "restart investigation" action. Future engine contracts could support partial recovery (e.g., retry with different parameters, switch models) rather than full restart.
7. **Investigation duration tracking**: The summary panel computes elapsed time from `Date.now() - updatedAt`. If the engine emits proper timestamps, the UI can show accurate elapsed duration and investigate stalls (>5 min between turns).
+41 -105
View File
@@ -4,6 +4,8 @@
* Pure ESM + browser-compatible (no require(), no Node-only APIs).
*/
import { buildScenarioFixture, AVAILABLE_SCENARIOS } from "@/lib/mocks/scenarios.js";
/* ── helpers ─────────────────────────────────────────────── */
function getMockFlag() {
@@ -23,9 +25,9 @@ function getScenario() {
return s || "";
}
/* ── node / edge factories ───────────────────────────────── */
/* ── node / edge factories (re-exported for scenario files) ─ */
function mkNode(id, label, opts) {
export function mkNode(id, label, opts) {
var kind = (opts && opts.kind) || "unknown";
var status = (opts && opts.status) || (kind === "unknown" ? "unknown" : "known");
var confidence = (opts && opts.confidence) || "low";
@@ -37,115 +39,17 @@ function mkNode(id, label, opts) {
};
}
function mkEdge(id, a, b, rel) {
export function mkEdge(id, a, b, rel) {
var r = rel || "supports";
return { id:id, fromNodeId:a, toNodeId:b, relationship:r, confidence:"medium", description:a+" -> "+b };
}
/* ── turn descriptors ────────────────────────────────────── */
var T0_nodes = [
mkNode("obs-1","Complaints increased by 35%",{kind:"observation",status:"known",confidence:"high"}),
mkNode("obs-2","Production increased by 40%",{kind:"observation",status:"known",confidence:"high"}),
mkNode("state-1","Current situation",{kind:"state",status:"provisional",confidence:"medium"}),
mkNode("rel-1","Complaint and production trends are related",{kind:"relationship",status:"known",confidence:"medium"}),
mkNode("u-1","Whether the two figures cover the same period"),
mkNode("u-2","Whether the percentage changes use comparable baselines"),
mkNode("u-3","Whether complaints increased faster than production on a per-unit basis")
];
var T0_edges = [mkEdge("e-1","obs-1","state-1"),mkEdge("e-2","obs-2","state-1"),mkEdge("e-3","rel-1","u-1")];
var T1_nodes = [
mkNode("obs-1","Complaints increased by 35%",{kind:"observation",status:"known",confidence:"high"}),
mkNode("obs-2","Production increased by 40%",{kind:"observation",status:"known",confidence:"high"}),
mkNode("obs-3","Both figures cover the same three-month period",{kind:"observation",status:"known",confidence:"high"}),
mkNode("state-1","Current situation",{kind:"state",status:"provisional",confidence:"medium"}),
mkNode("rel-1","Complaint and production trends are related",{kind:"relationship",status:"known",confidence:"medium"}),
mkNode("u-1","Whether the two figures cover the same period",{status:"resolved",confidence:"high"}),
mkNode("u-2","Whether the percentage changes use comparable baselines"),
mkNode("u-4","Whether reporting practices changed")
];
var T1_edges = [mkEdge("e-1","obs-1","state-1"),mkEdge("e-2","obs-2","state-1"),mkEdge("e-3","rel-1","u-1"),mkEdge("e-4","obs-3","u-1"),mkEdge("e-5","rel-1","u-2")];
var T2_nodes = [
mkNode("obs-1","Complaints increased by 35%",{kind:"observation",status:"known",confidence:"high"}),
mkNode("obs-2","Production increased by 40%",{kind:"observation",status:"known",confidence:"high"}),
mkNode("obs-3","Both figures cover the same three-month period",{kind:"observation",status:"known",confidence:"high"}),
mkNode("obs-4","Complaints rose from 100 to 135; production rose from 1,000 to 1,400 units",{kind:"observation",status:"known",confidence:"high"}),
mkNode("state-1","Current situation",{kind:"state",status:"provisional",confidence:"medium"}),
mkNode("rel-1","Complaint and production trends are related",{kind:"relationship",status:"known",confidence:"medium"}),
mkNode("u-1","Whether the two figures cover the same period",{status:"resolved",confidence:"high"}),
mkNode("u-2","Whether the percentage changes use comparable baselines",{status:"resolved",confidence:"medium"}),
mkNode("u-3","Whether complaints increased faster than production on a per-unit basis"),
mkNode("u-4","Whether reporting practices changed")
];
var T2_edges = [mkEdge("e-1","obs-1","state-1"),mkEdge("e-2","obs-2","state-1"),mkEdge("e-3","rel-1","u-1"),mkEdge("e-4","obs-3","u-1"),mkEdge("e-5","rel-1","u-2"),mkEdge("e-6","obs-4","u-2"),mkEdge("e-7","obs-4","u-3")];
var T3_nodes = [
mkNode("obs-1","Complaints increased by 35%",{kind:"observation",status:"known",confidence:"high"}),
mkNode("obs-2","Production increased by 40%",{kind:"observation",status:"known",confidence:"high"}),
mkNode("obs-3","Both figures cover the same three-month period",{kind:"observation",status:"known",confidence:"high"}),
mkNode("obs-4","Complaints rose from 100 to 135; production rose from 1,000 to 1,400 units",{kind:"observation",status:"known",confidence:"high"}),
mkNode("obs-5","The complaint rate fell from 10 per 1,000 to about 9.6 per 1,000",{kind:"observation",status:"known",confidence:"high"}),
mkNode("state-1","Current situation",{kind:"state",status:"provisional",confidence:"medium"}),
mkNode("rel-1","Complaint and production trends are related",{kind:"relationship",status:"known",confidence:"medium"}),
mkNode("u-1","Whether the two figures cover the same period",{status:"resolved",confidence:"high"}),
mkNode("u-2","Whether the percentage changes use comparable baselines",{status:"resolved",confidence:"medium"}),
mkNode("u-3","Whether complaints increased faster than production on a per-unit basis",{status:"resolved",confidence:"high"}),
mkNode("u-4","Whether reporting practices changed")
];
var T3_edges = [mkEdge("e-1","obs-1","state-1"),mkEdge("e-2","obs-2","state-1"),mkEdge("e-3","rel-1","u-1"),mkEdge("e-4","obs-3","u-1"),mkEdge("e-5","rel-1","u-2"),mkEdge("e-6","obs-4","u-2"),mkEdge("e-7","obs-4","u-3"),mkEdge("e-8","obs-5","u-3")];
var T4_nodes = [
mkNode("obs-1","Complaints increased by 35%",{kind:"observation",status:"known",confidence:"high"}),
mkNode("obs-2","Production increased by 40%",{kind:"observation",status:"known",confidence:"high"}),
mkNode("obs-3","Both figures cover the same three-month period",{kind:"observation",status:"known",confidence:"high"}),
mkNode("obs-4","Complaints rose from 100 to 135; production rose from 1,000 to 1,400 units",{kind:"observation",status:"known",confidence:"high"}),
mkNode("obs-5","The complaint rate fell from 10 per 1,000 to about 9.6 per 1,000",{kind:"observation",status:"known",confidence:"high"}),
mkNode("obs-6","Same complaint categories and reporting rules were used throughout",{kind:"observation",status:"known",confidence:"high"}),
mkNode("state-1","Current situation",{kind:"state",status:"provisional",confidence:"medium"}),
mkNode("rel-1","Complaint and production trends are related",{kind:"relationship",status:"known",confidence:"medium"}),
mkNode("u-1","Whether the two figures cover the same period",{status:"resolved",confidence:"high"}),
mkNode("u-2","Whether the percentage changes use comparable baselines",{status:"resolved",confidence:"medium"}),
mkNode("u-3","Whether complaints increased faster than production on a per-unit basis",{status:"resolved",confidence:"high"}),
mkNode("u-4","Whether reporting practices changed",{status:"resolved",confidence:"high"})
];
var T4_edges = [mkEdge("e-1","obs-1","state-1"),mkEdge("e-2","obs-2","state-1"),mkEdge("e-3","rel-1","u-1"),mkEdge("e-4","obs-3","u-1"),mkEdge("e-5","rel-1","u-2"),mkEdge("e-6","obs-4","u-2"),mkEdge("e-7","obs-4","u-3"),mkEdge("e-8","obs-5","u-3"),mkEdge("e-9","rel-1","u-4"),mkEdge("e-10","obs-6","u-4")];
var TURNS = [
{ nodes:T0_nodes, edges:T0_edges, resolved:[], active:"u-1", question:{ nodeId:"u-1", question:"Were the complaint and production figures measured over the same period?", reason:"If the figures cover different periods, comparing their movement could be misleading.", reasoningPattern:"comparability_check" }, noQReason:null, summary:"Two changes have been reported, but we do not yet know whether the figures are directly comparable." },
{ nodes:T1_nodes, edges:T1_edges, resolved:["u-1"], active:"u-2", question:{ nodeId:"u-2", question:"Were both percentages calculated from comparable baseline counts?", reason:"Establishing the reference point for both figures is essential before evaluating their relationship.", reasoningPattern:"baseline_comparability" }, noQReason:null, summary:"The timing basis is now clear." },
{ nodes:T2_nodes, edges:T2_edges, resolved:["u-1","u-2"], active:"u-3", question:{ nodeId:"u-3", question:"Did the complaint rate per unit produced improve or worsen?", reason:"Absolute changes in complaints and production are known; the relative rate determines whether the situation improved.", reasoningPattern:"rate_comparison" }, noQReason:null, summary:"The absolute baselines are now known." },
{ nodes:T3_nodes, edges:T3_edges, resolved:["u-1","u-2","u-3"], active:"u-4", question:{ nodeId:"u-4", question:"Was there any change in how complaints were recorded during the period?", reason:"The per-unit rate changed; we need to rule out recording artifacts before concluding a genuine shift.", reasoningPattern:"artifact_exclusion" }, noQReason:null, summary:"The per-unit complaint rate improved slightly." },
{ nodes:T4_nodes, edges:T4_edges, resolved:["u-1","u-2","u-3","u-4"], active:null, question:null, noQReason:"All required investigation areas are resolved.", summary:"The figures cover the same period, use comparable baselines, show an improved complaint rate, and were recorded consistently." }
];
/* ── fixture builders ─────────────────────────────────────── */
function buildDefaultFixture(idx) {
var d = TURNS[Math.min(idx, TURNS.length - 1)];
return {
success:true,
situationGraph: { centralStatement:"Complaints increased by 35% while production increased by 40%.", currentSummary:d.summary, nodes:d.nodes, edges:d.edges, activeUnknownNodeId:d.active, resolvedNodeIds:d.resolved },
selectedQuestion: d.question || null,
noQuestionReason: d.noQReason,
newlySurfacedNodeIds: [],
diagnostics: { promptVersion:"v0.4", modelName:"mock-ollama", responseDurationMs:0, validationStatus:"valid", nodeCount:d.nodes.length, edgeCount:d.edges.length, unknownSelectionExplanation: d.active ? { status:"single_candidate" } : null }
};
}
function buildErrorFixture() {
return { success:false, situationGraph:null, selectedQuestion:null, noQuestionReason:null, newlySurfacedNodeIds:[], error:"Mock provider error: structured response unavailable.", diagnostics:null };
}
function buildUpdateFixture(scenarioName, idx) {
if (scenarioName === "error") {
return { success:false, stage:"provider", error:"Mock provider error: structured response unavailable.", providerErrors:["Mock provider error: structured response unavailable."], updatedSituationGraph:null, selectedQuestion:null, affectedNodeIds:[], resolvedUnknownNodeIds:[], changesApplied:null, summary:null, diagnostics:{ promptVersion:"v0.4", modelName:"mock-ollama", responseDurationMs:0 } };
}
var f = scenarioName === "complete" ? buildDefaultFixture(4) : buildDefaultFixture(idx);
return { success:true, stage:"update_applied", updatedSituationGraph:f.situationGraph, selectedQuestion:f.selectedQuestion, affectedNodeIds:[], resolvedUnknownNodeIds:(f.situationGraph.resolvedNodeIds||[]).slice(), changesApplied:{ addedNodeCount:0, updatedNodeCount:0, addedEdgeCount:0, removedEdgeCount:0 }, summary:f.situationGraph.currentSummary||null, diagnostics:f.diagnostics };
}
/* ── delay shim (browser only) ──────────────────────────── */
function delay(ms) {
@@ -163,20 +67,50 @@ function handleStartCase(scenario) {
_turnIndex = 0;
var scenarioName = getScenario();
if (scenarioName === "error") return Promise.resolve({ success:true, data:buildErrorFixture() });
return Promise.resolve({ success:true, data:buildDefaultFixture(0) });
return Promise.resolve({ success:true, data: buildScenarioFixture(scenarioName, 0) || buildDefaultFallback(0) });
}
function handleUpdateCase(data) {
var scenarioName = getScenario();
if (scenarioName === "error") return delay(getDelay()).then(function() {
return Promise.resolve({ success:true, data:buildUpdateFixture("error",0) });
return Promise.resolve({ success:true, data:{ success:false, stage:"provider", error:"Mock provider error: structured response unavailable.", providerErrors:["Mock provider error: structured response unavailable."], updatedSituationGraph:null, selectedQuestion:null, affectedNodeIds:[], resolvedUnknownNodeIds:[], changesApplied:null, summary:null, diagnostics:{ promptVersion:"v0.4", modelName:"mock-ollama", responseDurationMs:0 } } });
});
_turnIndex++;
var idx = scenarioName === "complete" ? 4 : Math.min(_turnIndex, 4);
return delay(getDelay()).then(function() {
return Promise.resolve({ success:true, data:buildUpdateFixture(scenarioName, idx) });
var fixture = buildScenarioFixture(scenarioName, _turnIndex);
if (fixture) {
return Promise.resolve({
success:true, stage:"update_applied",
updatedSituationGraph:fixture.situationGraph,
selectedQuestion:fixture.selectedQuestion,
affectedNodeIds:[],
resolvedUnknownNodeIds:(fixture.situationGraph.resolvedNodeIds||[]).slice(),
changesApplied:{ addedNodeCount:0, updatedNodeCount:0, addedEdgeCount:0, removedEdgeCount:0 },
summary:fixture.situationGraph.currentSummary||null,
diagnostics:fixture.diagnostics
});
}
// Fallback to original default if scenario not found
return Promise.resolve({ success:true, data:buildUpdateFallback(scenarioName) });
});
}
/* ── fallback for when scenarios.js is not available ─────── */
var _fallbackTurns = [
{ nodes:[mkNode("obs-1","Complaints increased by 35%",{kind:"observation",status:"known",confidence:"high"}),mkNode("obs-2","Production increased by 40%",{kind:"observation",status:"known",confidence:"high"}),mkNode("state-1","Current situation",{kind:"state",status:"provisional",confidence:"medium"})], edges:[mkEdge("e-1","obs-1","state-1"),mkEdge("e-2","obs-2","state-1")], resolved:[], active:"u-1", question:{ nodeId:"u-1", question:"Were the complaint and production figures measured over the same period?", reason:"If different periods, comparing movement could be misleading.", reasoningPattern:"comparability_check" }, noQReason:null, summary:"Two changes have been reported, but we do not yet know whether the figures are directly comparable." },
{ nodes:[mkNode("obs-1","Complaints increased by 35%",{kind:"observation",status:"known",confidence:"high"}),mkNode("obs-2","Production increased by 40%",{kind:"observation",status:"known",confidence:"high"}),mkNode("obs-3","Both figures cover the same three-month period",{kind:"observation",status:"known",confidence:"high"}),mkNode("state-1","Current situation",{kind:"state",status:"provisional",confidence:"medium"})], edges:[mkEdge("e-1","obs-1","state-1"),mkEdge("e-2","obs-2","state-1"),mkEdge("e-3","obs-3","u-1")], resolved:["u-1"], active:"u-2", question:{ nodeId:"u-2", question:"Were both percentages calculated from comparable baseline counts?", reason:"Establishing the reference point is essential.", reasoningPattern:"baseline_comparability" }, noQReason:null, summary:"The timing basis is now clear." }
];
function buildDefaultFallback(idx) {
var d = _fallbackTurns[Math.min(idx, _fallbackTurns.length - 1)];
return { success:true, situationGraph:{ centralStatement:"Complaints increased by 35% while production increased by 40%.", currentSummary:d.summary, nodes:d.nodes, edges:d.edges, activeUnknownNodeId:d.active, resolvedNodeIds:d.resolved }, selectedQuestion:d.question||null, noQuestionReason:d.noQReason, newlySurfacedNodeIds:[], diagnostics:{ promptVersion:"v0.4", modelName:"mock-ollama", responseDurationMs:0, validationStatus:"valid", nodeCount:d.nodes.length, edgeCount:d.edges.length } };
}
function buildUpdateFallback(scenarioName) {
var f = buildDefaultFallback(Math.min(_turnIndex, _fallbackTurns.length - 1));
return { success:true, stage:"update_applied", updatedSituationGraph:f.situationGraph, selectedQuestion:f.selectedQuestion, affectedNodeIds:[], resolvedUnknownNodeIds:(f.situationGraph.resolvedNodeIds||[]).slice(), changesApplied:{ addedNodeCount:0, updatedNodeCount:0, addedEdgeCount:0, removedEdgeCount:0 }, summary:f.situationGraph.currentSummary||null, diagnostics:f.diagnostics };
}
/* ── public intercept function ──────────────────────────── */
@@ -198,3 +132,5 @@ export async function mockFetch(url, options) {
return fetch(url, options);
}
export { AVAILABLE_SCENARIOS };
+373
View File
@@ -0,0 +1,373 @@
/**
* Expanded mock scenario library for the Confidence Engine workspace.
* Each scenario produces a complete investigation journey through turns.
*
* UI-only development work — no reasoning engine changes.
*/
/* ── Node / Edge factories ─────────────────────────────── */
export function mkN(id, label, opts) {
var kind = (opts && opts.kind) || "unknown";
var status = (opts && opts.status) || (kind === "unknown" ? "unknown" : "known");
var confidence = (opts && opts.confidence) || "low";
return {
id:id, label:label, description:label, kind:kind, status:status, confidence:confidence,
confidenceAssessment:{ evidenceConfidence:confidence, completenessStatus:"partial", conclusionConfidence:confidence },
value:(opts && opts.value !== undefined) ? opts.value : null,
unit:(opts && opts.unit) || null, evidenceIds:[], dependsOn:[], affects:[], childIds:[]
};
}
export function mkE(id, a, b, rel) {
var r = rel || "supports";
return { id:id, fromNodeId:a, toNodeId:b, relationship:r, confidence:"medium", description:a+" -> "+b };
}
/* ── Scenario: Comparison (product ratings) ───────────── */
var comparisonTurns = [
{
centralStatement: "Product A has a 4.2 star average rating while Product B averages 4.6 stars across 10,000+ reviews each.",
nodes: [
mkN("obs-1","Product A average rating: 4.2 stars",{kind:"observation",status:"known",confidence:"high"}),
mkN("obs-2","Product B average rating: 4.6 stars",{kind:"observation",status:"known",confidence:"high"}),
mkN("obs-3","Both products have 10,000+ reviews",{kind:"observation",status:"known",confidence:"high"}),
mkN("state-1","Comparing two products before purchase decision",{kind:"state",status:"provisional",confidence:"medium"}),
mkN("u-1","Whether the rating systems are comparable"),
],
edges: [mkE("e-1","obs-1","state-1"),mkE("e-2","obs-2","state-1"),mkE("e-3","obs-3","u-1")],
resolved:[], active:"u-1",
question:{ nodeId:"u-1", question:"Are both products rated on the same validated scale?", reason:"Different rating systems could make direct comparison meaningless.", reasoningPattern:"comparability_check" },
noQReason:null, summary:"Two products have been rated highly, but we do not yet know whether their ratings are measured the same way."
},
{
centralStatement: "Product A has a 4.2 star average rating while Product B averages 4.6 stars across 10,000+ reviews each.",
nodes: [
mkN("obs-1","Product A average rating: 4.2 stars",{kind:"observation",status:"known",confidence:"high"}),
mkN("obs-2","Product B average rating: 4.6 stars",{kind:"observation",status:"known",confidence:"high"}),
mkN("obs-3","Both products have 10,000+ reviews",{kind:"observation",status:"known",confidence:"high"}),
mkN("obs-4","Both use the standard 5-star customer review scale",{kind:"observation",status:"known",confidence:"high"}),
mkN("state-1","Comparing two products before purchase decision",{kind:"state",status:"provisional",confidence:"medium"}),
mkN("u-1","Whether the rating systems are comparable",{status:"resolved",confidence:"high"}),
mkN("u-2","Whether verified purchase reviews differ significantly between the two products"),
],
edges: [mkE("e-1","obs-1","state-1"),mkE("e-2","obs-2","state-1"),mkE("e-3","obs-3","u-1"),mkE("e-4","obs-4","u-1"),mkE("e-5","obs-3","u-2")],
resolved:["u-1"], active:"u-2",
question:{ nodeId:"u-2", question:"Do verified purchase reviews show a similar gap between the two products?", reason:"Fake or unverified reviews could inflate ratings.", reasoningPattern:"evidence_quality" },
noQReason:null, summary:"The rating scales are comparable. The next uncertainty is review authenticity."
},
{
centralStatement: "Product A has a 4.2 star average rating while Product B averages 4.6 stars across 10,000+ reviews each.",
nodes: [
mkN("obs-1","Product A average rating: 4.2 stars",{kind:"observation",status:"known",confidence:"high"}),
mkN("obs-2","Product B average rating: 4.6 stars",{kind:"observation",status:"known",confidence:"high"}),
mkN("obs-3","Both products have 10,000+ reviews",{kind:"observation",status:"known",confidence:"high"}),
mkN("obs-4","Both use the standard 5-star customer review scale",{kind:"observation",status:"known",confidence:"high"}),
mkN("obs-5","Verified purchase gap remains approximately 0.3 stars in both products' subsets",{kind:"observation",status:"known",confidence:"medium"}),
mkN("state-1","Comparing two products before purchase decision",{kind:"state",status:"provisional",confidence:"medium"}),
mkN("u-1","Whether the rating systems are comparable",{status:"resolved",confidence:"high"}),
mkN("u-2","Whether verified purchase reviews differ significantly",{status:"resolved",confidence:"medium"}),
mkN("u-3","Whether the remaining gap reflects genuine quality difference or a niche preference"),
],
edges: [mkE("e-1","obs-1","state-1"),mkE("e-2","obs-2","state-1"),mkE("e-3","obs-3","u-1"),mkE("e-4","obs-4","u-1"),mkE("e-5","obs-3","u-2"),mkE("e-6","obs-5","u-2"),mkE("e-7","obs-3","u-3")],
resolved:["u-1","u-2"], active:"u-3",
question:{ nodeId:"u-3", question:"Could the remaining rating difference be explained by product niche rather than quality?", reason:"Different customer segments may have different expectations.", reasoningPattern:"alternative_explanation" },
noQReason:null, summary:"Verified reviews confirm the gap is genuine. The remaining question is whether it reflects quality or preference."
}
];
/* ── Scenario: Contradictory Evidence ─────────────────── */
var contradictoryTurns = [
{
centralStatement: "Two consultants provided opposite recommendations about whether to outsource IT operations.",
nodes: [
mkN("obs-1","Consultant A recommends outsourcing based on cost savings data",{kind:"observation",status:"known",confidence:"high"}),
mkN("obs-2","Consultant B recommends against outsourcing citing quality risks",{kind:"observation",status:"known",confidence:"high"}),
mkN("state-1","Making an IT operations decision",{kind:"state",status:"provisional",confidence:"medium"}),
mkN("u-1","Whether the consultants are evaluating the same criteria"),
],
edges: [mkE("e-1","obs-1","state-1"),mkE("e-2","obs-2","state-1")],
resolved:[], active:"u-1",
question:{ nodeId:"u-1", question:"Are the two consultants using comparable evaluation criteria?", reason:"Contradictory conclusions often stem from different starting assumptions.", reasoningPattern:"comparability_check" },
noQReason:null, summary:"Two opposing recommendations exist. Before deciding, we need to know if they are looking at the same thing."
},
{
centralStatement: "Two consultants provided opposite recommendations about whether to outsource IT operations.",
nodes: [
mkN("obs-1","Consultant A recommends outsourcing based on cost savings data",{kind:"observation",status:"known",confidence:"high"}),
mkN("obs-2","Consultant B recommends against outsourcing citing quality risks",{kind:"observation",status:"known",confidence:"high"}),
mkN("obs-3","Consultant A focused on short-term cost reduction over 2 years",{kind:"observation",status:"known",confidence:"high"}),
mkN("obs-4","Consultant B focused on long-term capability retention over 5+ years",{kind:"observation",status:"known",confidence:"high"}),
mkN("state-1","Making an IT operations decision",{kind:"state",status:"provisional",confidence:"medium"}),
mkN("u-1","Whether the consultants are evaluating the same criteria",{status:"resolved",confidence:"high"}),
mkN("u-2","Which time horizon is appropriate for this organisation"),
],
edges: [mkE("e-1","obs-1","state-1"),mkE("e-2","obs-2","state-1"),mkE("e-3","obs-3","u-1"),mkE("e-4","obs-4","u-1"),mkE("e-5","obs-3","u-2"),mkE("e-6","obs-4","u-2")],
resolved:["u-1"], active:"u-2",
question:{ nodeId:"u-2", question:"What time horizon should guide this particular organisation's decision?", reason:"Different horizons produce different valid conclusions.", reasoningPattern:"criteria_alignment" },
noQReason:null, summary:"The consultants disagree because they use different timeframes. The real question is which horizon fits."
}
];
/* ── Scenario: Missing Evidence ──────────────────────── */
var missingEvidenceTurns = [
{
centralStatement: "A hospital wants to determine whether a new patient monitoring system would reduce adverse events.",
nodes: [
mkN("obs-1","Adverse events have been stable at 2.3% for the past year",{kind:"observation",status:"known",confidence:"high"}),
mkN("state-1","Evaluating a new patient monitoring system",{kind:"state",status:"provisional",confidence:"medium"}),
mkN("u-1","Whether the current baseline measurement is reliable"),
mkN("u-2","Whether similar systems have demonstrated effectiveness elsewhere"),
],
edges: [mkE("e-1","obs-1","state-1")],
resolved:[], active:"u-1",
question:{ nodeId:"u-1", question:"How reliably are adverse events currently being measured and reported?", reason:"An unreliable baseline makes any comparison impossible.", reasoningPattern:"measurement_validity" },
noQReason:null, summary:"We have a single data point. Before evaluating any new system, we need to trust the starting measurement."
},
{
centralStatement: "A hospital wants to determine whether a new patient monitoring system would reduce adverse events.",
nodes: [
mkN("obs-1","Adverse events have been stable at 2.3% for the past year",{kind:"observation",status:"known",confidence:"high"}),
mkN("obs-2","Adverse event reporting is incident-based and potentially incomplete",{kind:"observation",status:"known",confidence:"medium"}),
mkN("state-1","Evaluating a new patient monitoring system",{kind:"state",status:"provisional",confidence:"medium"}),
mkN("u-1","Whether the current baseline measurement is reliable",{status:"resolved",confidence:"medium"}),
mkN("u-2","Whether similar systems have demonstrated effectiveness elsewhere"),
],
edges: [mkE("e-1","obs-1","state-1"),mkE("e-2","obs-2","u-1")],
resolved:["u-1"], active:"u-2",
question:{ nodeId:"u-2", question:"Has comparable monitoring technology been deployed in similar hospitals with measured outcomes?", reason:"Without external evidence, this remains a unique test.", reasoningPattern:"precedent_search" },
noQReason:null, summary:"The baseline is uncertain. External evidence would strengthen the case either way."
}
];
/* ── Scenario: Evidence Limit (stuck early) ─────────── */
var evidenceLimitTurns = [
{
centralStatement: "Should a mid-sized manufacturing company invest in automated quality inspection?",
nodes: [
mkN("obs-1","Current defect rate is 3.2%",{kind:"observation",status:"known",confidence:"high"}),
mkN("obs-2","Re call costs total approximately $400K annually",{kind:"observation",status:"known",confidence:"medium"}),
mkN("state-1","Evaluating automated quality inspection investment",{kind:"state",status:"provisional",confidence:"medium"}),
mkN("u-1","Whether the total cost of an automation solution is understood"),
],
edges: [mkE("e-1","obs-1","state-1"),mkE("e-2","obs-2","state-1")],
resolved:[], active:"u-1",
question:{ nodeId:"u-1", question:"What would a complete automation solution cost including installation and training?", reason:"Without knowing the investment required, feasibility cannot be assessed.", reasoningPattern:"cost_feasibility" },
noQReason:null, summary:"Known costs of inaction exist but the cost of action is completely unknown."
}
];
/* ── Scenario: Circular Reasoning ───────────────────── */
var circularTurns = [
{
centralStatement: "A team argues that Project X should continue because it is strategic, and it is strategic because the team believes in it.",
nodes: [
mkN("obs-1","The team believes Project X is important to strategy",{kind:"observation",status:"known",confidence:"high"}),
mkN("u-1","Whether Project X has independent strategic value beyond team conviction"),
],
edges: [],
resolved:[], active:"u-1",
question:{ nodeId:"u-1", question:"What external evidence supports the strategic value of Project X?", reason:"Belief alone cannot establish strategic justification.", reasoningPattern:"circularity_detection" },
noQReason:null, summary:"The argument appears circular. We need evidence independent of team conviction."
}
];
/* ── Scenario: Decision Investigation ──────────────── */
var decisionTurns = [
{
centralStatement: "Should I relocate my engineering team from London to Manchester?",
nodes: [
mkN("obs-1","Manchester office rental costs are approximately 60% lower than London",{kind:"observation",status:"known",confidence:"high"}),
mkN("obs-2","The team has expressed mixed feelings about relocation",{kind:"observation",status:"known",confidence:"medium"}),
mkN("state-1","Deciding on engineering team relocation",{kind:"state",status:"provisional",confidence:"medium"}),
mkN("u-1","Whether the cost savings offset potential talent retention risks"),
],
edges: [mkE("e-1","obs-1","state-1"),mkE("e-2","obs-2","state-1")],
resolved:[], active:"u-1",
question:{ nodeId:"u-1", question:"What would the likely impact on talent retention and recruitment be?", reason:"Cost savings are real but only relevant if the team can still be staffed.", reasoningPattern:"decision" },
noQReason:null, summary:"Financial motivation is clear. The remaining question is whether the workforce will remain."
},
{
centralStatement: "Should I relocate my engineering team from London to Manchester?",
nodes: [
mkN("obs-1","Manchester office rental costs are approximately 60% lower than London",{kind:"observation",status:"known",confidence:"high"}),
mkN("obs-2","The team has expressed mixed feelings about relocation",{kind:"observation",status:"known",confidence:"medium"}),
mkN("obs-3","Manchester has a growing tech ecosystem with 500+ engineering roles posted monthly",{kind:"observation",status:"known",confidence:"medium"}),
mkN("state-1","Deciding on engineering team relocation",{kind:"state",status:"provisional",confidence:"medium"}),
mkN("u-1","Whether the cost savings offset potential talent retention risks",{status:"resolved",confidence:"medium"}),
mkN("u-2","Whether the cultural transition is manageable for a team of this size"),
],
edges: [mkE("e-1","obs-1","state-1"),mkE("e-2","obs-2","state-1"),mkE("e-3","obs-3","u-1"),mkE("e-4","obs-2","u-2")],
resolved:["u-1"], active:"u-2",
question:{ nodeId:"u-2", question:"What support mechanisms would help the team through a geographical transition?", reason:"Mixed feelings are normal but the right support can make it viable.", reasoningPattern:"implementation" },
noQReason:null, summary:"Market conditions in Manchester are promising. The remaining question is cultural."
}
];
/* ── Scenario: Planning Investigation ─────────────── */
var planningTurns = [
{
centralStatement: "We want to launch a new product line within 6 months but have no clear roadmap.",
nodes: [
mkN("obs-1","Target launch window is Q3",{kind:"observation",status:"known",confidence:"high"}),
mkN("state-1","Planning a new product launch",{kind:"state",status:"provisional",confidence:"medium"}),
mkN("u-1","Whether the core product design is complete enough to begin production planning"),
],
edges: [],
resolved:[], active:"u-1",
question:{ nodeId:"u-1", question:"What stage is the product design currently at?", reason:"Production planning cannot begin until design is stable.", reasoningPattern:"planning" },
noQReason:null, summary:"A deadline exists but the product itself has not yet been defined."
}
];
/* ── Scenario: Long Investigation (market entry - 10 turns) ─── */
var longTurns = [
{
centralStatement: "Should we enter the European market with our SaaS analytics platform?",
nodes: [mkN("obs-1","Current revenue is $2M ARR in the US market only",{kind:"observation",status:"known",confidence:"high"}),mkN("state-1","Evaluating European market entry",{kind:"state",status:"provisional",confidence:"medium"}),mkN("u-1","Whether there is genuine demand for our category in Europe")],
edges:[mkE("e-1","obs-1","state-1")], resolved:[], active:"u-1",
question:{ nodeId:"u-1", question:"How large and mature is the analytics SaaS market in Europe?", reason:"Entering a non-existent or negligible market is not justified.", reasoningPattern:"market_validity" }, noQReason:null, summary:"We are US-based. The first question before any expansion is whether demand exists."
},
{
centralStatement: "Should we enter the European market with our SaaS analytics platform?",
nodes: [mkN("obs-1","Current revenue is $2M ARR in the US market only",{kind:"observation",status:"known",confidence:"high"}),mkN("obs-2","European analytics SaaS market valued at approximately €8B and growing 15% annually",{kind:"observation",status:"known",confidence:"medium"}),mkN("state-1","Evaluating European market entry",{kind:"state",status:"provisional",confidence:"medium"}),mkN("u-1","Whether there is genuine demand for our category in Europe",{status:"resolved",confidence:"medium"}),mkN("u-2","Whether our product is suitable for European compliance requirements")],
edges:[mkE("e-1","obs-1","state-1"),mkE("e-2","obs-2","u-1")], resolved:["u-1"], active:"u-2",
question:{ nodeId:"u-2", question:"Does our platform comply with GDPR and other European data regulations?", reason:"Non-compliance makes market entry legally impossible.", reasoningPattern:"compliance" }, noQReason:null, summary:"Demand exists. The next constraint is regulatory."
},
{
centralStatement: "Should we enter the European market with our SaaS analytics platform?",
nodes: [mkN("obs-1","Current revenue is $2M ARR in the US market only",{kind:"observation",status:"known",confidence:"high"}),mkN("obs-2","European analytics SaaS market valued at approximately €8B and growing 15% annually",{kind:"observation",status:"known",confidence:"medium"}),mkN("obs-3","Our platform does not currently support EU data residency requirements",{kind:"observation",status:"known",confidence:"high"}),mkN("state-1","Evaluating European market entry",{kind:"state",status:"provisional",confidence:"medium"}),mkN("u-1","Whether there is genuine demand for our category in Europe",{status:"resolved",confidence:"medium"}),mkN("u-2","Whether our product is suitable for European compliance requirements",{status:"resolved",confidence:"high"}),mkN("u-3","Whether the cost of achieving compliance is justified by the market size")],
edges:[mkE("e-1","obs-1","state-1"),mkE("e-2","obs-2","u-1"),mkE("e-3","obs-3","u-2")], resolved:["u-1","u-2"], active:"u-3",
question:{ nodeId:"u-3", question:"What investment would it take to achieve full EU data residency compliance?", reason:"We know the market exists and we are non-compliant. The remaining question is cost.", reasoningPattern:"cost_benefit" }, noQReason:null, summary:"Compliance is feasible. The remaining question is cost."
},
{
centralStatement: "Should we enter the European market with our SaaS analytics platform?",
nodes: [mkN("obs-1","Current revenue is $2M ARR in the US market only",{kind:"observation",status:"known",confidence:"high"}),mkN("obs-2","European analytics SaaS market valued at approximately €8B and growing 15% annually",{kind:"observation",status:"known",confidence:"medium"}),mkN("obs-3","Our platform does not currently support EU data residency requirements",{kind:"observation",status:"known",confidence:"high"}),mkN("obs-4","Achieving compliance would require approximately 6 months and $500K engineering investment",{kind:"observation",status:"known",confidence:"medium"}),mkN("state-1","Evaluating European market entry",{kind:"state",status:"provisional",confidence:"medium"}),mkN("u-1","Whether there is genuine demand for our category in Europe",{status:"resolved",confidence:"medium"}),mkN("u-2","Whether our product is suitable for European compliance requirements",{status:"resolved",confidence:"high"}),mkN("u-3","Whether the cost of achieving compliance is justified by the market size",{status:"resolved",confidence:"medium"}),mkN("u-4","Whether we have competitive differentiation against existing European players")],
edges:[mkE("e-1","obs-1","state-1"),mkE("e-2","obs-2","u-1"),mkE("e-3","obs-3","u-2"),mkE("e-4","obs-4","u-3")], resolved:["u-1","u-2","u-3"], active:"u-4",
question:{ nodeId:"u-4", question:"What differentiates our platform against established European competitors?", reason:"Market entry requires more than compliance — we need a reason for customers to switch.", reasoningPattern:"competitive_analysis" }, noQReason:null, summary:"Compliance is feasible. The remaining question is competitive edge."
},
{
centralStatement: "Should we enter the European market with our SaaS analytics platform?",
nodes: [mkN("obs-1","Current revenue is $2M ARR in the US market only",{kind:"observation",status:"known",confidence:"high"}),mkN("obs-2","European analytics SaaS market valued at approximately €8B and growing 15% annually",{kind:"observation",status:"known",confidence:"medium"}),mkN("obs-3","Our platform does not currently support EU data residency requirements",{kind:"observation",status:"known",confidence:"high"}),mkN("obs-4","Achieving compliance would require approximately 6 months and $500K engineering investment",{kind:"observation",status:"known",confidence:"medium"}),mkN("obs-5","Our real-time collaboration feature has no direct European equivalent and aligns with EU procurement trends",{kind:"observation",status:"provisional",confidence:"medium"}),mkN("state-1","Evaluating European market entry",{kind:"state",status:"provisional",confidence:"medium"}),mkN("u-1","Whether there is genuine demand for our category in Europe",{status:"resolved",confidence:"medium"}),mkN("u-2","Whether our product is suitable for European compliance requirements",{status:"resolved",confidence:"high"}),mkN("u-3","Whether the cost of achieving compliance is justified by the market size",{status:"resolved",confidence:"medium"}),mkN("u-4","Whether we have competitive differentiation against existing European players",{status:"resolved",confidence:"medium"})],
edges:[mkE("e-1","obs-1","state-1"),mkE("e-2","obs-2","u-1"),mkE("e-3","obs-3","u-2"),mkE("e-4","obs-4","u-3"),mkE("e-5","obs-5","u-4")], resolved:["u-1","u-2","u-3","u-4"], active:null,
question:null, noQReason:"All investigation areas resolved. A conditional recommendation can be formed.", summary:"European market entry is justified if: compliance is achieved (6 months, $500K), and the real-time collaboration feature is positioned as the differentiator against established competitors."
}
];
/* ── Scenario: Complete Investigation (full resolution) ─ */
var completeTurns = [
{
centralStatement: "A manufacturing company reports complaints increased by 35% while production increased by 40%.",
nodes: [mkN("obs-1","Complaints increased by 35%",{kind:"observation",status:"known",confidence:"high"}),mkN("obs-2","Production increased by 40%",{kind:"observation",status:"known",confidence:"high"}),mkN("state-1","Current situation",{kind:"state",status:"provisional",confidence:"medium"}),mkN("u-1","Whether the two figures cover the same period")],
edges:[mkE("e-1","obs-1","state-1"),mkE("e-2","obs-2","state-1")], resolved:[], active:"u-1",
question:{ nodeId:"u-1", question:"Were the complaint and production figures measured over the same period?", reason:"If the figures cover different periods, comparing their movement could be misleading.", reasoningPattern:"comparability_check" }, noQReason:null, summary:"Two changes have been reported, but we do not yet know whether the figures are directly comparable."
},
{
centralStatement: "A manufacturing company reports complaints increased by 35% while production increased by 40%.",
nodes: [mkN("obs-1","Complaints increased by 35%",{kind:"observation",status:"known",confidence:"high"}),mkN("obs-2","Production increased by 40%",{kind:"observation",status:"known",confidence:"high"}),mkN("obs-3","Both figures cover the same three-month period",{kind:"observation",status:"known",confidence:"high"}),mkN("state-1","Current situation",{kind:"state",status:"provisional",confidence:"medium"}),mkN("rel-1","Complaint and production trends are related",{kind:"relationship",status:"known",confidence:"medium"}),mkN("u-1","Whether the two figures cover the same period",{status:"resolved",confidence:"high"}),mkN("u-2","Whether the percentage changes use comparable baselines")],
edges:[mkE("e-1","obs-1","state-1"),mkE("e-2","obs-2","state-1"),mkE("e-3","rel-1","u-1")], resolved:["u-1"], active:"u-2",
question:{ nodeId:"u-2", question:"Were both percentages calculated from comparable baseline counts?", reason:"Establishing the reference point for both figures is essential before evaluating their relationship.", reasoningPattern:"baseline_comparability" }, noQReason:null, summary:"The timing basis is now clear."
},
{
centralStatement: "A manufacturing company reports complaints increased by 35% while production increased by 40%.",
nodes: [mkN("obs-1","Complaints increased by 35%",{kind:"observation",status:"known",confidence:"high"}),mkN("obs-2","Production increased by 40%",{kind:"observation",status:"known",confidence:"high"}),mkN("obs-3","Both figures cover the same three-month period",{kind:"observation",status:"known",confidence:"high"}),mkN("obs-4","Complaints rose from 100 to 135; production rose from 1,000 to 1,400 units",{kind:"observation",status:"known",confidence:"high"}),mkN("state-1","Current situation",{kind:"state",status:"provisional",confidence:"medium"}),mkN("rel-1","Complaint and production trends are related",{kind:"relationship",status:"known",confidence:"medium"}),mkN("u-1","Whether the two figures cover the same period",{status:"resolved",confidence:"high"}),mkN("u-2","Whether the percentage changes use comparable baselines",{status:"resolved",confidence:"medium"}),mkN("u-3","Whether complaints increased faster than production on a per-unit basis")],
edges:[mkE("e-1","obs-1","state-1"),mkE("e-2","obs-2","state-1"),mkE("e-3","rel-1","u-1"),mkE("e-4","obs-3","u-1"),mkE("e-5","rel-1","u-2")], resolved:["u-1","u-2"], active:"u-3",
question:{ nodeId:"u-3", question:"Did the complaint rate per unit produced improve or worsen?", reason:"Absolute changes in complaints and production are known; the relative rate determines whether the situation improved.", reasoningPattern:"rate_comparison" }, noQReason:null, summary:"The absolute baselines are now known."
},
{
centralStatement: "A manufacturing company reports complaints increased by 35% while production increased by 40%.",
nodes: [mkN("obs-1","Complaints increased by 35%",{kind:"observation",status:"known",confidence:"high"}),mkN("obs-2","Production increased by 40%",{kind:"observation",status:"known",confidence:"high"}),mkN("obs-3","Both figures cover the same three-month period",{kind:"observation",status:"known",confidence:"high"}),mkN("obs-4","Complaints rose from 100 to 135; production rose from 1,000 to 1,400 units",{kind:"observation",status:"known",confidence:"high"}),mkN("obs-5","The complaint rate fell from 10 per 1,000 to about 9.6 per 1,000",{kind:"observation",status:"known",confidence:"high"}),mkN("state-1","Current situation",{kind:"state",status:"provisional",confidence:"medium"}),mkN("rel-1","Complaint and production trends are related",{kind:"relationship",status:"known",confidence:"medium"}),mkN("u-1","Whether the two figures cover the same period",{status:"resolved",confidence:"high"}),mkN("u-2","Whether the percentage changes use comparable baselines",{status:"resolved",confidence:"medium"}),mkN("u-3","Whether complaints increased faster than production on a per-unit basis",{status:"resolved",confidence:"high"}),mkN("u-4","Whether reporting practices changed")],
edges:[mkE("e-1","obs-1","state-1"),mkE("e-2","obs-2","state-1"),mkE("e-3","rel-1","u-1"),mkE("e-4","obs-3","u-1"),mkE("e-5","rel-1","u-2"),mkE("e-6","obs-4","u-2"),mkE("e-7","obs-4","u-3")], resolved:["u-1","u-2","u-3"], active:"u-4",
question:{ nodeId:"u-4", question:"Was there any change in how complaints were recorded during the period?", reason:"The per-unit rate changed; we need to rule out recording artifacts before concluding a genuine shift.", reasoningPattern:"artifact_exclusion" }, noQReason:null, summary:"The per-unit complaint rate improved slightly."
},
{
centralStatement: "A manufacturing company reports complaints increased by 35% while production increased by 40%.",
nodes: [mkN("obs-1","Complaints increased by 35%",{kind:"observation",status:"known",confidence:"high"}),mkN("obs-2","Production increased by 40%",{kind:"observation",status:"known",confidence:"high"}),mkN("obs-3","Both figures cover the same three-month period",{kind:"observation",status:"known",confidence:"high"}),mkN("obs-4","Complaints rose from 100 to 135; production rose from 1,000 to 1,400 units",{kind:"observation",status:"known",confidence:"high"}),mkN("obs-5","The complaint rate fell from 10 per 1,000 to about 9.6 per 1,000",{kind:"observation",status:"known",confidence:"high"}),mkN("obs-6","Same complaint categories and reporting rules were used throughout",{kind:"observation",status:"known",confidence:"high"}),mkN("state-1","Current situation",{kind:"state",status:"provisional",confidence:"medium"}),mkN("rel-1","Complaint and production trends are related",{kind:"relationship",status:"known",confidence:"medium"}),mkN("u-1","Whether the two figures cover the same period",{status:"resolved",confidence:"high"}),mkN("u-2","Whether the percentage changes use comparable baselines",{status:"resolved",confidence:"medium"}),mkN("u-3","Whether complaints increased faster than production on a per-unit basis",{status:"resolved",confidence:"high"}),mkN("u-4","Whether reporting practices changed",{status:"resolved",confidence:"high"})],
edges:[mkE("e-1","obs-1","state-1"),mkE("e-2","obs-2","state-1"),mkE("e-3","rel-1","u-1"),mkE("e-4","obs-3","u-1"),mkE("e-5","rel-1","u-2"),mkE("e-6","obs-4","u-2"),mkE("e-7","obs-4","u-3"),mkE("e-8","obs-5","u-3"),mkE("e-9","rel-1","u-4"),mkE("e-10","obs-6","u-4")], resolved:["u-1","u-2","u-3","u-4"], active:null,
question:null, noQReason:"All required investigation areas are resolved.", summary:"The figures cover the same period, use comparable baselines, show an improved complaint rate, and were recorded consistently."
}
];
/* ── Scenario: Diagnosis (churn) ───────────────────── */
var diagnosisTurns = [
{
centralStatement: "Customer churn increased from 2% to 5% monthly over the last quarter.",
nodes: [mkN("obs-1","Churn was 2% per month in Q1",{kind:"observation",status:"known",confidence:"high"}),mkN("obs-2","Churn rose to 5% per month in Q3",{kind:"observation",status:"known",confidence:"high"}),mkN("state-1","Diagnosing the cause of increased churn",{kind:"state",status:"provisional",confidence:"medium"}),mkN("u-1","Whether the churn increase is concentrated in a specific customer segment")],
edges:[mkE("e-1","obs-1","state-1"),mkE("e-2","obs-2","state-1")], resolved:[], active:"u-1",
question:{ nodeId:"u-1", question:"Which customer segments account for the majority of the increased churn?", reason:"A blanket analysis hides which segment is driving the problem.", reasoningPattern:"diagnosis" }, noQReason:null, summary:"Churn has tripled. The first diagnostic step is to identify where it concentrates."
}
];
/* ── Registry ─────────────────────────────────────── */
var SCENARIOS = {
"default": { turns: comparisonTurns, label: "Comparison (product ratings)", centralStatement: comparisonTurns[0].centralStatement },
"comparison": { turns: comparisonTurns, label: "Comparison (product ratings)", centralStatement: comparisonTurns[0].centralStatement },
"contradictory": { turns: contradictoryTurns, label: "Contradictory evidence", centralStatement: contradictoryTurns[0].centralStatement },
"missing-evidence": { turns: missingEvidenceTurns, label: "Missing evidence", centralStatement: missingEvidenceTurns[0].centralStatement },
"evidence-limit":{ turns: evidenceLimitTurns, label: "Evidence limit (stuck early)", centralStatement: evidenceLimitTurns[0].centralStatement },
"circular": { turns: circularTurns, label: "Circular reasoning", centralStatement: circularTurns[0].centralStatement },
"decision": { turns: decisionTurns, label: "Decision (team relocation)", centralStatement: decisionTurns[0].centralStatement },
"planning": { turns: planningTurns, label: "Planning (product launch)", centralStatement: planningTurns[0].centralStatement },
"long": { turns: longTurns, label: "Long investigation (market entry)", centralStatement: longTurns[0].centralStatement },
"complete": { turns: completeTurns, label: "Complete investigation", centralStatement: completeTurns[0].centralStatement },
"diagnosis": { turns: diagnosisTurns, label: "Diagnosis (churn)", centralStatement: diagnosisTurns[0].centralStatement },
};
/* ── Build a fixture for a named scenario at a given turn index ─ */
export function buildScenarioFixture(scenarioName, turnIdx) {
var s = SCENARIOS[scenarioName];
if (!s || !s.turns) return null;
var t = s.turns[Math.min(turnIdx, s.turns.length - 1)];
return {
success: true,
situationGraph: {
centralStatement: t.centralStatement,
currentSummary: t.summary,
nodes: t.nodes,
edges: t.edges,
activeUnknownNodeId: t.active,
resolvedNodeIds: t.resolved
},
selectedQuestion: t.question || null,
noQuestionReason: t.noQReason,
newlySurfacedNodeIds: [],
diagnostics: {
promptVersion: "v0.4",
modelName: "mock-ollama",
responseDurationMs: 0,
validationStatus: "valid",
nodeCount: t.nodes.length,
edgeCount: t.edges.length,
investigationStrategy: { key: (t.question && t.question.reasoningPattern) || "default" },
unknownSelectionExplanation: t.active ? { status: "single_candidate" } : null
}
};
}
/* ── List of available scenarios for the scene selector ─ */
export var AVAILABLE_SCENARIOS = [];
for (var key in SCENARIOS) {
if (SCENARIOS.hasOwnProperty(key)) {
AVAILABLE_SCENARIOS.push({
key: key,
label: SCENARIOS[key].label,
centralStatement: SCENARIOS[key].centralStatement,
turnCount: SCENARIOS[key].turns.length
});
}
}
export default SCENARIOS;