diff --git a/.ciagent/CHECKPOINT.json b/.ciagent/CHECKPOINT.json index 8d7c225..131d37b 100644 --- a/.ciagent/CHECKPOINT.json +++ b/.ciagent/CHECKPOINT.json @@ -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" } \ No newline at end of file diff --git a/.ciagent/PLAN.md b/.ciagent/PLAN.md index 4b3d918..3b9c693 100644 --- a/.ciagent/PLAN.md +++ b/.ciagent/PLAN.md @@ -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` 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. \ No newline at end of file +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. \ No newline at end of file diff --git a/frontend/src/app/library/page.tsx b/frontend/src/app/library/page.tsx new file mode 100644 index 0000000..10cc8f5 --- /dev/null +++ b/frontend/src/app/library/page.tsx @@ -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 ( +
+ +
+ ); +} \ No newline at end of file diff --git a/frontend/src/components/library-view.test.tsx b/frontend/src/components/library-view.test.tsx new file mode 100644 index 0000000..6506c25 --- /dev/null +++ b/frontend/src/components/library-view.test.tsx @@ -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(); + + 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(); + + 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); + }); +}); \ No newline at end of file diff --git a/frontend/src/components/library-view.tsx b/frontend/src/components/library-view.tsx new file mode 100644 index 0000000..f8a6bcf --- /dev/null +++ b/frontend/src/components/library-view.tsx @@ -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({ 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

Loading library…

; + } + if (state.kind === "error") { + return

{state.message}

; + } + + return ( +
+

+ v2 preview — visual mockups, not yet functional +

+

Library

+

+ How finished research accumulates: a cross-project graph of what + you have investigated and an inbox of open questions worth + following up. +

+ +
+ + +
+
+ ); +} + +/* ------------------------------------------------------------------ */ +/* Project graph panel */ +/* ------------------------------------------------------------------ */ + +function ProjectGraphPanel({ graph }: { graph: ProjectGraph }) { + return ( +
+

+ Project graph +

+

+ Topics you have researched, linked where they share sources. +

+ +
+ {graph.nodes.map((node) => { + const nodeEdges = graph.edges.filter( + (e) => e.fromId === node.id || e.toId === node.id, + ); + return ( +
+

+ {node.topic} +

+ {node.findings.length > 0 && ( +

+ {node.findings[0]} +

+ )} +
+ {node.linkedSources.map((source) => ( + + {source} + + ))} +
+
    + {nodeEdges.map((edge) => { + const otherId = edge.fromId === node.id ? edge.toId : edge.fromId; + const other = graph.nodes.find((n) => n.id === otherId); + return ( +
  • 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}`} +
  • + ); + })} + {nodeEdges.length === 0 && ( +
  • No links
  • + )} +
+
+ ); + })} +
+
+ ); +} + +/* ------------------------------------------------------------------ */ +/* 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 ( +
+

+ Task inbox +

+

+ Open questions worth following up · {openCount} open +

+ +
    + {items.map((item) => ( +
  • + toggle(item.id)} + aria-label={item.question} + className="mt-1 h-4 w-4 accent-[var(--color-accent)]" + /> + + {item.question} + +
  • + ))} +
+
+ ); +} \ No newline at end of file diff --git a/frontend/src/components/reading-room.tsx b/frontend/src/components/reading-room.tsx index a1d02c9..2eae7d7 100644 --- a/frontend/src/components/reading-room.tsx +++ b/frontend/src/components/reading-room.tsx @@ -73,6 +73,10 @@ export function ReadingRoom({ id }: { id: string }) { ← Back to run + {" · "} + + Library (v2 preview) +

{report.title} diff --git a/frontend/src/components/run-view.tsx b/frontend/src/components/run-view.tsx index 91a07c3..a805224 100644 --- a/frontend/src/components/run-view.tsx +++ b/frontend/src/components/run-view.tsx @@ -216,6 +216,11 @@ export function RunView({ id }: { id: string }) { Read the report

+

+ + Library (v2 preview) + +

); } diff --git a/frontend/src/components/three-click-path.test.tsx b/frontend/src/components/three-click-path.test.tsx new file mode 100644 index 0000000..a6de76e --- /dev/null +++ b/frontend/src/components/three-click-path.test.tsx @@ -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(); + 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( + , + ); + // 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(); + // 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(); + }); +}); \ No newline at end of file diff --git a/frontend/src/lib/api/client.ts b/frontend/src/lib/api/client.ts index 816dfaf..cf36826 100644 --- a/frontend/src/lib/api/client.ts +++ b/frontend/src/lib/api/client.ts @@ -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; + + /** The task inbox mockup (maps to GET /api/tasks in v0.2). */ + getTaskInbox(): Promise; } /** diff --git a/frontend/src/lib/api/mock/library-fixtures.ts b/frontend/src/lib/api/mock/library-fixtures.ts new file mode 100644 index 0000000..4e1d39d --- /dev/null +++ b/frontend/src/lib/api/mock/library-fixtures.ts @@ -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", + }, +]; \ No newline at end of file diff --git a/frontend/src/lib/api/mock/mock-client.ts b/frontend/src/lib/api/mock/mock-client.ts index 139aca2..ba3cca4 100644 --- a/frontend/src/lib/api/mock/mock-client.ts +++ b/frontend/src/lib/api/mock/mock-client.ts @@ -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 { + return structuredClone(projectGraphFixture); + } + + /** The task inbox mockup (v2 preview, REQ-60). */ + async getTaskInbox(): Promise { + return structuredClone(taskInboxFixture); + } + /** The scripted run for the prototype's demo question. */ #defaultRun(): ResearchRun { return buildScriptedRun({ diff --git a/frontend/src/lib/types.ts b/frontend/src/lib/types.ts index 5372e5b..4c9195c 100644 --- a/frontend/src/lib/types.ts +++ b/frontend/src/lib/types.ts @@ -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) */ /* ------------------------------------------------------------------ */