feat(FN-3637): establish canonical roadmap plugin ID and compatibility rout

Merged commits stabilize the Fusion roadmap plugin's identity and routing surface, establishing a canonical plugin ID and compatibility routes so the roadmap plugin integrates cleanly with the dashboard's plugin system. Added new roadmap-routes and roadmap-suggestions modules in the dashboard packag

Fusion-Task-Id: FN-3637
This commit is contained in:
Fusion
2026-05-11 06:04:17 -07:00
committed by gsxdsm
parent 119ba88a3f
commit d910c0c0f3
33 changed files with 219 additions and 81 deletions

View File

@@ -0,0 +1,5 @@
---
"@runfusion/fusion": patch
---
Align the bundled roadmap plugin to the canonical `fusion-plugin-roadmap` runtime id, expose roadmap APIs under `/api/plugins/fusion-plugin-roadmap/...`, and restore `/api/roadmaps` compatibility routing through plugin-owned handlers during migration.

View File

@@ -573,8 +573,8 @@ Route handlers may return either plain JSON values or a `PluginRouteResponse` en
Routes are mounted at `/api/plugins/{pluginId}/{path}`. Routes are mounted at `/api/plugins/{pluginId}/{path}`.
Route handlers receive the same loader-built `PluginContext` used by hooks/tools, including real `taskStore`, plugin `settings`, `logger`, `emitEvent`, and engine-injected `createAiSession` (when available): Route handlers receive the same loader-built `PluginContext` used by hooks/tools, including real `taskStore`, plugin `settings`, `logger`, `emitEvent`, and engine-injected `createAiSession` (when available):
- Example roadmap plugin route: `path: "/roadmaps"` in plugin `roadmap-planner` resolves to `/api/plugins/roadmap-planner/roadmaps` - Example roadmap plugin route: `path: "/roadmaps"` in plugin `fusion-plugin-roadmap` resolves to `/api/plugins/fusion-plugin-roadmap/roadmaps`
- Roadmap suggestion endpoints follow the same namespace (for example `/api/plugins/roadmap-planner/roadmaps/:roadmapId/suggestions/milestones`) - Roadmap suggestion endpoints follow the same namespace (for example `/api/plugins/fusion-plugin-roadmap/roadmaps/:roadmapId/suggestions/milestones`)
- Do not document or depend on legacy host-owned `/api/roadmaps` routes unless your current source still ships them - Do not document or depend on legacy host-owned `/api/roadmaps` routes unless your current source still ships them
- Plugin ID: `fusion-plugin-notification` - Plugin ID: `fusion-plugin-notification`
@@ -1264,7 +1264,7 @@ Polls CI status for branches and provides custom API endpoints.
Standalone roadmap planning plugin extracted from dashboard host code. Standalone roadmap planning plugin extracted from dashboard host code.
- Demonstrates: `hooks.onSchemaInit` for plugin-owned schema DDL (`ensureRoadmapSchema`) - Demonstrates: `hooks.onSchemaInit` for plugin-owned schema DDL (`ensureRoadmapSchema`)
- Demonstrates: plugin-scoped route namespace under `/api/plugins/roadmap-planner/*` - Demonstrates: plugin-scoped route namespace under `/api/plugins/fusion-plugin-roadmap/*`
- Demonstrates: top-level navigation registration through `dashboardViews` (`viewId: "roadmaps"`) and host static view registration - Demonstrates: top-level navigation registration through `dashboardViews` (`viewId: "roadmaps"`) and host static view registration
- Demonstrates: AI suggestion flows that consume `ctx.createAiSession` through plugin route handlers - Demonstrates: AI suggestion flows that consume `ctx.createAiSession` through plugin route handlers

View File

@@ -177,7 +177,7 @@ Concrete references:
- Core schema tables include: `tasks`, `config`, `workflow_steps`, `activityLog`, `archivedTasks`, `automations`, `agents`, `agentHeartbeats`, approval tables (`approval_requests`, `approval_request_audit_events`), `task_documents`, `task_document_revisions`, mission hierarchy tables (`missions`, `milestones`, `slices`, `mission_features`, `mission_events`), plugin/routine tables (`plugins`, `routines`), roadmap tables (`roadmaps`, `roadmap_milestones`, `roadmap_features`), insight tables (`project_insights`, `project_insight_runs`), research tables (`research_runs`, `research_exports`, `research_run_events`), eval tables (`eval_runs`, `eval_task_results`, `eval_run_events`), todo tables (`todo_lists`, `todo_items`), `__meta` - Core schema tables include: `tasks`, `config`, `workflow_steps`, `activityLog`, `archivedTasks`, `automations`, `agents`, `agentHeartbeats`, approval tables (`approval_requests`, `approval_request_audit_events`), `task_documents`, `task_document_revisions`, mission hierarchy tables (`missions`, `milestones`, `slices`, `mission_features`, `mission_events`), plugin/routine tables (`plugins`, `routines`), roadmap tables (`roadmaps`, `roadmap_milestones`, `roadmap_features`), insight tables (`project_insights`, `project_insight_runs`), research tables (`research_runs`, `research_exports`, `research_run_events`), eval tables (`eval_runs`, `eval_task_results`, `eval_run_events`), todo tables (`todo_lists`, `todo_items`), `__meta`
- Migration-created tables include: `ai_sessions`, `messages`, `agentRatings`, `chat_sessions`, `chat_messages`, `runAuditEvents`, `mission_contract_assertions`, `mission_feature_assertions`, `mission_validator_runs`, `mission_validator_failures`, `mission_fix_feature_lineage` - Migration-created tables include: `ai_sessions`, `messages`, `agentRatings`, `chat_sessions`, `chat_messages`, `runAuditEvents`, `mission_contract_assertions`, `mission_feature_assertions`, `mission_validator_runs`, `mission_validator_failures`, `mission_fix_feature_lineage`
- `ai_sessions.status` lifecycle includes `draft` (pre-start planning session), then `generating`, `awaiting_input`, terminal `complete` / `error` - `ai_sessions.status` lifecycle includes `draft` (pre-start planning session), then `generating`, `awaiting_input`, terminal `complete` / `error`
- **Roadmap feature ownership**: roadmap contracts, ordering/handoff helpers, persistence, routes, and dashboard UI live in `plugins/fusion-plugin-roadmap` (package `@fusion-plugin-examples/roadmap`) rather than dashboard/core ownership. - **Roadmap feature ownership**: roadmap contracts, ordering/handoff helpers, persistence, routes, and dashboard UI live in `plugins/fusion-plugin-roadmap` (package `@fusion-plugin-examples/roadmap`, plugin id `fusion-plugin-roadmap`) rather than dashboard/core ownership.
- **CentralCore**: `packages/core/src/central-core.ts` - **CentralCore**: `packages/core/src/central-core.ts`
- Global project registry, health, central activity feed, global concurrency - Global project registry, health, central activity feed, global concurrency
- Backed by `packages/core/src/central-db.ts` (`~/.fusion/fusion-central.db`) - Backed by `packages/core/src/central-db.ts` (`~/.fusion/fusion-central.db`)
@@ -425,12 +425,12 @@ Key roadmap invariants:
- cross-milestone feature moves must renumber both the source and destination milestone deterministically - cross-milestone feature moves must renumber both the source and destination milestone deterministically
**Roadmap frontend API contract (plugin namespace):** **Roadmap frontend API contract (plugin namespace):**
- Canonical frontend namespace: `/api/plugins/roadmap-planner/roadmaps` - Canonical frontend namespace: `/api/plugins/fusion-plugin-roadmap/roadmaps`
- Roadmaps: `GET /`, `POST /`, `GET /:roadmapId`, `PATCH /:roadmapId`, `DELETE /:roadmapId` - Roadmaps: `GET /`, `POST /`, `GET /:roadmapId`, `PATCH /:roadmapId`, `DELETE /:roadmapId`
- Milestones: `GET /:roadmapId/milestones`, `POST /:roadmapId/milestones`, `PATCH /milestones/:milestoneId`, `DELETE /milestones/:milestoneId`, `POST /:roadmapId/milestones/reorder` - Milestones: `GET /:roadmapId/milestones`, `POST /:roadmapId/milestones`, `PATCH /milestones/:milestoneId`, `DELETE /milestones/:milestoneId`, `POST /:roadmapId/milestones/reorder`
- Features: `GET /milestones/:milestoneId/features`, `POST /milestones/:milestoneId/features`, `PATCH /features/:featureId`, `DELETE /features/:featureId`, `POST /milestones/:milestoneId/features/reorder`, `POST /features/:featureId/move` - Features: `GET /milestones/:milestoneId/features`, `POST /milestones/:milestoneId/features`, `PATCH /features/:featureId`, `DELETE /features/:featureId`, `POST /milestones/:milestoneId/features/reorder`, `POST /features/:featureId/move`
- Export/Handoff: `GET /:roadmapId/export`, `GET /:roadmapId/handoff`, `GET /:roadmapId/handoff/mission`, `GET /:roadmapId/milestones/:milestoneId/features/:featureId/handoff/task` - Export/Handoff: `GET /:roadmapId/export`, `GET /:roadmapId/handoff`, `GET /:roadmapId/handoff/mission`, `GET /:roadmapId/milestones/:milestoneId/features/:featureId/handoff/task`
- Dashboard host no longer mounts legacy `/api/roadmaps`; roadmap REST traffic goes through the plugin namespace only. - Canonical roadmap REST namespace is plugin-scoped (`/api/plugins/fusion-plugin-roadmap/...`), while dashboard maintains a temporary `/api/roadmaps` compatibility mount that delegates to plugin-owned handlers during migration.
**Database schema:** **Database schema:**
- `roadmaps` — roadmap metadata (id, title, description, timestamps) - `roadmaps` — roadmap metadata (id, title, description, timestamps)
@@ -738,7 +738,7 @@ Key server capabilities:
- Insights routes (`insights-routes.ts`) - Insights routes (`insights-routes.ts`)
- Evals routes (`evals-routes.ts`) — `/api/evals` read surface for eval result listing/filtering, drill-down detail, and eval run metadata - Evals routes (`evals-routes.ts`) — `/api/evals` read surface for eval result listing/filtering, drill-down detail, and eval run metadata
- Research routes (`research-routes.ts`) — `/api/research` surface for runs, details, cancel/retry, exports, create-task, and attach-task actions; supports graceful degradation envelopes via availability payloads when capabilities are unavailable - Research routes (`research-routes.ts`) — `/api/research` surface for runs, details, cancel/retry, exports, create-task, and attach-task actions; supports graceful degradation envelopes via availability payloads when capabilities are unavailable
- Plugin-defined roadmap routes under `plugin-routes.ts` dispatch (`/api/plugins/roadmap-planner/...`) - Plugin-defined roadmap routes under `plugin-routes.ts` dispatch (`/api/plugins/fusion-plugin-roadmap/...`)
- Project-scoped store reuse via `project-store-resolver.ts` - Project-scoped store reuse via `project-store-resolver.ts`
- Rate limiting (`rate-limit.ts`) - Rate limiting (`rate-limit.ts`)
- Static SPA hosting (Vite build output) - Static SPA hosting (Vite build output)

View File

@@ -4,7 +4,7 @@
- Backend roadmap ownership lives in `plugins/fusion-plugin-roadmap/**`. - Backend roadmap ownership lives in `plugins/fusion-plugin-roadmap/**`.
- Frontend roadmap ownership lives in `plugins/fusion-plugin-roadmap/**` (`dashboard/` and `dashboard-view` export). - Frontend roadmap ownership lives in `plugins/fusion-plugin-roadmap/**` (`dashboard/` and `dashboard-view` export).
- Canonical API namespace is plugin-scoped: `/api/plugins/roadmap-planner/...`. - Canonical API namespace is plugin-scoped: `/api/plugins/fusion-plugin-roadmap/...`.
- Dashboard Roadmaps view is registered via plugin dashboard view host (`registerBundledPluginViews` + `PluginDashboardViewHost`), not hardcoded app-level `RoadmapsView` import. - Dashboard Roadmaps view is registered via plugin dashboard view host (`registerBundledPluginViews` + `PluginDashboardViewHost`), not hardcoded app-level `RoadmapsView` import.
- Roadmap schema initialization is plugin-owned via `hooks.onSchemaInit` (`ensureRoadmapSchema`). - Roadmap schema initialization is plugin-owned via `hooks.onSchemaInit` (`ensureRoadmapSchema`).
- AI suggestions use plugin-context session factory (`ctx.createAiSession` passed into suggestion generation), not direct `@fusion/engine` imports. - AI suggestions use plugin-context session factory (`ctx.createAiSession` passed into suggestion generation), not direct `@fusion/engine` imports.
@@ -21,7 +21,7 @@
### Route contract expectations ### Route contract expectations
- Plugin routes are expected under `/api/plugins/roadmap-planner/...` via host plugin route mounting. - Plugin routes are expected under `/api/plugins/fusion-plugin-roadmap/...` via host plugin route mounting.
- Legacy `/api/roadmaps...` compatibility behavior (if present) must delegate to plugin implementation and be documented. - Legacy `/api/roadmaps...` compatibility behavior (if present) must delegate to plugin implementation and be documented.
## 3) Host/plugin integration checks ## 3) Host/plugin integration checks
@@ -34,14 +34,14 @@
### Integration points verified (Step 1) ### Integration points verified (Step 1)
- `plugins/fusion-plugin-roadmap/src/index.ts` declares: - `plugins/fusion-plugin-roadmap/src/index.ts` declares:
- `manifest.id = "roadmap-planner"` - `manifest.id = "fusion-plugin-roadmap"`
- `hooks.onSchemaInit = ensureRoadmapSchema` - `hooks.onSchemaInit = ensureRoadmapSchema`
- `routes = createRoadmapPluginRoutes()` - `routes = createRoadmapPluginRoutes()`
- `dashboardViews` entry `{ viewId: "roadmaps", placement: "primary" }` - `dashboardViews` entry `{ viewId: "roadmaps", placement: "primary" }`
- `packages/dashboard/app/plugins/registerBundledPluginViews.ts` statically registers: - `packages/dashboard/app/plugins/registerBundledPluginViews.ts` statically registers:
- plugin id `roadmap-planner` - plugin id `fusion-plugin-roadmap`
- view id `roadmaps` - view id `roadmaps`
- lazy import `@fusion-plugin-examples/roadmap/dashboard-view` - lazy import `@fusion-plugin-examples/fusion-plugin-roadmap/dashboard-view`
- `packages/dashboard/app/App.tsx` renders plugin views through `PluginDashboardViewHost`. - `packages/dashboard/app/App.tsx` renders plugin views through `PluginDashboardViewHost`.
## 4) Manual UI scenarios ## 4) Manual UI scenarios
@@ -71,7 +71,7 @@ Notes:
### Step 2 backend/API verification checks ### Step 2 backend/API verification checks
- `pnpm --filter @fusion-plugin-examples/roadmap test` - `pnpm --filter @fusion-plugin-examples/fusion-plugin-roadmap test`
- ✅ Pass (9 files, 227 tests) - ✅ Pass (9 files, 227 tests)
- Coverage includes roadmap CRUD store operations, milestone/feature reorder, cross-milestone move, handoff/export mapping, deterministic ordering/lineage, route handlers, dashboard API client namespace, and AI suggestion behavior (success/error/timeout/service unavailable). - Coverage includes roadmap CRUD store operations, milestone/feature reorder, cross-milestone move, handoff/export mapping, deterministic ordering/lineage, route handlers, dashboard API client namespace, and AI suggestion behavior (success/error/timeout/service unavailable).
- `pnpm --filter @fusion/dashboard exec vitest run src/__tests__/roadmap-routes.routes.test.ts src/__tests__/plugin-routes-wiring.test.ts --silent=passed-only --reporter=dot` - `pnpm --filter @fusion/dashboard exec vitest run src/__tests__/roadmap-routes.routes.test.ts src/__tests__/plugin-routes-wiring.test.ts --silent=passed-only --reporter=dot`

View File

@@ -41,7 +41,7 @@ describe("CLI bundle output", () => {
expect(content).not.toContain('"@fusion/core"'); expect(content).not.toContain('"@fusion/core"');
expect(content).not.toContain('"@fusion/dashboard"'); expect(content).not.toContain('"@fusion/dashboard"');
expect(content).not.toContain('"@fusion/engine"'); expect(content).not.toContain('"@fusion/engine"');
expect(content).not.toContain('"@fusion-plugin-examples/roadmap"'); expect(content).not.toContain('"@fusion-plugin-examples/fusion-plugin-roadmap"');
}); });
it("does not contain runtime memory-backend side-load imports", () => { it("does not contain runtime memory-backend side-load imports", () => {
@@ -187,7 +187,7 @@ describe("CLI bundle output", () => {
expect(existsSync(manifestPath)).toBe(true); expect(existsSync(manifestPath)).toBe(true);
const manifest = JSON.parse(readFileSync(manifestPath, "utf-8")) as { id?: string; name?: string }; const manifest = JSON.parse(readFileSync(manifestPath, "utf-8")) as { id?: string; name?: string };
expect(manifest.id).toBe("roadmap-planner"); expect(manifest.id).toBe("fusion-plugin-roadmap");
expect(typeof manifest.name).toBe("string"); expect(typeof manifest.name).toBe("string");
expect(manifest.name?.length).toBeGreaterThan(0); expect(manifest.name?.length).toBeGreaterThan(0);

View File

@@ -598,7 +598,7 @@ describe("Database", () => {
it("executes roadmap plugin schema hook to create roadmap-owned tables and indexes", async () => { it("executes roadmap plugin schema hook to create roadmap-owned tables and indexes", async () => {
await db.runPluginSchemaInits([ await db.runPluginSchemaInits([
{ {
pluginId: "roadmap-planner", pluginId: "fusion-plugin-roadmap",
hook: ensureRoadmapSchema, hook: ensureRoadmapSchema,
}, },
]); ]);

View File

@@ -9,13 +9,13 @@ describe("PluginLoader.createRouteContext", () => {
const baseStore = { getRootDir: () => "/tmp" } as any; const baseStore = { getRootDir: () => "/tmp" } as any;
const loader = new PluginLoader({ pluginStore, taskStore: baseStore }); const loader = new PluginLoader({ pluginStore, taskStore: baseStore });
const resolveProjectTaskStore = vi.fn(); const resolveProjectTaskStore = vi.fn();
const ctx = await loader.createRouteContext("roadmap-planner", { const ctx = await loader.createRouteContext("fusion-plugin-roadmap", {
taskStore: baseStore, taskStore: baseStore,
settings: { ok: true }, settings: { ok: true },
resolveProjectTaskStore, resolveProjectTaskStore,
}); });
expect(ctx.pluginId).toBe("roadmap-planner"); expect(ctx.pluginId).toBe("fusion-plugin-roadmap");
expect(ctx.settings).toEqual({ ok: true }); expect(ctx.settings).toEqual({ ok: true });
expect(ctx.resolveProjectTaskStore).toBe(resolveProjectTaskStore); expect(ctx.resolveProjectTaskStore).toBe(resolveProjectTaskStore);
}); });

View File

@@ -841,7 +841,7 @@ describe("PluginUiSlotDefinition", () => {
describe("PluginDashboardViewDefinition", () => { describe("PluginDashboardViewDefinition", () => {
it("accepts a valid PluginDashboardViewDefinition with optional fields", () => { it("accepts a valid PluginDashboardViewDefinition with optional fields", () => {
const view = { const view = {
viewId: "roadmap-planner", viewId: "fusion-plugin-roadmap",
label: "Roadmap Planner", label: "Roadmap Planner",
componentPath: "./views/RoadmapPlanner.js", componentPath: "./views/RoadmapPlanner.js",
icon: "Map", icon: "Map",
@@ -850,7 +850,7 @@ describe("PluginDashboardViewDefinition", () => {
description: "Plan milestones and slices", description: "Plan milestones and slices",
}; };
expect(view.viewId).toBe("roadmap-planner"); expect(view.viewId).toBe("fusion-plugin-roadmap");
expect(view.placement).toBe("overflow"); expect(view.placement).toBe("overflow");
expect(view.description).toContain("milestones"); expect(view.description).toContain("milestones");
}); });

View File

@@ -834,7 +834,7 @@ describe("plugin dashboard view API wrappers", () => {
it("fetchPluginDashboardViews calls /api/plugins/dashboard-views", async () => { it("fetchPluginDashboardViews calls /api/plugins/dashboard-views", async () => {
globalThis.fetch = vi.fn().mockReturnValue(mockFetchResponse(true, [ globalThis.fetch = vi.fn().mockReturnValue(mockFetchResponse(true, [
{ {
pluginId: "roadmap-planner", pluginId: "fusion-plugin-roadmap",
view: { viewId: "roadmaps", label: "Roadmaps", componentPath: "./dashboard-view" }, view: { viewId: "roadmaps", label: "Roadmaps", componentPath: "./dashboard-view" },
}, },
])); ]));
@@ -850,7 +850,7 @@ describe("plugin dashboard view API wrappers", () => {
it("fetchPluginUiSlots calls /api/plugins/ui-slots and keeps slot shape", async () => { it("fetchPluginUiSlots calls /api/plugins/ui-slots and keeps slot shape", async () => {
globalThis.fetch = vi.fn().mockReturnValue(mockFetchResponse(true, [ globalThis.fetch = vi.fn().mockReturnValue(mockFetchResponse(true, [
{ {
pluginId: "roadmap-planner", pluginId: "fusion-plugin-roadmap",
slot: { slot: {
slotId: "task-detail-tab", slotId: "task-detail-tab",
label: "Roadmap Details", label: "Roadmap Details",

View File

@@ -411,7 +411,7 @@ describe("MobileNavBar", () => {
experimentalFeatures={{}} experimentalFeatures={{}}
pluginDashboardViews={[ pluginDashboardViews={[
{ {
pluginId: "roadmap-planner", pluginId: "fusion-plugin-roadmap",
view: { viewId: "roadmaps", label: "Roadmaps", componentPath: "./RoadmapsView", icon: "Map", placement: "primary" }, view: { viewId: "roadmaps", label: "Roadmaps", componentPath: "./RoadmapsView", icon: "Map", placement: "primary" },
}, },
]} ]}

View File

@@ -73,7 +73,7 @@ describe("useViewState", () => {
const { result } = renderHook(() => useViewState(createOptions())); const { result } = renderHook(() => useViewState(createOptions()));
await waitFor(() => { await waitFor(() => {
expect(result.current.taskView).toBe("plugin:roadmap-planner:roadmaps"); expect(result.current.taskView).toBe("plugin:fusion-plugin-roadmap:roadmaps");
}); });
}); });

View File

@@ -37,7 +37,7 @@ function isTaskView(value: string | null): value is TaskView {
return value !== null && (isBuiltInTaskView(value) || isPluginViewId(value)); return value !== null && (isBuiltInTaskView(value) || isPluginViewId(value));
} }
const LEGACY_ROADMAPS_PLUGIN_VIEW = getPluginViewId("roadmap-planner", "roadmaps"); const LEGACY_ROADMAPS_PLUGIN_VIEW = getPluginViewId("fusion-plugin-roadmap", "roadmaps");
function normalizeTaskView(value: TaskView): TaskView { function normalizeTaskView(value: TaskView): TaskView {
return value === "devserver" ? "dev-server" : value; return value === "devserver" ? "dev-server" : value;
@@ -47,7 +47,7 @@ function migrateLegacyRoadmapsView(value: string): TaskView {
if (value !== "roadmaps") { if (value !== "roadmaps") {
return "board"; return "board";
} }
return isPluginViewRegistered("roadmap-planner", "roadmaps") ? LEGACY_ROADMAPS_PLUGIN_VIEW : "board"; return isPluginViewRegistered("fusion-plugin-roadmap", "roadmaps") ? LEGACY_ROADMAPS_PLUGIN_VIEW : "board";
} }
interface UseViewStateOptions { interface UseViewStateOptions {

View File

@@ -18,7 +18,7 @@ describe("pluginViewRegistry", () => {
it("builds plugin IDs", () => { it("builds plugin IDs", () => {
expect(getPluginViewId("plugin-a", "main")).toBe("plugin:plugin-a:main"); expect(getPluginViewId("plugin-a", "main")).toBe("plugin:plugin-a:main");
expect(getPluginViewId("roadmap-planner", "roadmaps")).toBe("plugin:roadmap-planner:roadmaps"); expect(getPluginViewId("fusion-plugin-roadmap", "roadmaps")).toBe("plugin:fusion-plugin-roadmap:roadmaps");
}); });
it("parses and validates plugin IDs", () => { it("parses and validates plugin IDs", () => {
@@ -78,11 +78,11 @@ describe("pluginViewRegistry", () => {
expect(await screen.findByText("proj-1")).toBeInTheDocument(); expect(await screen.findByText("proj-1")).toBeInTheDocument();
}); });
it("resolves roadmap-planner registry entry and avoids unavailable fallback", async () => { it("resolves fusion-plugin-roadmap registry entry and avoids unavailable fallback", async () => {
const RoadmapsView = lazy(async () => ({ default: () => <div>Roadmaps Plugin View</div> })); const RoadmapsView = lazy(async () => ({ default: () => <div>Roadmaps Plugin View</div> }));
registerPluginView("roadmap-planner", "roadmaps", RoadmapsView); registerPluginView("fusion-plugin-roadmap", "roadmaps", RoadmapsView);
render(<>{PluginDashboardViewHost({ viewId: "plugin:roadmap-planner:roadmaps" })}</>); render(<>{PluginDashboardViewHost({ viewId: "plugin:fusion-plugin-roadmap:roadmaps" })}</>);
expect(await screen.findByText("Roadmaps Plugin View")).toBeInTheDocument(); expect(await screen.findByText("Roadmaps Plugin View")).toBeInTheDocument();
expect(screen.queryByTestId("plugin-view-unavailable")).toBeNull(); expect(screen.queryByTestId("plugin-view-unavailable")).toBeNull();

View File

@@ -15,7 +15,7 @@ vi.mock("@fusion-plugin-examples/dependency-graph/dashboard-view", () => ({
DependencyGraphDashboardView: (...args: unknown[]) => MockDependencyGraphDashboardView(...args), DependencyGraphDashboardView: (...args: unknown[]) => MockDependencyGraphDashboardView(...args),
})); }));
vi.mock("@fusion-plugin-examples/roadmap/dashboard-view", () => ({ vi.mock("@fusion-plugin-examples/fusion-plugin-roadmap/dashboard-view", () => ({
RoadmapDashboardView: (...args: unknown[]) => MockRoadmapDashboardView(...args), RoadmapDashboardView: (...args: unknown[]) => MockRoadmapDashboardView(...args),
})); }));
@@ -37,7 +37,7 @@ describe("registerBundledPluginViews", () => {
registerBundledPluginViews(); registerBundledPluginViews();
expect(getPluginViewComponent("fusion-plugin-dependency-graph", "graph")).toBeTruthy(); expect(getPluginViewComponent("fusion-plugin-dependency-graph", "graph")).toBeTruthy();
expect(getPluginViewComponent("roadmap-planner", "roadmaps")).toBeTruthy(); expect(getPluginViewComponent("fusion-plugin-roadmap", "roadmaps")).toBeTruthy();
expect(getPluginViewComponent("fusion-plugin-cli-printing-press", "wizard")).toBeTruthy(); expect(getPluginViewComponent("fusion-plugin-cli-printing-press", "wizard")).toBeTruthy();
expect(getPluginViewComponent("fusion-plugin-cli-printing-press", "manage")).toBeTruthy(); expect(getPluginViewComponent("fusion-plugin-cli-printing-press", "manage")).toBeTruthy();
}); });
@@ -56,7 +56,7 @@ describe("registerBundledPluginViews", () => {
registerBundledPluginViews(); registerBundledPluginViews();
expect(isPluginViewRegistered("fusion-plugin-dependency-graph", "graph")).toBe(true); expect(isPluginViewRegistered("fusion-plugin-dependency-graph", "graph")).toBe(true);
expect(isPluginViewRegistered("roadmap-planner", "roadmaps")).toBe(true); expect(isPluginViewRegistered("fusion-plugin-roadmap", "roadmaps")).toBe(true);
expect(isPluginViewRegistered("fusion-plugin-cli-printing-press", "wizard")).toBe(true); expect(isPluginViewRegistered("fusion-plugin-cli-printing-press", "wizard")).toBe(true);
expect(isPluginViewRegistered("fusion-plugin-cli-printing-press", "manage")).toBe(true); expect(isPluginViewRegistered("fusion-plugin-cli-printing-press", "manage")).toBe(true);
// Unknown plugin/view should not be registered // Unknown plugin/view should not be registered

View File

@@ -80,7 +80,7 @@ export function registerBundledPluginViews(): void {
); );
registerPluginView( registerPluginView(
"roadmap-planner", "fusion-plugin-roadmap",
"roadmaps", "roadmaps",
lazy(loadRoadmapView), lazy(loadRoadmapView),
); );

View File

@@ -1193,24 +1193,24 @@ describe("plugin-defined route dispatch", () => {
it("registers PATCH routes from plugins", () => { it("registers PATCH routes from plugins", () => {
const pluginRunner = { const pluginRunner = {
getPluginRoutes: vi.fn().mockReturnValue([ getPluginRoutes: vi.fn().mockReturnValue([
{ pluginId: "roadmap-planner", route: { method: "PATCH", path: "/roadmaps/x", handler: vi.fn() } }, { pluginId: "fusion-plugin-roadmap", route: { method: "PATCH", path: "/roadmaps/x", handler: vi.fn() } },
]), ]),
}; };
const pluginStore = createMockPluginStore(); const pluginStore = createMockPluginStore();
const router = createPluginRouter(pluginStore, createMockPluginLoader({ const router = createPluginRouter(pluginStore, createMockPluginLoader({
createRouteContext: vi.fn().mockResolvedValue({ createRouteContext: vi.fn().mockResolvedValue({
pluginId: "roadmap-planner", pluginId: "fusion-plugin-roadmap",
taskStore: createMockTaskStore(), taskStore: createMockTaskStore(),
settings: {}, settings: {},
logger: { info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn() }, logger: { info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn() },
emitEvent: vi.fn(), emitEvent: vi.fn(),
}), }),
getPlugin: vi.fn().mockReturnValue({ manifest: { id: "roadmap-planner" } }), getPlugin: vi.fn().mockReturnValue({ manifest: { id: "fusion-plugin-roadmap" } }),
} as any), pluginRunner as any, createMockTaskStore()); } as any), pluginRunner as any, createMockTaskStore());
const stack = (router as any).stack as Array<{ route?: { path: string; methods: Record<string, boolean> } }>; const stack = (router as any).stack as Array<{ route?: { path: string; methods: Record<string, boolean> } }>;
const patchRoute = stack.find((layer) => layer.route?.path === "/roadmap-planner/roadmaps/x"); const patchRoute = stack.find((layer) => layer.route?.path === "/fusion-plugin-roadmap/roadmaps/x");
expect(patchRoute?.route?.methods.patch).toBe(true); expect(patchRoute?.route?.methods.patch).toBe(true);
}); });
@@ -1218,14 +1218,14 @@ describe("plugin-defined route dispatch", () => {
const routeHandler = vi.fn().mockResolvedValue({ ok: true }); const routeHandler = vi.fn().mockResolvedValue({ ok: true });
const pluginRunner = { const pluginRunner = {
getPluginRoutes: vi.fn().mockReturnValue([ getPluginRoutes: vi.fn().mockReturnValue([
{ pluginId: "roadmap-planner", route: { method: "POST", path: "/ctx-check", handler: routeHandler } }, { pluginId: "fusion-plugin-roadmap", route: { method: "POST", path: "/ctx-check", handler: routeHandler } },
]), ]),
}; };
const scopedPluginStore = createMockPluginStore(); const scopedPluginStore = createMockPluginStore();
const scopedTaskStore = createMockTaskStore({ getPluginStore: vi.fn().mockReturnValue(scopedPluginStore) }); const scopedTaskStore = createMockTaskStore({ getPluginStore: vi.fn().mockReturnValue(scopedPluginStore) });
mockGetOrCreateProjectStore.mockResolvedValue(scopedTaskStore); mockGetOrCreateProjectStore.mockResolvedValue(scopedTaskStore);
const createRouteContext = vi.fn().mockImplementation(async (_pluginId: string, overrides: any) => ({ const createRouteContext = vi.fn().mockImplementation(async (_pluginId: string, overrides: any) => ({
pluginId: "roadmap-planner", pluginId: "fusion-plugin-roadmap",
taskStore: overrides.taskStore, taskStore: overrides.taskStore,
settings: overrides.settings, settings: overrides.settings,
logger: { info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn() }, logger: { info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn() },
@@ -1235,7 +1235,7 @@ describe("plugin-defined route dispatch", () => {
})); }));
const pluginLoader = createMockPluginLoader({ const pluginLoader = createMockPluginLoader({
createRouteContext, createRouteContext,
getPlugin: vi.fn().mockReturnValue({ manifest: { id: "roadmap-planner" } }), getPlugin: vi.fn().mockReturnValue({ manifest: { id: "fusion-plugin-roadmap" } }),
} as any); } as any);
const pluginStore = createMockPluginStore(); const pluginStore = createMockPluginStore();
@@ -1243,9 +1243,9 @@ describe("plugin-defined route dispatch", () => {
app.use(express.json()); app.use(express.json());
app.use("/api/plugins", createPluginRouter(pluginStore, pluginLoader, pluginRunner as any, createMockTaskStore())); app.use("/api/plugins", createPluginRouter(pluginStore, pluginLoader, pluginRunner as any, createMockTaskStore()));
const res = await REQUEST(app, "POST", "/api/plugins/roadmap-planner/ctx-check", { projectId: "proj_123" }); const res = await REQUEST(app, "POST", "/api/plugins/fusion-plugin-roadmap/ctx-check", { projectId: "proj_123" });
expect(res.status).toBe(200); expect(res.status).toBe(200);
expect(createRouteContext).toHaveBeenCalledWith("roadmap-planner", expect.objectContaining({ expect(createRouteContext).toHaveBeenCalledWith("fusion-plugin-roadmap", expect.objectContaining({
taskStore: scopedTaskStore, taskStore: scopedTaskStore,
resolveProjectTaskStore: projectStoreResolver.getOrCreateProjectStore, resolveProjectTaskStore: projectStoreResolver.getOrCreateProjectStore,
})); }));

View File

@@ -1028,7 +1028,7 @@ describe("GET /api/plugins/dashboard-views", () => {
it("keeps dashboard-views payload separate from ui-slots payload", async () => { it("keeps dashboard-views payload separate from ui-slots payload", async () => {
(pluginLoader.getPluginDashboardViews as ReturnType<typeof vi.fn>).mockReturnValue([ (pluginLoader.getPluginDashboardViews as ReturnType<typeof vi.fn>).mockReturnValue([
{ {
pluginId: "roadmap-planner", pluginId: "fusion-plugin-roadmap",
view: { view: {
viewId: "roadmaps", viewId: "roadmaps",
label: "Roadmaps", label: "Roadmaps",
@@ -1038,7 +1038,7 @@ describe("GET /api/plugins/dashboard-views", () => {
]); ]);
(pluginLoader.getPluginUiSlots as ReturnType<typeof vi.fn>).mockReturnValue([ (pluginLoader.getPluginUiSlots as ReturnType<typeof vi.fn>).mockReturnValue([
{ {
pluginId: "roadmap-planner", pluginId: "fusion-plugin-roadmap",
slot: { slot: {
slotId: "task-detail-tab", slotId: "task-detail-tab",
label: "Roadmap Details", label: "Roadmap Details",
@@ -1696,7 +1696,8 @@ describe("createPluginRouter plugin-defined route responses", () => {
const res = await performGet(app, "/plugins/demo/html"); const res = await performGet(app, "/plugins/demo/html");
expect(res.status).toBe(200); expect(res.status).toBe(200);
expect(res.headers["content-type"]).toContain("text/html"); expect(res.headers["content-type"]).toContain("text/html");
expect(res.text).toContain("<html><body>Hello</body></html>"); const html = typeof res.text === "string" ? res.text : String(res.body ?? "");
expect(html).toContain("<html><body>Hello</body></html>");
}); });
it("propagates custom response headers", async () => { it("propagates custom response headers", async () => {

View File

@@ -4,18 +4,21 @@ import { describe, it, expect } from "vitest";
import express from "express"; import express from "express";
import { registerIntegratedRouters } from "../routes/register-integrated-routers.js"; import { registerIntegratedRouters } from "../routes/register-integrated-routers.js";
describe("integrated roadmap routes removed", () => { describe("integrated roadmap routes compatibility", () => {
it("does not register a legacy /roadmaps mount", () => { it("registers a legacy /roadmaps mount that delegates to plugin routes", () => {
const router = express.Router(); const router = express.Router();
registerIntegratedRouters({ registerIntegratedRouters({
router, router,
store: {} as never, store: {} as never,
}); });
const mountedPaths = (router as unknown as { stack?: Array<{ regexp?: { source?: string } }> }).stack const stack = (router as unknown as { stack?: Array<{ regexp?: { source?: string }; handle?: { stack?: Array<{ route?: { path?: string } }> } }> }).stack ?? [];
?.map((layer) => layer.regexp?.source ?? "")
?? [];
expect(mountedPaths.some((path) => path.includes("roadmaps"))).toBe(false); const hasRoadmapMount = stack.some((layer) => {
if (layer.regexp?.source?.includes("roadmaps")) return true;
return layer.handle?.stack?.some((nested) => typeof nested.route?.path === "string" && nested.route.path.startsWith("/roadmaps")) ?? false;
});
expect(hasRoadmapMount).toBe(true);
}); });
}); });

View File

@@ -0,0 +1,9 @@
import { describe, it, expect } from "vitest";
import { SUGGESTION_TIMEOUT_MS } from "../roadmap-suggestions.js";
describe("roadmap suggestions compatibility exports", () => {
it("re-exports suggestion timeout from roadmap plugin", () => {
expect(typeof SUGGESTION_TIMEOUT_MS).toBe("number");
expect(SUGGESTION_TIMEOUT_MS).toBeGreaterThan(0);
});
});

View File

@@ -0,0 +1,88 @@
import { Router, type Request, type Response } from "express";
import { getCreateAiSessionFactory, type PluginContext, type TaskStore } from "@fusion/core";
import { createRoadmapPluginRoutes } from "@fusion-plugin-examples/roadmap";
import { getOrCreateProjectStore } from "./project-store-resolver.js";
function asQueryRecord(query: Request["query"]): Record<string, string | string[] | undefined> {
return query as Record<string, string | string[] | undefined>;
}
function resolveProjectId(req: Request): string | undefined {
const queryProjectId = req.query.projectId;
if (typeof queryProjectId === "string" && queryProjectId.trim()) return queryProjectId;
if (req.body && typeof req.body === "object") {
const bodyProjectId = (req.body as { projectId?: unknown }).projectId;
if (typeof bodyProjectId === "string" && bodyProjectId.trim()) return bodyProjectId;
}
return undefined;
}
export function createRoadmapCompatibilityRouter(defaultTaskStore: TaskStore): Router {
const router = Router();
const routes = createRoadmapPluginRoutes();
for (const route of routes) {
if (!route.path.startsWith("/roadmaps")) continue;
const handler = async (req: Request, res: Response): Promise<void> => {
const projectId = resolveProjectId(req);
const scopedStore = projectId ? await getOrCreateProjectStore(projectId) : null;
const taskStore = scopedStore ?? defaultTaskStore;
const createAiSession = await getCreateAiSessionFactory();
const ctx: PluginContext = {
pluginId: "fusion-plugin-roadmap",
taskStore,
settings: {},
logger: console,
emitEvent: () => {},
createAiSession,
resolveProjectTaskStore: getOrCreateProjectStore,
};
const result = await route.handler({ params: req.params as Record<string, string>, query: asQueryRecord(req.query), body: req.body }, ctx);
if (result && typeof result === "object" && "status" in (result as Record<string, unknown>) && typeof (result as { status?: unknown }).status === "number") {
const response = result as { status: number; body?: unknown; headers?: Record<string, string>; contentType?: string };
if (response.headers) {
for (const [name, value] of Object.entries(response.headers)) {
res.setHeader(name, value);
}
}
if (response.contentType) res.setHeader("Content-Type", response.contentType);
if (response.status === 204) {
res.status(204).send();
return;
}
if (response.body === undefined) {
res.status(response.status).send();
return;
}
res.status(response.status).json(response.body);
return;
}
res.status(200).json(result);
};
switch (route.method) {
case "GET":
router.get(route.path, handler);
break;
case "POST":
router.post(route.path, handler);
break;
case "PUT":
router.put(route.path, handler);
break;
case "PATCH":
router.patch(route.path, handler);
break;
case "DELETE":
router.delete(route.path, handler);
break;
}
}
return router;
}

View File

@@ -0,0 +1,15 @@
export {
FEATURE_SUGGESTION_SYSTEM_PROMPT,
MILESTONE_SUGGESTION_SYSTEM_PROMPT,
ParseError,
ServiceUnavailableError,
SUGGESTION_TIMEOUT_MS,
ValidationError,
__resetSuggestionState,
__setCreateAiSessionFactory,
__setCreateFnAgent,
generateFeatureSuggestions,
generateMilestoneSuggestions,
validateFeatureSuggestionInput,
validateSuggestionInput,
} from "@fusion-plugin-examples/roadmap/roadmap-suggestions";

View File

@@ -139,7 +139,7 @@ Integrated routers are mounted through `register-integrated-routers.ts` and inte
- `createMissionRouter``/api/missions` - `createMissionRouter``/api/missions`
- `createInsightsRouter``/api/insights` - `createInsightsRouter``/api/insights`
- `createTodoRouter``/api/todos` - `createTodoRouter``/api/todos`
- Roadmap endpoints are plugin-owned and exposed under `/api/plugins/roadmap-planner/...`. - Roadmap endpoints are plugin-owned and exposed under `/api/plugins/fusion-plugin-roadmap/...`.
- `registerIntegratedDevServerRouter(...)` mounts: - `registerIntegratedDevServerRouter(...)` mounts:
- `createDevServerRouter``/api/dev-server` - `createDevServerRouter``/api/dev-server`

View File

@@ -6,6 +6,7 @@ import { createInsightsRouter } from "../insights-routes.js";
import { createEvalsRouter } from "../evals-routes.js"; import { createEvalsRouter } from "../evals-routes.js";
import { createResearchRouter } from "../research-routes.js"; import { createResearchRouter } from "../research-routes.js";
import { createTodoRouter } from "../todo-routes.js"; import { createTodoRouter } from "../todo-routes.js";
import { createRoadmapCompatibilityRouter } from "../roadmap-routes.js";
import { createDevServerRouter } from "../dev-server-routes.js"; import { createDevServerRouter } from "../dev-server-routes.js";
import type { AiSessionStore } from "../ai-session-store.js"; import type { AiSessionStore } from "../ai-session-store.js";
import { createStashRecoveryRouter } from "./register-stash-recovery-routes.js"; import { createStashRecoveryRouter } from "./register-stash-recovery-routes.js";
@@ -37,6 +38,7 @@ export function registerIntegratedRouters({
router.use("/evals", createEvalsRouter(store)); router.use("/evals", createEvalsRouter(store));
router.use("/research", createResearchRouter(store)); router.use("/research", createResearchRouter(store));
router.use("/todos", createTodoRouter(store)); router.use("/todos", createTodoRouter(store));
router.use("/roadmaps", createRoadmapCompatibilityRouter(store));
router.use("/stash-recovery", createStashRecoveryRouter(store)); router.use("/stash-recovery", createStashRecoveryRouter(store));
} }

View File

@@ -4,9 +4,9 @@
## Plugin identity ## Plugin identity
- Manifest id: `roadmap-planner` - Manifest id: `fusion-plugin-roadmap`
- Route namespace: `/api/plugins/roadmap-planner/*` - Route namespace: `/api/plugins/fusion-plugin-roadmap/*`
- Dashboard view id: `plugin:roadmap-planner:roadmaps` - Dashboard view id: `plugin:fusion-plugin-roadmap:roadmaps`
## Package layout ## Package layout
@@ -18,7 +18,7 @@
- `src/dashboard/RoadmapsView.tsx` — plugin-owned roadmap planner page - `src/dashboard/RoadmapsView.tsx` — plugin-owned roadmap planner page
- `src/dashboard/useRoadmaps.ts` — plugin-owned roadmap CRUD/reorder/suggestions/handoff hook - `src/dashboard/useRoadmaps.ts` — plugin-owned roadmap CRUD/reorder/suggestions/handoff hook
- `src/dashboard/RoadmapsView.css` — plugin-owned roadmap styles - `src/dashboard/RoadmapsView.css` — plugin-owned roadmap styles
- `src/dashboard/api.ts` — plugin-local client for `/api/plugins/roadmap-planner/*` - `src/dashboard/api.ts` — plugin-local client for `/api/plugins/fusion-plugin-roadmap/*`
- `src/roadmap-types.ts` + `src/store/*` — roadmap domain types/store - `src/roadmap-types.ts` + `src/store/*` — roadmap domain types/store
## Exported surfaces ## Exported surfaces
@@ -60,7 +60,7 @@ Host-owned responsibilities:
- Execute plugin schema hooks during DB startup and expose resulting tables/indexes to plugin routes. - Execute plugin schema hooks during DB startup and expose resulting tables/indexes to plugin routes.
- Inject `createAiSession()` into plugin runtime/route context. - Inject `createAiSession()` into plugin runtime/route context.
- Discover plugin dashboard views via `/api/plugins/dashboard-views` and resolve plugin view IDs (for roadmap: `plugin:roadmap-planner:roadmaps`) through the host view registry. - Discover plugin dashboard views via `/api/plugins/dashboard-views` and resolve plugin view IDs (for roadmap: `plugin:fusion-plugin-roadmap:roadmaps`) through the host view registry.
## Notes ## Notes

View File

@@ -1,5 +1,5 @@
{ {
"id": "roadmap-planner", "id": "fusion-plugin-roadmap",
"name": "Roadmaps", "name": "Roadmaps",
"version": "0.1.0", "version": "0.1.0",
"description": "Standalone roadmap planning plugin", "description": "Standalone roadmap planning plugin",

View File

@@ -44,11 +44,11 @@ describe("roadmap dashboard api client", () => {
await updateRoadmap("RM-1", { title: "B" }, "proj-1"); await updateRoadmap("RM-1", { title: "B" }, "proj-1");
await deleteRoadmap("RM-1", "proj-1"); await deleteRoadmap("RM-1", "proj-1");
expect(fetchMock.mock.calls[0][0]).toBe("/api/plugins/roadmap-planner/roadmaps?projectId=proj-1"); expect(fetchMock.mock.calls[0][0]).toBe("/api/plugins/fusion-plugin-roadmap/roadmaps?projectId=proj-1");
expect(fetchMock.mock.calls[1][0]).toBe("/api/plugins/roadmap-planner/roadmaps/RM-1?projectId=proj-1"); expect(fetchMock.mock.calls[1][0]).toBe("/api/plugins/fusion-plugin-roadmap/roadmaps/RM-1?projectId=proj-1");
expect(fetchMock.mock.calls[2][0]).toBe("/api/plugins/roadmap-planner/roadmaps?projectId=proj-1"); expect(fetchMock.mock.calls[2][0]).toBe("/api/plugins/fusion-plugin-roadmap/roadmaps?projectId=proj-1");
expect(fetchMock.mock.calls[3][0]).toBe("/api/plugins/roadmap-planner/roadmaps/RM-1?projectId=proj-1"); expect(fetchMock.mock.calls[3][0]).toBe("/api/plugins/fusion-plugin-roadmap/roadmaps/RM-1?projectId=proj-1");
expect(fetchMock.mock.calls[4][0]).toBe("/api/plugins/roadmap-planner/roadmaps/RM-1?projectId=proj-1"); expect(fetchMock.mock.calls[4][0]).toBe("/api/plugins/fusion-plugin-roadmap/roadmaps/RM-1?projectId=proj-1");
}); });
it("uses plugin namespace for milestone and feature reorder/move + suggestions + handoff", async () => { it("uses plugin namespace for milestone and feature reorder/move + suggestions + handoff", async () => {
@@ -68,14 +68,14 @@ describe("roadmap dashboard api client", () => {
await fetchRoadmapHandoff("RM-1", "proj-1"); await fetchRoadmapHandoff("RM-1", "proj-1");
const calledUrls = fetchMock.mock.calls.map((call) => String(call[0])); const calledUrls = fetchMock.mock.calls.map((call) => String(call[0]));
expect(calledUrls).toContain("/api/plugins/roadmap-planner/roadmaps/RM-1/milestones?projectId=proj-1"); expect(calledUrls).toContain("/api/plugins/fusion-plugin-roadmap/roadmaps/RM-1/milestones?projectId=proj-1");
expect(calledUrls).toContain("/api/plugins/roadmap-planner/roadmaps/milestones/RMS-1?projectId=proj-1"); expect(calledUrls).toContain("/api/plugins/fusion-plugin-roadmap/roadmaps/milestones/RMS-1?projectId=proj-1");
expect(calledUrls).toContain("/api/plugins/roadmap-planner/roadmaps/RM-1/milestones/reorder?projectId=proj-1"); expect(calledUrls).toContain("/api/plugins/fusion-plugin-roadmap/roadmaps/RM-1/milestones/reorder?projectId=proj-1");
expect(calledUrls).toContain("/api/plugins/roadmap-planner/roadmaps/milestones/RMS-1/features/reorder?projectId=proj-1"); expect(calledUrls).toContain("/api/plugins/fusion-plugin-roadmap/roadmaps/milestones/RMS-1/features/reorder?projectId=proj-1");
expect(calledUrls).toContain("/api/plugins/roadmap-planner/roadmaps/features/RF-1/move?projectId=proj-1"); expect(calledUrls).toContain("/api/plugins/fusion-plugin-roadmap/roadmaps/features/RF-1/move?projectId=proj-1");
expect(calledUrls).toContain("/api/plugins/roadmap-planner/roadmaps/RM-1/suggestions/milestones?projectId=proj-1"); expect(calledUrls).toContain("/api/plugins/fusion-plugin-roadmap/roadmaps/RM-1/suggestions/milestones?projectId=proj-1");
expect(calledUrls).toContain("/api/plugins/roadmap-planner/roadmaps/milestones/RMS-1/suggestions/features?projectId=proj-1"); expect(calledUrls).toContain("/api/plugins/fusion-plugin-roadmap/roadmaps/milestones/RMS-1/suggestions/features?projectId=proj-1");
expect(calledUrls).toContain("/api/plugins/roadmap-planner/roadmaps/RM-1/handoff?projectId=proj-1"); expect(calledUrls).toContain("/api/plugins/fusion-plugin-roadmap/roadmaps/RM-1/handoff?projectId=proj-1");
}); });
it("surfaces server error body when available", async () => { it("surfaces server error body when available", async () => {

View File

@@ -17,7 +17,7 @@ import plugin, {
normalizeRoadmapMilestoneOrder, normalizeRoadmapMilestoneOrder,
} from "../index.js"; } from "../index.js";
describe("roadmap-planner package surface", () => { describe("fusion-plugin-roadmap package surface", () => {
const tmpDirs: string[] = []; const tmpDirs: string[] = [];
afterEach(async () => { afterEach(async () => {
@@ -48,7 +48,7 @@ describe("roadmap-planner package surface", () => {
}); });
it("exports plugin manifest with roadmap id", () => { it("exports plugin manifest with roadmap id", () => {
expect(plugin.manifest.id).toBe("roadmap-planner"); expect(plugin.manifest.id).toBe("fusion-plugin-roadmap");
}); });
it("registers onSchemaInit hook that creates roadmap tables and indexes", () => { it("registers onSchemaInit hook that creates roadmap tables and indexes", () => {

View File

@@ -3,7 +3,7 @@ import { createRoadmapPluginRoutes } from "../roadmap-routes.js";
function createCtx() { function createCtx() {
return { return {
pluginId: "roadmap-planner", pluginId: "fusion-plugin-roadmap",
taskStore: { taskStore: {
getDatabase: () => ({}), getDatabase: () => ({}),
getRootDir: () => "/tmp/project", getRootDir: () => "/tmp/project",

View File

@@ -13,7 +13,7 @@ import type {
RoadmapFeatureTaskPlanningHandoff, RoadmapFeatureTaskPlanningHandoff,
} from "../roadmap-types.js"; } from "../roadmap-types.js";
const BASE = "/api/plugins/roadmap-planner"; const BASE = "/api/plugins/fusion-plugin-roadmap";
async function request<T>(path: string, init?: RequestInit): Promise<T> { async function request<T>(path: string, init?: RequestInit): Promise<T> {
const response = await fetch(`${BASE}${path}`, { const response = await fetch(`${BASE}${path}`, {

View File

@@ -4,7 +4,7 @@ import { ensureRoadmapSchema } from "./roadmap-schema.js";
const plugin = definePlugin({ const plugin = definePlugin({
manifest: { manifest: {
id: "roadmap-planner", id: "fusion-plugin-roadmap",
name: "Roadmaps", name: "Roadmaps",
version: "0.1.0", version: "0.1.0",
description: "Standalone roadmap planning plugin", description: "Standalone roadmap planning plugin",
@@ -57,17 +57,17 @@ export {
normalizeRoadmapFeatureOrder, normalizeRoadmapFeatureOrder,
applyRoadmapFeatureReorder, applyRoadmapFeatureReorder,
moveRoadmapFeature, moveRoadmapFeature,
} from "./store/roadmap-ordering.js"; } from "./roadmap-ordering.js";
export { export {
mapFeatureToTaskHandoff, mapFeatureToTaskHandoff,
mapRoadmapToMissionHandoff, mapRoadmapToMissionHandoff,
mapRoadmapWithHierarchyToMissionHandoff, mapRoadmapWithHierarchyToMissionHandoff,
mapAllFeaturesToTaskHandoffs, mapAllFeaturesToTaskHandoffs,
} from "./store/roadmap-handoff.js"; } from "./roadmap-handoff.js";
export { RoadmapStore } from "./store/roadmap-store.js"; export { RoadmapStore } from "./roadmap-store.js";
export type { RoadmapStoreEvents } from "./store/roadmap-store.js"; export type { RoadmapStoreEvents } from "./roadmap-store.js";
export { ensureRoadmapSchema } from "./roadmap-schema.js"; export { ensureRoadmapSchema } from "./roadmap-schema.js";
export { RoadmapDashboardView } from "./dashboard-view.js"; export { RoadmapDashboardView } from "./dashboard-view.js";

View File

@@ -0,0 +1,6 @@
export {
mapFeatureToTaskHandoff,
mapRoadmapToMissionHandoff,
mapRoadmapWithHierarchyToMissionHandoff,
mapAllFeaturesToTaskHandoffs,
} from "./store/roadmap-handoff.js";

View File

@@ -0,0 +1,7 @@
export {
normalizeRoadmapMilestoneOrder,
applyRoadmapMilestoneReorder,
normalizeRoadmapFeatureOrder,
applyRoadmapFeatureReorder,
moveRoadmapFeature,
} from "./store/roadmap-ordering.js";

View File

@@ -0,0 +1,2 @@
export { RoadmapStore } from "./store/roadmap-store.js";
export type { RoadmapStoreEvents } from "./store/roadmap-store.js";