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:
@@ -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.
|
||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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`
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|||||||
@@ -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);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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");
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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" },
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
|
|||||||
@@ -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");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ export function registerBundledPluginViews(): void {
|
|||||||
);
|
);
|
||||||
|
|
||||||
registerPluginView(
|
registerPluginView(
|
||||||
"roadmap-planner",
|
"fusion-plugin-roadmap",
|
||||||
"roadmaps",
|
"roadmaps",
|
||||||
lazy(loadRoadmapView),
|
lazy(loadRoadmapView),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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,
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -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 () => {
|
||||||
|
|||||||
@@ -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);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
88
packages/dashboard/src/roadmap-routes.ts
Normal file
88
packages/dashboard/src/roadmap-routes.ts
Normal 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;
|
||||||
|
}
|
||||||
15
packages/dashboard/src/roadmap-suggestions.ts
Normal file
15
packages/dashboard/src/roadmap-suggestions.ts
Normal 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";
|
||||||
@@ -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`
|
||||||
|
|
||||||
|
|||||||
@@ -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));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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 () => {
|
||||||
|
|||||||
@@ -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", () => {
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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}`, {
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|||||||
6
plugins/fusion-plugin-roadmap/src/roadmap-handoff.ts
Normal file
6
plugins/fusion-plugin-roadmap/src/roadmap-handoff.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
export {
|
||||||
|
mapFeatureToTaskHandoff,
|
||||||
|
mapRoadmapToMissionHandoff,
|
||||||
|
mapRoadmapWithHierarchyToMissionHandoff,
|
||||||
|
mapAllFeaturesToTaskHandoffs,
|
||||||
|
} from "./store/roadmap-handoff.js";
|
||||||
7
plugins/fusion-plugin-roadmap/src/roadmap-ordering.ts
Normal file
7
plugins/fusion-plugin-roadmap/src/roadmap-ordering.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
export {
|
||||||
|
normalizeRoadmapMilestoneOrder,
|
||||||
|
applyRoadmapMilestoneReorder,
|
||||||
|
normalizeRoadmapFeatureOrder,
|
||||||
|
applyRoadmapFeatureReorder,
|
||||||
|
moveRoadmapFeature,
|
||||||
|
} from "./store/roadmap-ordering.js";
|
||||||
2
plugins/fusion-plugin-roadmap/src/roadmap-store.ts
Normal file
2
plugins/fusion-plugin-roadmap/src/roadmap-store.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export { RoadmapStore } from "./store/roadmap-store.js";
|
||||||
|
export type { RoadmapStoreEvents } from "./store/roadmap-store.js";
|
||||||
Reference in New Issue
Block a user