feat(FN-2991): merge fusion/fn-2991

Commits merged:
- fix(FN-2991): align failed research status badge semantics
- feat(FN-2991): add research dashboard view and navigation entry
- fix(FN-2991): complete Step 8 — resolve lint and typecheck gates
- test(FN-2991): address review feedback for Step 7
- test(FN-2991): complete Step 7 — add research store and route tests
- feat(FN-2991): complete Step 6 — add research client API helpers
- feat(FN-2991): complete Step 5 — add research REST routes
- feat(FN-2991): complete Step 4 — wire research store into core exports
- feat(FN-2991): complete Step 3 — implement research store
- feat(FN-2991): complete Step 2 — add research schema and migration
- feat(FN-2991): complete Step 1 — add research domain types

Files changed:
packages/core/src/__tests__/db.test.ts             |  26 +-
 packages/core/src/__tests__/insight-store.test.ts  |   8 +-
 packages/core/src/__tests__/mission-store.test.ts  |   2 +-
 packages/core/src/__tests__/research-store.test.ts | 118 +++++++
 packages/core/src/__tests__/roadmap-store.test.ts  |   2 +-
 packages/core/src/__tests__/run-audit.test.ts      |   2 +-
 packages/core/src/__tests__/task-documents.test.ts |   2 +-
 packages/core/src/db.ts                            |  82 ++++-
 packages/core/src/index.ts                         |  30 ++
 packages/core/src/research-store.ts                | 377 +++++++++++++++++++++
 packages/core/src/research-types.ts                | 162 +++++++++
 packages/core/src/store.ts                         |  14 +
 packages/dashboard/app/App.tsx                     |  12 +
 .../app/api/__tests__/research-api.test.ts         | 120 +++++++
 packages/dashboard/app/api/legacy.ts               | 132 +++++++-
 packages/dashboard/app/components/Header.tsx       |  23 +-
 packages/dashboard/app/components/MobileNavBar.tsx |  16 +-
 packages/dashboard/app/components/ResearchView.css | 110 ++++++
 packages/dashboard/app/components/ResearchView.tsx | 145 ++++++++
 .../app/components/__tests__/Header.test.tsx       |   4 +-
 .../app/components/__tests__/ResearchView.test.tsx | 170 ++++++++++
 packages/dashboard/app/hooks/useViewState.ts       |   3 +-
 packages/dashboard/src/research-routes.ts          | 223 ++++++++++++
 .../src/routes/register-integrated-routers.ts      |   2 +
 24 files changed, 1751 insertions(+), 34 deletions(-)

Fusion-Task-Id: FN-2991
This commit is contained in:
Fusion
2026-04-29 22:21:27 -07:00
committed by gsxdsm
parent b78dd3e055
commit c5de7e183b
24 changed files with 1751 additions and 34 deletions

View File

@@ -131,7 +131,7 @@ describe("Database", () => {
});
it("seeds schema version", () => {
expect(db.getSchemaVersion()).toBe(54);
expect(db.getSchemaVersion()).toBe(55);
});
it("seeds lastModified", () => {
@@ -154,7 +154,7 @@ describe("Database", () => {
it("is idempotent - calling init() twice does not fail", () => {
expect(() => db.init()).not.toThrow();
expect(db.getSchemaVersion()).toBe(54);
expect(db.getSchemaVersion()).toBe(55);
});
it("does not overwrite existing config on re-init", () => {
@@ -761,7 +761,7 @@ describe("schema migrations", () => {
db.init();
// Verify version bumped to 29 (includes v1→v2 through v26→v29)
expect(db.getSchemaVersion()).toBe(54);
expect(db.getSchemaVersion()).toBe(55);
// Verify new columns exist and existing data is intact
const cols = db.prepare("PRAGMA table_info(tasks)").all() as Array<{ name: string }>;
@@ -786,11 +786,11 @@ describe("schema migrations", () => {
const db = new Database(fusionDir);
db.init();
expect(db.getSchemaVersion()).toBe(54);
expect(db.getSchemaVersion()).toBe(55);
// Re-init should not fail
db.init();
expect(db.getSchemaVersion()).toBe(54);
expect(db.getSchemaVersion()).toBe(55);
db.close();
});
@@ -825,7 +825,7 @@ describe("schema migrations", () => {
db.init();
expect(db.getSchemaVersion()).toBe(54);
expect(db.getSchemaVersion()).toBe(55);
const cols = db.prepare("PRAGMA table_info(tasks)").all() as Array<{ name: string }>;
expect(cols.map((col) => col.name)).toContain("priority");
@@ -866,7 +866,7 @@ describe("schema migrations", () => {
db.init();
expect(db.getSchemaVersion()).toBe(54);
expect(db.getSchemaVersion()).toBe(55);
const cols = db.prepare("PRAGMA table_info(tasks)").all() as Array<{ name: string }>;
const colNames = cols.map((col) => col.name);
@@ -935,7 +935,7 @@ describe("schema migrations", () => {
db.init();
expect(db.getSchemaVersion()).toBe(54);
expect(db.getSchemaVersion()).toBe(55);
const cols = db.prepare("PRAGMA table_info(tasks)").all() as Array<{ name: string }>;
const colNames = cols.map((col) => col.name);
@@ -994,7 +994,7 @@ describe("schema migrations", () => {
db.init();
expect(db.getSchemaVersion()).toBe(54);
expect(db.getSchemaVersion()).toBe(55);
const cols = db.prepare("PRAGMA table_info(chat_messages)").all() as Array<{ name: string }>;
expect(cols.map((col) => col.name)).toContain("attachments");
@@ -1068,7 +1068,7 @@ describe("schema migrations", () => {
db.init();
expect(db.getSchemaVersion()).toBe(54);
expect(db.getSchemaVersion()).toBe(55);
const tables = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name = 'agentRatings'").all() as Array<{ name: string }>;
expect(tables).toEqual([{ name: "agentRatings" }]);
@@ -1092,7 +1092,7 @@ describe("schema migrations", () => {
db.init();
expect(db.getSchemaVersion()).toBe(54);
expect(db.getSchemaVersion()).toBe(55);
const tables = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name = 'mission_events'").all() as Array<{ name: string }>;
expect(tables).toEqual([{ name: "mission_events" }]);
@@ -1196,7 +1196,7 @@ describe("schema migrations", () => {
db.init();
// Verify version bumped to 29
expect(db.getSchemaVersion()).toBe(54);
expect(db.getSchemaVersion()).toBe(55);
// Verify new columns exist and existing data is intact
const cols = db.prepare("PRAGMA table_info(tasks)").all() as Array<{ name: string }>;
@@ -1647,7 +1647,7 @@ describe("createDatabase factory", () => {
const db = createDatabase(fusionDir);
db.init();
expect(db.getSchemaVersion()).toBe(54);
expect(db.getSchemaVersion()).toBe(55);
expect(db.getLastModified()).toBeGreaterThan(0);
db.close();

View File

@@ -779,7 +779,7 @@ describe("Migration: pre-33 DB upgrade", () => {
// Step 1: Create a fresh database at v33 (runs all migrations up to 33)
const db1 = createDatabase(legacyDir);
db1.init();
expect(db1.getSchemaVersion()).toBe(54);
expect(db1.getSchemaVersion()).toBe(55);
db1.close();
// Step 2: Manually downgrade to version 32 and drop insight tables
@@ -814,7 +814,7 @@ describe("Migration: pre-33 DB upgrade", () => {
expect(tableNamesBefore).not.toContain("project_insight_runs");
// Now run init — this triggers the v32→v33 migration
db3.init();
expect(db3.getSchemaVersion()).toBe(54);
expect(db3.getSchemaVersion()).toBe(55);
// Step 4: Verify insight tables exist after migration
const tablesAfter = db3.prepare(
@@ -845,12 +845,12 @@ describe("Migration: pre-33 DB upgrade", () => {
try {
const db1 = createDatabase(testDir);
db1.init();
expect(db1.getSchemaVersion()).toBe(54);
expect(db1.getSchemaVersion()).toBe(55);
db1.close();
const db2 = createDatabase(testDir);
expect(() => db2.init()).not.toThrow();
expect(db2.getSchemaVersion()).toBe(54);
expect(db2.getSchemaVersion()).toBe(55);
db2.close();
} finally {
rmSync(testDir, { recursive: true, force: true });

View File

@@ -2629,7 +2629,7 @@ describe("MissionStore", () => {
describe("Loop State & Validator Run Schema (v31)", () => {
it("schema version is 40 after migration", () => {
expect(db.getSchemaVersion()).toBe(54);
expect(db.getSchemaVersion()).toBe(55);
});
it("mission_features table has loop state columns", () => {

View File

@@ -0,0 +1,118 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import { mkdtempSync } from "node:fs";
import { join } from "node:path";
import { tmpdir } from "node:os";
import { createDatabase, type Database } from "../db.js";
import { ResearchStore } from "../research-store.js";
describe("ResearchStore", () => {
let db: Database;
let store: ResearchStore;
beforeEach(() => {
const fusionDir = mkdtempSync(join(tmpdir(), "fn-research-test-"));
db = createDatabase(fusionDir, { inMemory: true });
db.init();
store = new ResearchStore(db);
});
it("creates, gets, updates, lists and deletes runs", () => {
const run = store.createRun({ query: "test topic", tags: ["a"] });
expect(run.id).toMatch(/^RR-/);
expect(store.getRun(run.id)?.query).toBe("test topic");
const updated = store.updateRun(run.id, { topic: "new topic", error: "oops" });
expect(updated?.topic).toBe("new topic");
const listed = store.listRuns({ status: "pending" });
expect(listed.map((r) => r.id)).toContain(run.id);
expect(store.deleteRun(run.id)).toBe(true);
expect(store.getRun(run.id)).toBeUndefined();
expect(store.deleteRun("RR-missing")).toBe(false);
});
it("handles status transitions with lifecycle timestamps", () => {
const run = store.createRun({ query: "status test" });
store.updateStatus(run.id, "running");
const running = store.getRun(run.id)!;
expect(running.startedAt).toBeTruthy();
store.updateStatus(run.id, "completed");
const completed = store.getRun(run.id)!;
expect(completed.completedAt).toBeTruthy();
const failed = store.createRun({ query: "failure" });
store.updateStatus(failed.id, "failed");
expect(store.getRun(failed.id)?.completedAt).toBeTruthy();
const cancelled = store.createRun({ query: "cancel" });
store.updateStatus(cancelled.id, "cancelled");
expect(store.getRun(cancelled.id)?.cancelledAt).toBeTruthy();
});
it("appends events, manages sources, and sets results", () => {
const run = store.createRun({ query: "events" });
const event = store.appendEvent(run.id, { type: "info", message: "started" });
expect(event.id).toMatch(/^REVT-/);
const source = store.addSource(run.id, {
type: "web",
reference: "https://example.com",
status: "pending",
});
store.updateSource(run.id, source.id, { status: "completed", title: "Example" });
store.setResults(run.id, {
summary: "Done",
findings: [{ heading: "H1", content: "C1", sources: [source.id], confidence: 0.8 }],
});
const next = store.getRun(run.id)!;
expect(next.events).toHaveLength(1);
expect(next.sources[0].status).toBe("completed");
expect(next.results?.summary).toBe("Done");
});
it("supports filtering, search, ordering, exports and stats", () => {
const r1 = store.createRun({ query: "alpha", topic: "first", tags: ["core"] });
const r2 = store.createRun({ query: "beta", topic: "second", tags: ["edge"] });
store.setResults(r2.id, { summary: "beta summary", findings: [] });
expect(store.listRuns({ tag: "core" }).map((r) => r.id)).toEqual([r1.id]);
expect(store.searchRuns("beta").map((r) => r.id)).toContain(r2.id);
const all = store.listRuns();
expect(all[0].createdAt <= all[1].createdAt).toBe(true);
const ex = store.createExport(r1.id, "json", "{}");
expect(store.getExports(r1.id)).toHaveLength(1);
expect(store.getExport(ex.id)?.runId).toBe(r1.id);
expect(store.getExport("REXP-missing")).toBeUndefined();
store.updateStatus(r1.id, "running");
store.updateStatus(r2.id, "completed");
const stats = store.getStats();
expect(stats.total).toBeGreaterThanOrEqual(2);
expect(stats.byStatus.completed).toBeGreaterThanOrEqual(1);
store.deleteRun(r1.id);
expect(store.getExports(r1.id)).toHaveLength(0);
});
it("emits status events and throws for missing run mutations", () => {
const onStatus = vi.fn();
const onCompleted = vi.fn();
store.on("run:status_changed", onStatus);
store.on("run:completed", onCompleted);
const run = store.createRun({ query: "events" });
store.updateStatus(run.id, "completed");
expect(onStatus).toHaveBeenCalled();
expect(onCompleted).toHaveBeenCalled();
expect(() => store.appendEvent("missing", { type: "info", message: "x" })).toThrow(/not found/i);
expect(() => store.addSource("missing", { type: "web", reference: "x", status: "pending" })).toThrow(/not found/i);
expect(() => store.setResults("missing", { findings: [] })).toThrow(/not found/i);
});
});

View File

@@ -742,7 +742,7 @@ describe("RoadmapStore", () => {
describe("schema version", () => {
it("schema version is 40 after init", () => {
expect(db.getSchemaVersion()).toBe(54);
expect(db.getSchemaVersion()).toBe(55);
});
});

View File

@@ -465,7 +465,7 @@ describe("Run Audit", () => {
});
it("schema version is bumped to 40", () => {
expect(db.getSchemaVersion()).toBe(54);
expect(db.getSchemaVersion()).toBe(55);
});
});
});

View File

@@ -51,7 +51,7 @@ describe("TaskStore task documents", () => {
expect(tableNames.has("task_documents")).toBe(true);
expect(tableNames.has("task_document_revisions")).toBe(true);
expect(db.getSchemaVersion()).toBe(54);
expect(db.getSchemaVersion()).toBe(55);
const index = db
.prepare(