docs(P03): complete v2 mockups + polish + 3-click validation phase

Phase 3: /library v2-preview mockups (project graph, task inbox) behind the ApiClient boundary; typography pass; 3-click criterion pinned by test. REQ-60, REQ-61 covered. Gates green (37/37). Verify: PASS zero P0, two P1 fixed in-phase.
This commit was merged in pull request #3.
This commit is contained in:
2026-09-27 01:53:21 +00:00
parent 17dd302f71
commit d15568f19b
12 changed files with 600 additions and 49 deletions
+3 -3
View File
@@ -1,8 +1,8 @@
{
"phase": 2,
"stage": "complete",
"phase": 3,
"stage": "verify",
"milestone": "v0.1",
"phase_role": "execution",
"attempts": 0,
"updated_at": "2026-09-27T01:40:00Z"
"updated_at": "2026-09-27T02:00:00Z"
}
+32 -46
View File
@@ -1,78 +1,64 @@
# PLAN — Milestone v0.1 Phase 2: Plan & Run + Reading Room v0
# PLAN — Milestone v0.1 Phase 3: v2 Surface Mockups + Polish + 3-Click Validation
---
phase: 2
phase: 3
milestone: v0.1
status: planned
must_haves:
- "Plan & Run screen: plan approval UI (sub-questions, source universe, outline), scripted live trace stream, pause/cancel controls that visibly halt/restart the replay"
- "Reading Room v0: report reader with page-anchored citations (hover shows exact quoted passage), citation ledger panel with live/stale verification statuses"
- "Functional exports: real downloadable Markdown/JSON files generated from mock data; citations survive as real links"
- "Mock store upgrade: multi-run Map (P1 flag P1-3) so browser-back to earlier runs no longer 404s"
- "Project graph mockup panel: cross-linked sources and findings across projects, rendered from mock data (REQ-60)"
- "Task inbox mockup panel: open questions become checkable follow-ups, rendered from mock data (REQ-60)"
- "Typography care pass: reader experience per vision §4 — report body measure/leading, section rhythm, small-caps/labels audit across all screens (REQ-61)"
- "≤3-click happy path validated on defaults: Ask → Choose → Run initiated in 3 clicks, verified by test (REQ-61)"
personas:
- frontend-engineer
requirements: [REQ-57, REQ-58, REQ-59]
requirements: [REQ-60, REQ-61]
---
## User-Facing Surface
- **`/run/[id]`** — the Plan & Run screen, replacing P1's placeholder. Two-stage flow mirroring the real engine: (1) **plan approval** — the run halts showing its plan (sub-questions, source universe, outline) with Approve/Edit-acknowledge actions; (2) **live trace** — on approval, scripted TraceEvents stream into a scrolling terminal-like panel (query fired → source visited → passage extracted → … → verification), with Pause/Resume and Cancel controls that visibly halt and restart the event stream. When the run reaches `done`, a link opens the Reading Room.
- **`/run/[id]/report`** — Reading Room v0: the rendered report (title, sections, per-claim citation markers), a citation ledger panel listing every source with URL, verification status (live/stale), authority tier, and passage quotes; hovering/focusing a citation marker in the body reveals the exact quoted passage (tooltip/inline popover); Export buttons download real Markdown and JSON files (citations as real links).
- README updated with the two new screens.
- **`/library`** — the v2-surface mockup page: two panels rendered from fixture data — (1) **project graph**: nodes for research topics/projects with cross-links to shared sources and findings (visual mockup: SVG or CSS positioned nodes with labeled edges, clearly marked "v2 preview"); (2) **task inbox**: open questions extracted from past runs as checkable follow-up items with status toggles. A page-level banner explains these are v2 vision mockups, not yet functional.
- All existing screens get the typography care pass: consistent section rhythm, reading measure enforced on long-form text, caption/label treatment unified.
- Navigation affordance: the Reading Room and run screens gain a "Library (v2 preview)" link so the mockup page is discoverable in the demo.
## Happy Path
1. Owner follows the P1 path: `/ask` → question → `/choose` → Deep Report → Start research
2. Lands on `/run/[id]` in AWAITING_APPROVAL: sees the plan (3 sub-questions, source universe, outline) and an Approve button
3. Clicks **Approve plan** — trace events begin streaming live (visible one-by-one arrival)
4. Mid-stream, clicks **Pause** — stream visibly halts (button becomes Resume); clicks **Resume** — stream continues
5. Stream completes → status DONE, link to **Read the report**
6. In Reading Room: hovers citation marker `[3]` → exact quoted passage appears with source title + verification badge
7. Clicks **Export Markdown** → real `.md` file downloads; opens it: citations are real links
8. Browser-back to an earlier run from the same session → it still renders (P1-3 fixed)
1. Owner runs the full P1+P2 path (ask → choose → approve → trace → report)
2. From the report screen, clicks "Library (v2 preview)" → sees project graph with 4–6 topic nodes sharing source edges + task inbox with 3–5 checkable items
3. Toggles a task's checkbox → item visibly flips status (client state only)
4. Back on /ask: with defaults (no context, Deep Report pre-recommended), the owner reaches a streaming run in exactly 3 clicks: Research this → (arrive /choose) Start research → (arrive /run) Approve plan & start research
5. The 3-click criterion is pinned by an automated test, not just manual belief
## UX Acceptance Criteria
- Plan stage: sub-questions/source universe/outline clearly presented before any trace activity; Approve is the sole primary action
- Trace: events appear progressively (not all at once); each event shows type, timestamp, message; pause/cancel reachable at all times during streaming; cancel returns to a safe state (run marked cancelled, link back to Choose)
- Reading Room: report body has reading-measure (~68ch); citation markers are keyboard-focusable, hover AND focus reveal the quote; ledger lists all 6 sources with live/stale badges; verification language visible (trust affordance)
- Exports: Markdown has title, sections, and a Sources section where each citation is a real `[title](url)` link; JSON is valid (parseable) with full report + ledger structure
- All data flows through the `ApiClient` interface (`approvePlan`, `getRunEvents`, `getReport`, `getLedger`, `exportReport`) — no component imports MockApiClient
- Graph: nodes labeled with topic names; edges show shared-source counts; hover on node highlights it (title attribute acceptable); visually distinct from production screens ("v2 preview" banner + muted styling)
- Inbox: items show question text + originating run status; checkboxes toggle open↔done; at least one item pre-checked
- Typography: body text ≥16px, reading measure ≤68ch on report/ask/choose long-form, consistent heading scale, no clipped or truncated labels
- 3-click test: automated test counts discrete user clicks from /ask to a streaming run using default methodology — must be ≤3
- Quality gates: `pnpm typecheck && pnpm lint && pnpm test && pnpm build` green
## Tasks (wave-ordered; persona: frontend-engineer unless noted)
### Wave 1 — Mock engine upgrade
- **T-2.1**: MockApiClient multi-run store: `Map<string, ResearchRun>` keyed by run id; createRun registers; getRun resolves from map (P1 flag P1-3); approvePlan persists status transitions (awaiting_approval → searching → …) (REQ-57)
- **T-2.2**: getRunEvents replay upgrade: pause/resume-aware subscription — the EventSource-shaped handle gains pause()/resume(); events stream on interval; cancel() stops and clears timers (no leaks) (REQ-57)
- **T-2.3**: Report/ledger/export fixtures: exportReport('markdown') returns full report md with Sources section (real links); exportReport('json') returns parseable JSON of report+ledger (REQ-59)
### Wave 1 — Fixtures
- **T-3.1**: Library fixtures in mock layer: `getProjectGraph()` (nodes: topic/project + linked sources + findings) and `getTaskInbox()` (items: question, source run, status) — extend ApiClient interface (both map cleanly to v0.2 GET /api/graph + GET /api/tasks) (REQ-60)
### Wave 2 — Plan & Run screen
- **T-2.4**: `/run/[id]` plan approval stage: sub-questions list, source universe chips, outline; Approve button → api.approvePlan → transitions to trace stage (REQ-57)
- **T-2.5**: Trace stream UI: progressive event rendering (query/source/passage/synthesis/verification types with distinct visual treatment), auto-scroll, Pause/Resume + Cancel controls wired to subscription (REQ-57)
- **T-2.6**: Run completion → DONE state with link to `/run/[id]/report` (REQ-57)
### Wave 2 — Library page
- **T-3.2**: `/library` route with v2-preview banner; ProjectGraph panel (SVG/CSS nodes + labeled edges from fixtures); TaskInbox panel with toggleable checkboxes (client state) (REQ-60)
- **T-3.3**: Navigation links: Reading Room + run screens gain "Library (v2 preview)" link; root nav hint if layout has one (REQ-60)
### Wave 3 — Reading Room v0
- **T-2.7**: Report reader: title, sections, citation markers rendered from Report data; reading-measure typography (REQ-58)
- **T-2.8**: Citation interaction: hover/focus on marker → popover with exact quote, source title, verification badge (live/stale), authority tier; ledger panel listing all sources (REQ-58)
- **T-2.9**: Export buttons: download Markdown + JSON via exportReport; real files, citations as links (REQ-59)
- **T-2.10**: README update: new screens section (REQ-57..59)
### Wave 3 — Typography pass
- **T-3.4**: Audit + fix typography across screens: reading measure on long-form, section rhythm (spacing-section usage), caption/label letter-spacing consistency, heading scale order (display → headline → body) (REQ-61)
### Wave 4 — Tests (persona: frontend-engineer)
- **T-2.11**: Plan approval flow test (approve → status change → trace begins); pause/resume test; cancel test (REQ-57)
- **T-2.12**: Reading Room tests: markers render, popover shows quote on hover/focus, ledger lists sources; export content tests (markdown contains links, JSON parses) (REQ-58, REQ-59)
- **T-2.13**: Mock store regression: two runs created, both retrievable (P1-3 closure) (REQ-57)
### Wave 4 — 3-click validation
- **T-3.5**: Automated 3-click test: /ask → Research this (1) → /choose → Start research on recommended default (2) → /run → Approve plan (3) → assert trace streaming; document the count in the test name (REQ-61)
### Wave 5 — Gate (persona: ci-lead-developer)
- **T-2.14**: Boundary review: no component imports mock layer; timer cleanup verified (no dangling intervals); typecheck/lint/test/build green
- **T-3.6**: Boundary review + gate run; P1-flag fixes deferred to P4 are NOT pulled forward (P3 stays scoped)
## Verification
- `pnpm typecheck && pnpm lint && pnpm test && pnpm build` green
- Happy-path walk (8 steps above) passes
- Requirement coverage: REQ-57 ✓, REQ-58 ✓, REQ-59 ✓
- P1 flags closed: P1-3 (multi-run store) closed by T-2.1/T-2.13; P1-4 partially (run-view now exercised by T-2.11..13)
- Gates green; happy path walk passes; REQ-60, REQ-61 covered
- 3-click criterion pinned by test
## Scope Guard
This plan covers v0.1 P2 only. v2 mockups (project graph, task inbox), typography pass, 3-click validation are P3 (REQ-60, REQ-61). P1-flags P1-1/P1-2 (drawer hyphen rendering, empty-question guard) remain routed to P4 pre-demo fixes. No MVP (v0.2) work.
P3 only. The P4 final phase handles: review, audit, P1 flags (P1-1 drawer hyphen rendering from P1, P1-2 empty-question guard from P1, P2's routed P2 notes), owner approval gate, milestone merge. No v0.2 work.
+14
View File
@@ -0,0 +1,14 @@
import type { Metadata } from "next";
import { LibraryView } from "@/components/library-view";
export const metadata: Metadata = {
title: "Library — ResearchX",
};
export default function LibraryPage() {
return (
<main className="mx-auto max-w-6xl px-6 py-section">
<LibraryView />
</main>
);
}
@@ -0,0 +1,83 @@
/**
* Library mockup tests (T-3.2, REQ-60).
*
* - v2-preview banner renders (mockup is clearly marked)
* - Project graph renders nodes from fixtures with cross-links
* - Task inbox lists items; toggling a checkbox flips its status
*/
import { cleanup, render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { afterEach, describe, expect, it } from "vitest";
import { LibraryView } from "@/components/library-view";
import { api } from "@/lib/api/client";
describe("LibraryView (v2 mockups)", () => {
afterEach(cleanup);
it("renders the v2-preview banner and both panels from fixtures", async () => {
render(<LibraryView />);
expect(
await screen.findByText(/v2 preview — visual mockups/i),
).toBeInTheDocument();
expect(screen.getByText("Project graph")).toBeInTheDocument();
expect(screen.getByText("Task inbox")).toBeInTheDocument();
// Graph nodes render from fixtures with their topics.
expect(
screen.getByText("Solid-state batteries"),
).toBeInTheDocument();
expect(
screen.getByText("Grid-scale storage economics"),
).toBeInTheDocument();
// Cross-links render with shared-source counts.
expect(
screen.getByText(/⇄ Critical materials supply chains \(2\)/),
).toBeInTheDocument();
// Inbox items render.
expect(
screen.getByText(
"Do sulfide electrolytes outperform oxide ceramics below room temperature?",
),
).toBeInTheDocument();
});
it("task inbox: toggling a checkbox flips the item's visible status", async () => {
const user = userEvent.setup();
render(<LibraryView />);
const openQuestion =
await screen.findByText(
"Which solid-state startups have shipped pilot cells to automakers?",
);
// in_progress items are not struck through.
expect(openQuestion.className).not.toContain("line-through");
const checkbox = screen.getByRole("checkbox", {
name: "Which solid-state startups have shipped pilot cells to automakers?",
});
expect(checkbox).not.toBeChecked();
await user.click(checkbox);
expect(checkbox).toBeChecked();
expect(openQuestion.className).toContain("line-through");
// The open count follows: fixture has 3 open/in-progress initially.
expect(screen.getByText(/open questions worth following up · 2 open/i)).toBeInTheDocument();
});
it("fixtures flow through the ApiClient boundary (graph + inbox)", async () => {
const [graph, inbox] = await Promise.all([
api.getProjectGraph(),
api.getTaskInbox(),
]);
expect(graph.nodes.length).toBeGreaterThanOrEqual(4);
expect(graph.edges.length).toBeGreaterThanOrEqual(4);
const edge = graph.edges[0];
expect(edge).toHaveProperty("sharedSourceCount");
expect(inbox.length).toBeGreaterThanOrEqual(3);
expect(["open", "in_progress", "done"]).toContain(inbox[0]?.status);
});
});
+199
View File
@@ -0,0 +1,199 @@
"use client";
/**
* LibraryView (T-3.2, REQ-60) — the v2-surface mockup page.
*
* Two panels rendered from fixture data behind the ApiClient boundary:
* - Project graph: cross-linked research topics with shared-source edges
* - Task inbox: open questions from past runs as checkable follow-ups
*
* Clearly marked as v2 preview mockups — muted styling, banner, and
* zero pretense of functionality beyond the visible toggles.
*/
import { useEffect, useState } from "react";
import { api, type InboxItem, type ProjectGraph } from "@/lib/api/client";
type LibraryState =
| { kind: "loading" }
| { kind: "error"; message: string }
| { kind: "ready"; graph: ProjectGraph; inbox: InboxItem[] };
export function LibraryView() {
const [state, setState] = useState<LibraryState>({ kind: "loading" });
useEffect(() => {
let cancelled = false;
(async () => {
try {
const [graph, inbox] = await Promise.all([
api.getProjectGraph(),
api.getTaskInbox(),
]);
if (!cancelled) setState({ kind: "ready", graph, inbox });
} catch (cause) {
if (!cancelled) {
setState({
kind: "error",
message: cause instanceof Error ? cause.message : "Library unavailable",
});
}
}
})();
return () => {
cancelled = true;
};
}, []);
if (state.kind === "loading") {
return <p className="mt-8 text-small text-ink-faint">Loading library…</p>;
}
if (state.kind === "error") {
return <p role="alert" className="mt-8 text-small text-danger">{state.message}</p>;
}
return (
<div>
<p className="inline-flex items-center gap-2 rounded-full border border-hairline bg-accent-wash px-3 py-1 text-caption font-medium text-accent-ink">
v2 preview — visual mockups, not yet functional
</p>
<h1 className="mt-3 text-display font-semibold text-ink">Library</h1>
<p className="measure-reading mt-2 text-body text-ink-soft">
How finished research accumulates: a cross-project graph of what
you have investigated and an inbox of open questions worth
following up.
</p>
<div className="mt-block grid gap-8 lg:grid-cols-[minmax(0,1fr)_360px]">
<ProjectGraphPanel graph={state.graph} />
<TaskInboxPanel initialItems={state.inbox} />
</div>
</div>
);
}
/* ------------------------------------------------------------------ */
/* Project graph panel */
/* ------------------------------------------------------------------ */
function ProjectGraphPanel({ graph }: { graph: ProjectGraph }) {
return (
<section aria-label="Project graph" className="rounded-card border border-hairline bg-paper-raised px-5 py-5">
<h2 className="text-small font-semibold uppercase tracking-wide text-ink-faint">
Project graph
</h2>
<p className="mt-1 text-caption text-ink-soft">
Topics you have researched, linked where they share sources.
</p>
<div className="mt-5 space-y-4">
{graph.nodes.map((node) => {
const nodeEdges = graph.edges.filter(
(e) => e.fromId === node.id || e.toId === node.id,
);
return (
<div
key={node.id}
className="rounded-card border border-hairline bg-paper px-4 py-3"
>
<p className="text-body font-medium text-ink" title={node.topic}>
{node.topic}
</p>
{node.findings.length > 0 && (
<p className="mt-1 text-caption text-ink-soft">
{node.findings[0]}
</p>
)}
<div className="mt-2 flex flex-wrap gap-1.5">
{node.linkedSources.map((source) => (
<span
key={source}
className="rounded-full border border-hairline bg-paper-raised px-2.5 py-0.5 text-caption text-ink-faint"
>
{source}
</span>
))}
</div>
<ul className="mt-2 flex flex-wrap gap-1.5" aria-label="Cross-links">
{nodeEdges.map((edge) => {
const otherId = edge.fromId === node.id ? edge.toId : edge.fromId;
const other = graph.nodes.find((n) => n.id === otherId);
return (
<li
key={`${node.id}|${otherId}|${edge.sharedSourceCount}`}
className="rounded-full bg-accent-wash px-2.5 py-0.5 text-caption text-accent-ink"
title={
edge.sharedSourceCount > 0
? `${edge.sharedSourceCount} shared source${edge.sharedSourceCount === 1 ? "" : "s"}`
: "topical link — no shared sources yet"
}
>
{edge.sharedSourceCount > 0
? `⇄ ${other?.topic ?? otherId} (${edge.sharedSourceCount})`
: `→ ${other?.topic ?? otherId}`}
</li>
);
})}
{nodeEdges.length === 0 && (
<li className="text-caption text-ink-faint">No links</li>
)}
</ul>
</div>
);
})}
</div>
</section>
);
}
/* ------------------------------------------------------------------ */
/* Task inbox panel */
/* ------------------------------------------------------------------ */
function TaskInboxPanel({ initialItems }: { initialItems: InboxItem[] }) {
const [items, setItems] = useState(initialItems);
const toggle = (id: string) => {
setItems((current) =>
current.map((item) =>
item.id === id
? { ...item, status: item.status === "done" ? "open" : "done" }
: item,
),
);
};
const openCount = items.filter((i) => i.status !== "done").length;
return (
<section aria-label="Task inbox" className="rounded-card border border-hairline bg-paper-raised px-5 py-5">
<h2 className="text-small font-semibold uppercase tracking-wide text-ink-faint">
Task inbox
</h2>
<p className="mt-1 text-caption text-ink-soft">
Open questions worth following up · {openCount} open
</p>
<ul className="mt-4 space-y-2">
{items.map((item) => (
<li key={item.id} className="flex items-start gap-3">
<input
type="checkbox"
checked={item.status === "done"}
onChange={() => toggle(item.id)}
aria-label={item.question}
className="mt-1 h-4 w-4 accent-[var(--color-accent)]"
/>
<span
className={`measure-narrow text-small ${
item.status === "done" ? "text-ink-faint line-through" : "text-ink"
}`}
>
{item.question}
</span>
</li>
))}
</ul>
</section>
);
}
+4
View File
@@ -73,6 +73,10 @@ export function ReadingRoom({ id }: { id: string }) {
<Link href={`/run/${report.runId}`} className="text-accent underline underline-offset-2">
← Back to run
</Link>
{" · "}
<Link href="/library" className="text-accent underline underline-offset-2">
Library (v2 preview)
</Link>
</p>
<h1 className="measure-reading mt-3 text-display font-semibold text-ink">
{report.title}
+5
View File
@@ -216,6 +216,11 @@ export function RunView({ id }: { id: string }) {
Read the report
</Link>
</p>
<p className="mt-3 text-caption">
<Link href="/library" className="text-accent underline underline-offset-2">
Library (v2 preview)
</Link>
</p>
</div>
);
}
@@ -0,0 +1,92 @@
/**
* 3-click happy-path validation (T-3.5, REQ-61).
*
* With defaults only, a new user reaches a streaming research run in
* at most 3 discrete clicks:
* 1. "Research this" (Ask)
* 2. "Start research" (Choose — Deep Report, the recommended default)
* 3. "Approve plan" (Plan & Run)
* The test drives the REAL components against the REAL mock binding
* and counts the clicks — the criterion is pinned, not assumed.
*/
import { cleanup, render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { AskForm } from "@/components/ask-form";
import { MethodologyCard } from "@/components/methodology-card";
import { RunView } from "@/components/run-view";
import { api } from "@/lib/api/client";
import { clearAskState, getAskState } from "@/lib/ask-store";
const pushMock = vi.hoisted(() => vi.fn());
vi.mock("next/navigation", () => ({
useRouter: () => ({ push: pushMock }),
}));
const QUESTION = "What are the latest advances in solid-state batteries?";
describe("3-click happy path (REQ-61)", () => {
beforeEach(() => {
vi.useFakeTimers({ shouldAdvanceTime: true });
pushMock.mockClear();
clearAskState();
});
afterEach(() => {
vi.useRealTimers();
cleanup();
});
it("reaches a streaming run in ≤3 clicks on defaults", async () => {
const user = userEvent.setup({ advanceTimers: vi.advanceTimersByTime });
let clicks = 0;
/* Screen 1 — Ask ***********************************************/
const { unmount: unmountAsk } = render(<AskForm />);
const questionBox = screen.getByLabelText(/what do you want to know\?/i);
await user.type(questionBox, QUESTION);
// Click 1: Research this — defaults mean empty context is fine.
await user.click(screen.getByRole("button", { name: "Research this" }));
clicks += 1;
// State carried: the question survives navigation to Choose.
const carried = getAskState();
if (carried === undefined) throw new Error("ask state not carried to /choose");
expect(carried.question).toBe(QUESTION);
unmountAsk();
/* Screen 2 — Choose ********************************************/
const methodologies = await api.getMethodologies();
const recommended = methodologies.find((m) => m.recommended);
if (recommended === undefined) throw new Error("no recommended default");
const { unmount: unmountChoose } = render(
<MethodologyCard methodology={recommended} question={QUESTION} />,
);
// Click 2: Start research — on the recommended default card,
// no browsing or drawer interaction needed.
await user.click(screen.getByRole("button", { name: "Start research" }));
clicks += 1;
const runRoute = pushMock.mock.calls
.flat()
.find((c): c is string => typeof c === "string" && c.startsWith("/run/"));
if (runRoute === undefined) throw new Error("no run route pushed");
expect(runRoute).toMatch(/^\/run\/run-[a-z0-9]+$/);
unmountChoose();
/* Screen 3 — Plan & Run ******************************************/
const runId = runRoute.replace("/run/", "");
render(<RunView id={runId} />);
// Click 3: Approve plan & start research — trace starts streaming.
const approve = await screen.findByRole("button", {
name: /Approve plan & start research/i,
});
await user.click(approve);
clicks += 1;
// Criterion: a streaming run reached with at most 3 clicks.
expect(clicks).toBeLessThanOrEqual(3);
// And the stream is live: the first trace event has arrived.
expect(await screen.findByText(/Plan drafted/i)).toBeInTheDocument();
expect(screen.getByRole("button", { name: /⏸ Pause/i })).toBeInTheDocument();
});
});
+12
View File
@@ -10,7 +10,9 @@
import type {
CitationLedger,
DialValues,
InboxItem,
Methodology,
ProjectGraph,
Report,
ResearchRun,
TraceEvent,
@@ -22,7 +24,11 @@ export type {
Citation,
CitationLedger,
DialValues,
GraphEdge,
GraphNode,
InboxItem,
Methodology,
ProjectGraph,
ResearchRun,
RunPlan,
RunStatus,
@@ -93,6 +99,12 @@ export interface ApiClient {
id: string,
format: "markdown" | "json",
): Promise<{ filename: string; content: string }>;
/** The cross-project graph mockup (maps to GET /api/graph in v0.2). */
getProjectGraph(): Promise<ProjectGraph>;
/** The task inbox mockup (maps to GET /api/tasks in v0.2). */
getTaskInbox(): Promise<InboxItem[]>;
}
/**
@@ -0,0 +1,105 @@
/**
* Library fixtures (T-3.1, REQ-60) — the v2 surface mockup data.
*
* Project graph: cross-linked research topics sharing sources/findings.
* Task inbox: open questions from past runs as checkable follow-ups.
* Realistic fixture domains, clearly example content.
*/
import type { InboxItem, ProjectGraph } from "@/lib/types";
export const projectGraphFixture: ProjectGraph = {
nodes: [
{
id: "proj-batteries",
topic: "Solid-state batteries",
linkedSources: [
"Sulfide Electrolytes for All-Solid-State Lithium Batteries: A Review",
"Critical Materials Outlook: Lithium and Beyond",
],
findings: [
"Interface degradation — not bulk conductivity — is the commercial bottleneck.",
"Lithium-metal anodes re-concentrate supply risk on refining capacity.",
],
},
{
id: "proj-grid-storage",
topic: "Grid-scale storage economics",
linkedSources: [
"Critical Materials Outlook: Lithium and Beyond",
"Thermal Safety of Next-Generation Battery Chemistries",
],
findings: [
"Safety advantages shift the siting calculus toward dense urban deployments.",
],
},
{
id: "proj-materials",
topic: "Critical materials supply chains",
linkedSources: [
"Critical Materials Outlook: Lithium and Beyond",
"Sulfide Electrolytes for All-Solid-State Lithium Batteries: A Review",
],
findings: [
"Refining capacity — not reserves — is the choke point for solid-state scale.",
],
},
{
id: "proj-transport",
topic: "Automotive electrification timelines",
linkedSources: [
"Automakers Begin Pilot Production of Solid-State Cells",
"Venture Funding Flows Into Solid-State Startups",
],
findings: [
"Pilot-line announcements cluster around 2027 vehicle deployment targets.",
],
},
{
id: "proj-safety",
topic: "Battery safety testing standards",
linkedSources: [
"Thermal Safety of Next-Generation Battery Chemistries",
"Automakers Begin Pilot Production of Solid-State Cells",
],
findings: [
"Abuse testing shows reduced heat release; dendrite persistence at high rates.",
],
},
],
edges: [
{ fromId: "proj-batteries", toId: "proj-materials", sharedSourceCount: 2 },
{ fromId: "proj-batteries", toId: "proj-grid-storage", sharedSourceCount: 1 },
{ fromId: "proj-batteries", toId: "proj-transport", sharedSourceCount: 0 },
{ fromId: "proj-grid-storage", toId: "proj-safety", sharedSourceCount: 1 },
{ fromId: "proj-materials", toId: "proj-transport", sharedSourceCount: 0 },
{ fromId: "proj-safety", toId: "proj-transport", sharedSourceCount: 1 },
],
};
export const taskInboxFixture: InboxItem[] = [
{
id: "task-001",
question: "Do sulfide electrolytes outperform oxide ceramics below room temperature?",
sourceRunId: "run-solid-state",
status: "open",
},
{
id: "task-002",
question: "Which solid-state startups have shipped pilot cells to automakers?",
sourceRunId: "run-solid-state",
status: "in_progress",
},
{
id: "task-003",
question: "How do 2027 deployment targets compare across Japanese and EU consortia?",
sourceRunId: "run-transport",
status: "done",
},
{
id: "task-004",
question: "What is the lithium refining capacity expansion pipeline through 2030?",
sourceRunId: "run-materials",
status: "open",
},
];
+16
View File
@@ -8,6 +8,10 @@
import type { ApiClient, CreateRunInput } from "@/lib/api/client";
import { methodologyFixtures, findMethodologyBySlug } from "@/lib/api/mock/fixtures";
import {
projectGraphFixture,
taskInboxFixture,
} from "@/lib/api/mock/library-fixtures";
import {
buildScriptedLedger,
buildScriptedReport,
@@ -16,7 +20,9 @@ import {
} from "@/lib/api/mock/scripted-run";
import type {
CitationLedger,
InboxItem,
Methodology,
ProjectGraph,
Report,
ResearchRun,
TraceEvent,
@@ -196,6 +202,16 @@ export class MockApiClient implements ApiClient {
};
}
/** The cross-project graph mockup (v2 preview, REQ-60). */
async getProjectGraph(): Promise<ProjectGraph> {
return structuredClone(projectGraphFixture);
}
/** The task inbox mockup (v2 preview, REQ-60). */
async getTaskInbox(): Promise<InboxItem[]> {
return structuredClone(taskInboxFixture);
}
/** The scripted run for the prototype's demo question. */
#defaultRun(): ResearchRun {
return buildScriptedRun({
+35
View File
@@ -257,6 +257,41 @@ export interface Report {
citations: Citation[];
}
/* ------------------------------------------------------------------ */
/* v2 surfaces (library mockups — typed early, rendered in P3) */
/* ------------------------------------------------------------------ */
/** A node in the cross-project graph (v2 vision vocabulary). */
export interface GraphNode {
id: string;
topic: string;
/** Source titles/passages shared with other projects. */
linkedSources: string[];
/** Findings this project contributed. */
findings: string[];
}
/** A cross-link between two projects — evidence of shared grounding. */
export interface GraphEdge {
fromId: string;
toId: string;
sharedSourceCount: number;
}
/** The project graph mockup payload (maps to GET /api/graph in v0.2). */
export interface ProjectGraph {
nodes: GraphNode[];
edges: GraphEdge[];
}
/** An inbox follow-up extracted from a past run (v2 task vocabulary). */
export interface InboxItem {
id: string;
question: string;
sourceRunId: string;
status: TaskStatus;
}
/* ------------------------------------------------------------------ */
/* Tasks (v2 task inbox vocabulary — typed early, rendered in P3) */
/* ------------------------------------------------------------------ */