diff --git a/components/investigation-map.jsx b/components/investigation-map.jsx
new file mode 100644
index 0000000..1360013
--- /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 (
+
+
+ {icon}
+
+ {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
+
+
+ We are building an understanding of your situation one question at a time.
+
+
+
+ {topics.map((topic, i) => (
+
+ ))}
+
+
+ );
+}
\ No newline at end of file
diff --git a/components/reasoning-workspace.jsx b/components/reasoning-workspace.jsx
index 3bde7fc..112823d 100644
--- a/components/reasoning-workspace.jsx
+++ b/components/reasoning-workspace.jsx
@@ -5,6 +5,7 @@ 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 InvestigationMap from "@/components/investigation-map";
// ── Technical summary detector (main view filters these) ───
const TECHNICAL_PATTERNS = [
@@ -733,6 +734,11 @@ export default function ReasoningWorkspace({
)}
+ {/* ── Investigation Map (visible during active investigation) ─ */}
+ {hasSelectedQuestion && (
+
+ )}
+
{investigationHistory.length > 0 && }
diff --git a/docs/reasoning-contract-backlog.md b/docs/reasoning-contract-backlog.md
index d4d0593..dad316c 100644
--- a/docs/reasoning-contract-backlog.md
+++ b/docs/reasoning-contract-backlog.md
@@ -97,6 +97,13 @@ temporary workaround and the desired eventual contract.
| 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 |
+## Investigation Map (Workspace UX)
+
+| Feature | UI need | Temporary mock | Desired reasoning output | Likely stage | Notes |
+| ---------------------- | ---------------------------------- | --------------------------------------------------- | -------------------------------------------------------------- | ---------------------- | -------------------------------------------------------- |
+| InvestigationMap | Visible investigation progress | Mock topic set with manual turn-based status progression | Engine emits `investigationTopics: [{ title, status, ordering?, evidenceCount? }]` | Each turn — start and update response | UI displays topics in engine-determined order; statuses: "established" / "current" / "unknown" |
+| InvestigationMap | Topic status evolution across turns | Hardcoded PROGRESSION array indexed by `investigationHistory.length` | Engine determines which topics are established, active, or unknown at each turn | Question selection phase | Topics should not expose graph internals; plain-language labels only |
+
## 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.
diff --git a/lib/map/investigation-map-adapter.js b/lib/map/investigation-map-adapter.js
new file mode 100644
index 0000000..89171fe
--- /dev/null
+++ b/lib/map/investigation-map-adapter.js
@@ -0,0 +1,63 @@
+/**
+ * Investigation Map Mock Adapter
+ *
+ * Provides the Investigation Map with a set of investigation topics and their
+ * current status (established / current / unknown).
+ *
+ * TODO: Replace this mock adapter when the reasoning engine emits real
+ * investigation data. The eventual contract should provide:
+ * - `investigationTopics`: [{ title, status, evidenceCount? }]
+ * - `topicStatus` values: "established" | "current" | "unknown"
+ * - `topicOrdering`: the reasoning-engine-determined sequence
+ * - `evidenceCount`: optional count of supporting evidence per topic
+ *
+ * Until then, this adapter drives a realistic mock progression across turns.
+ */
+
+/** @type {Array<{ title: string }>} */
+const TOPICS = [
+ { title: "Central situation" },
+ { title: "Complaint trend direction" },
+ { title: "Measurement basis" },
+ { title: "Production volume context" },
+ { title: "QA process changes" },
+ { title: "Product change log" },
+ { title: "Support response patterns" },
+ { title: "Prior similar cases" },
+];
+
+/**
+ * Status progression per turn index.
+ * The reasoning engine will eventually determine these values.
+ */
+const PROGRESSION = [
+ // Turn 0 — initial analysis just started
+ ["established", "unknown", "unknown", "unknown", "unknown", "unknown", "unknown", "unknown"],
+ // Turn 1 — first question answered
+ ["established", "established", "current", "unknown", "unknown", "unknown", "unknown", "unknown"],
+ // Turn 2 — second question answered
+ ["established", "established", "established", "current", "unknown", "unknown", "unknown", "unknown"],
+ // Turn 3 — third question answered
+ ["established", "established", "established", "established", "current", "unknown", "unknown", "unknown"],
+ // Turn 4 — fourth question answered
+ ["established", "established", "established", "established", "established", "current", "unknown", "unknown"],
+ // Turn 5 — fifth question answered
+ ["established", "established", "established", "established", "established", "established", "current", "unknown"],
+ // Turn 6+ — final turn
+ ["established", "established", "established", "established", "established", "established", "established", "current"],
+];
+
+/**
+ * Get investigation map topics for a given turn index.
+ * @param {number} turnIndex - Zero-based turn number (0 = initial analysis).
+ * @returns {{ title: string, status: 'established' | 'current' | 'unknown' }[]}
+ */
+export function getInvestigationMapTopics(turnIndex) {
+ const idx = Math.min(turnIndex, PROGRESSION.length - 1);
+ return TOPICS.map((topic, i) => ({
+ title: topic.title,
+ status: PROGRESSION[idx][i],
+ }));
+}
+
+export default getInvestigationMapTopics;
\ No newline at end of file
diff --git a/tests/e2e/happy-path.spec.js b/tests/e2e/happy-path.spec.js
index a49f457..fb0cbd7 100644
--- a/tests/e2e/happy-path.spec.js
+++ b/tests/e2e/happy-path.spec.js
@@ -367,4 +367,75 @@ test("update submission replaces response panel with loading card, preserves con
await expect(rwTextarea).toBeVisible();
await expect(overlay).not.toBeVisible({ timeout: 3_000 });
}
+});
+
+/* ═══════ Test: investigation map appears and evolves across turns ═ */
+
+test("investigation map: appears after start, topics evolve across mocked turns", async ({ page }) => {
+ await page.goto("/");
+
+ await page.evaluate(() => {
+ window.__MOCK_ENABLED = true;
+ window.__MOCK_DELAY = "instant";
+ window.__MOCK_SCENARIO = "complete";
+ });
+
+ const textarea = page.locator("textarea[placeholder*=Describe]");
+ await textarea.fill(INVESTIGATION_SCENARIOS[0].centralStatement);
+ await page.getByRole("button", { name: "Analyse" }).click();
+
+ await waitForLoading(page);
+ await waitForWorkspaceReady(page);
+
+ // Verify the investigation map card is present after start
+ const mapCard = page.locator('[aria-label="Investigation map"]');
+ await expect(mapCard).toBeVisible();
+
+ // Map heading visible
+ await expect(mapCard.getByRole("heading", { name: "Investigation Map" })).toBeVisible();
+
+ // Helper text visible
+ await expect(mapCard.getByText(/building an understanding/i)).toBeVisible();
+
+ // At turn 0 (initial analysis): one topic established, one current, rest unknown
+ const establishedTopics = page.locator('[data-testid="map-topic-established"]');
+ const currentTopics = page.locator('[data-testid="map-topic-current"]');
+ const unknownTopics = page.locator('[data-testid="map-topic-unknown"]');
+
+ await expect(establishedTopics).toHaveCount(1);
+ await expect(currentTopics).toHaveCount(1);
+ await expect(unknownTopics).toHaveCount(6);
+
+ // Submit first answer → turn 1: one more established, next topic becomes current
+ const answerTextarea = page.locator('textarea[placeholder*=Answer]');
+ if (await answerTextarea.isVisible({ timeout: 3000 })) {
+ await answerTextarea.fill("The figures are comparable.");
+ await page.getByRole("button", { name: "Update situation" }).click();
+
+ await waitForLoading(page);
+ await waitForWorkspaceReady(page);
+
+ // Current investigation remains visible (context preserved)
+ await expect(page.getByRole("heading", { name: "Current investigation" })).toBeVisible({ timeout: 5_000 });
+
+ // Map should update: 2 established, 1 current, 5 unknown
+ await expect(establishedTopics).toHaveCount(2);
+ await expect(currentTopics).toHaveCount(1);
+ await expect(unknownTopics).toHaveCount(5);
+
+ // Submit second answer → turn 2
+ const answerTextarea2 = page.locator('textarea[placeholder*=Answer]');
+ if (await answerTextarea2.isVisible({ timeout: 3000 })) {
+ await answerTextarea2.fill("Complaint rate fell from 2.0 to 1.9 per 100 units.");
+ await page.getByRole("button", { name: "Update situation" }).click();
+
+ await waitForLoading(page);
+ await waitForWorkspaceReady(page);
+
+ // After second update: 3 established, current shifts again
+ await expect(establishedTopics).toHaveCount(3);
+ await expect(currentTopics).toHaveCount(1);
+ await expect(unknownTopics).toHaveCount(4);
+ }
+ }
});
\ No newline at end of file