@runfusion/fusion 0.25.0 → 0.27.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -0
- package/dist/bin.js +28004 -16888
- package/dist/client/assets/AgentDetailView-B7QRcHJH.css +1 -0
- package/dist/client/assets/AgentDetailView-DwLmRXTY.js +18 -0
- package/dist/client/assets/{AgentsView-B3jYk8Kt.js → AgentsView-D-N6aA0P.js} +12 -7
- package/dist/client/assets/ChatView-DnCdKu8Z.js +1 -0
- package/dist/client/assets/{DevServerView-DyGDEiBP.js → DevServerView-BiA1nYtt.js} +1 -1
- package/dist/client/assets/{DirectoryPicker-D5UIeIl6.js → DirectoryPicker-DvBviDG6.js} +1 -1
- package/dist/client/assets/{DocumentsView-DNHu1T8K.js → DocumentsView-BWXOxpuq.js} +1 -1
- package/dist/client/assets/{EvalsView-CpRobtDi.js → EvalsView-CJFbtL7i.js} +1 -1
- package/dist/client/assets/{ExperimentalAgentOnboardingModal-DOY_oZi7.js → ExperimentalAgentOnboardingModal-DuGIPd0B.js} +1 -1
- package/dist/client/assets/InsightsView-BBpRiolN.js +11 -0
- package/dist/client/assets/{MemoryView-PSc5lGJt.js → MemoryView-48LuNkKk.js} +2 -2
- package/dist/client/assets/NodesView-CGQWSNZM.js +14 -0
- package/dist/client/assets/{PiExtensionsManager-DL_QcN56.js → PiExtensionsManager-i-7UL2oh.js} +2 -2
- package/dist/client/assets/PluginManager-DoSAykD6.js +1 -0
- package/dist/client/assets/{ResearchView-BzCcDAS4.css → ResearchView-BEI4ZSGs.css} +1 -1
- package/dist/client/assets/ResearchView-XZuRtOxE.js +1 -0
- package/dist/client/assets/SettingsModal-Ci0_sqbU.css +1 -0
- package/dist/client/assets/{SettingsModal-CUCyaAyE.js → SettingsModal-CmeF8CN4.js} +1 -1
- package/dist/client/assets/SettingsModal-DBcjf9Bu.js +31 -0
- package/dist/client/assets/SettingsModal-DWKgRxBA.css +1 -0
- package/dist/client/assets/{SetupWizardModal-BKscasuh.js → SetupWizardModal-CgtvpMX9.js} +1 -1
- package/dist/client/assets/{SkillsView-BdELqTy7.js → SkillsView-DErYRumF.js} +1 -1
- package/dist/client/assets/StashRecoveryView-B_8WIQEo.css +1 -0
- package/dist/client/assets/StashRecoveryView-QJrNS4Vg.js +1 -0
- package/dist/client/assets/{TodoView-DFNGBDNV.js → TodoView-BD9NRwq0.js} +2 -2
- package/dist/client/assets/createLucideIcon-BazL2hk5.js +21 -0
- package/dist/client/assets/dashboard-view-BWGH_fAq.js +63 -0
- package/dist/client/assets/dashboard-view-BoTzlP8b.css +1 -0
- package/dist/client/assets/dashboard-view-DdGlfuu-.css +1 -0
- package/dist/client/assets/dashboard-view-Ws9_ZnKu.js +21 -0
- package/dist/client/assets/{folder-open-k1xmUMyr.js → folder-open-CHSlllzf.js} +1 -1
- package/dist/client/assets/index-DCovGm5b.css +1 -0
- package/dist/client/assets/index-bEwSVl7B.js +692 -0
- package/dist/client/assets/{star-ne32r3Y4.js → star-BgVwWAPz.js} +1 -1
- package/dist/client/assets/{upload-MS-2Gx53.js → upload-CAzycxr9.js} +1 -1
- package/dist/client/assets/{users-C519GSjH.js → users-CZnxCCCJ.js} +1 -1
- package/dist/client/index.html +2 -2
- package/dist/client/version.json +1 -1
- package/dist/droid-cli/package.json +1 -1
- package/dist/droid-cli/src/__tests__/index.test.ts +228 -0
- package/dist/extension.js +15810 -10205
- package/dist/pi-claude-cli/package.json +1 -1
- package/dist/pi-claude-cli/src/__tests__/provider.test.ts +36 -22
- package/dist/pi-claude-cli/src/provider.ts +7 -1
- package/dist/plugins/fusion-plugin-cli-printing-press/manifest.json +24 -0
- package/dist/plugins/fusion-plugin-cli-printing-press/package.json +44 -0
- package/dist/plugins/fusion-plugin-cli-printing-press/src/__tests__/TestRunnerPanel.test.tsx +99 -0
- package/dist/plugins/fusion-plugin-cli-printing-press/src/__tests__/config-flow.test.ts +91 -0
- package/dist/plugins/fusion-plugin-cli-printing-press/src/__tests__/dashboard-view.test.tsx +40 -0
- package/dist/plugins/fusion-plugin-cli-printing-press/src/__tests__/dashboard-views.test.ts +46 -0
- package/dist/plugins/fusion-plugin-cli-printing-press/src/__tests__/draft-store.test.ts +50 -0
- package/dist/plugins/fusion-plugin-cli-printing-press/src/__tests__/fixtures/exec-mock.ts +80 -0
- package/dist/plugins/fusion-plugin-cli-printing-press/src/__tests__/fixtures/fixtures.test.ts +40 -0
- package/dist/plugins/fusion-plugin-cli-printing-press/src/__tests__/fixtures/registry.ts +82 -0
- package/dist/plugins/fusion-plugin-cli-printing-press/src/__tests__/generator.test.ts +54 -0
- package/dist/plugins/fusion-plugin-cli-printing-press/src/__tests__/manage-view.test.tsx +98 -0
- package/dist/plugins/fusion-plugin-cli-printing-press/src/__tests__/manifest.test.ts +36 -0
- package/dist/plugins/fusion-plugin-cli-printing-press/src/__tests__/registration.test.ts +29 -0
- package/dist/plugins/fusion-plugin-cli-printing-press/src/__tests__/run-routes.test.ts +98 -0
- package/dist/plugins/fusion-plugin-cli-printing-press/src/__tests__/runner.test.ts +55 -0
- package/dist/plugins/fusion-plugin-cli-printing-press/src/__tests__/runtime-availability.test.ts +61 -0
- package/dist/plugins/fusion-plugin-cli-printing-press/src/__tests__/validation.test.ts +30 -0
- package/dist/plugins/fusion-plugin-cli-printing-press/src/__tests__/wizard-routes.test.ts +61 -0
- package/dist/plugins/fusion-plugin-cli-printing-press/src/__tests__/workflow-integration.test.ts +19 -0
- package/dist/plugins/fusion-plugin-cli-printing-press/src/dashboard-view.css +43 -0
- package/dist/plugins/fusion-plugin-cli-printing-press/src/dashboard-view.tsx +49 -0
- package/dist/plugins/fusion-plugin-cli-printing-press/src/generation/generator.ts +95 -0
- package/dist/plugins/fusion-plugin-cli-printing-press/src/generation/redact.ts +9 -0
- package/dist/plugins/fusion-plugin-cli-printing-press/src/generation/runner.ts +79 -0
- package/dist/plugins/fusion-plugin-cli-printing-press/src/generation/types.ts +31 -0
- package/dist/plugins/fusion-plugin-cli-printing-press/src/index.ts +58 -0
- package/dist/plugins/fusion-plugin-cli-printing-press/src/manage/EditDraftModal.tsx +75 -0
- package/dist/plugins/fusion-plugin-cli-printing-press/src/manage/useDrafts.ts +73 -0
- package/dist/plugins/fusion-plugin-cli-printing-press/src/manage-view.css +79 -0
- package/dist/plugins/fusion-plugin-cli-printing-press/src/manage-view.tsx +122 -0
- package/dist/plugins/fusion-plugin-cli-printing-press/src/routes/wizard-routes.ts +272 -0
- package/dist/plugins/fusion-plugin-cli-printing-press/src/run/TestRunnerPanel.css +70 -0
- package/dist/plugins/fusion-plugin-cli-printing-press/src/run/TestRunnerPanel.tsx +98 -0
- package/dist/plugins/fusion-plugin-cli-printing-press/src/run/useRunGeneratedCli.ts +37 -0
- package/dist/plugins/fusion-plugin-cli-printing-press/src/runtime/__tests__/executor-runtime-env.test.ts +191 -0
- package/dist/plugins/fusion-plugin-cli-printing-press/src/runtime/executor-runtime-env.ts +75 -0
- package/dist/plugins/fusion-plugin-cli-printing-press/src/storage/draft-store.ts +85 -0
- package/dist/plugins/fusion-plugin-cli-printing-press/src/store/__tests__/cli-press-store.test.ts +128 -0
- package/dist/plugins/fusion-plugin-cli-printing-press/src/store/__tests__/credentials.test.ts +62 -0
- package/dist/plugins/fusion-plugin-cli-printing-press/src/store/cli-press-store.ts +427 -0
- package/dist/plugins/fusion-plugin-cli-printing-press/src/store/cli-press-types.ts +110 -0
- package/dist/plugins/fusion-plugin-cli-printing-press/src/store/credentials.ts +95 -0
- package/dist/plugins/fusion-plugin-cli-printing-press/src/wizard/steps.tsx +55 -0
- package/dist/plugins/fusion-plugin-cli-printing-press/src/wizard/types.ts +33 -0
- package/dist/plugins/fusion-plugin-cli-printing-press/src/wizard/validation.ts +63 -0
- package/dist/plugins/fusion-plugin-cursor-runtime/package.json +1 -1
- package/dist/plugins/fusion-plugin-dependency-graph/package.json +1 -1
- package/dist/plugins/fusion-plugin-droid-runtime/package.json +1 -1
- package/dist/plugins/fusion-plugin-hermes-runtime/package.json +1 -1
- package/dist/plugins/fusion-plugin-openclaw-runtime/package.json +1 -1
- package/dist/plugins/fusion-plugin-paperclip-runtime/package.json +1 -1
- package/dist/plugins/fusion-plugin-reports/manifest.json +10 -0
- package/dist/plugins/fusion-plugin-reports/package.json +18 -2
- package/dist/plugins/fusion-plugin-reports/src/__tests__/approval.test.ts +164 -0
- package/dist/plugins/fusion-plugin-reports/src/__tests__/manifest.test.ts +14 -0
- package/dist/plugins/fusion-plugin-reports/src/__tests__/routes-approval.test.ts +109 -0
- package/dist/plugins/fusion-plugin-reports/src/__tests__/scaffold.test.ts +60 -0
- package/dist/plugins/fusion-plugin-reports/src/__tests__/share-blocks.test.ts +83 -0
- package/dist/plugins/fusion-plugin-reports/src/aggregation.ts +23 -0
- package/dist/plugins/fusion-plugin-reports/src/approval.ts +97 -0
- package/dist/plugins/fusion-plugin-reports/src/cadence.ts +23 -0
- package/dist/plugins/fusion-plugin-reports/src/dashboard/ReportsView.css +82 -0
- package/dist/plugins/fusion-plugin-reports/src/dashboard/ReportsView.tsx +24 -0
- package/dist/plugins/fusion-plugin-reports/src/dashboard/__tests__/ReportComparisonDrawer.test.tsx +12 -0
- package/dist/plugins/fusion-plugin-reports/src/dashboard/__tests__/ReportDetailPanel.test.tsx +12 -0
- package/dist/plugins/fusion-plugin-reports/src/dashboard/__tests__/ReportFiltersBar.test.tsx +14 -0
- package/dist/plugins/fusion-plugin-reports/src/dashboard/__tests__/ReportsView.test.tsx +27 -0
- package/dist/plugins/fusion-plugin-reports/src/dashboard/__tests__/api.test.ts +19 -0
- package/dist/plugins/fusion-plugin-reports/src/dashboard/__tests__/useReportSectionDiff.test.ts +11 -0
- package/dist/plugins/fusion-plugin-reports/src/dashboard/__tests__/useReports.test.ts +13 -0
- package/dist/plugins/fusion-plugin-reports/src/dashboard/api.ts +85 -0
- package/dist/plugins/fusion-plugin-reports/src/dashboard/components/ReportApprovalPanel.css +59 -0
- package/dist/plugins/fusion-plugin-reports/src/dashboard/components/ReportApprovalPanel.tsx +58 -0
- package/dist/plugins/fusion-plugin-reports/src/dashboard/components/ReportComparisonDrawer.tsx +21 -0
- package/dist/plugins/fusion-plugin-reports/src/dashboard/components/ReportDetailPanel.tsx +29 -0
- package/dist/plugins/fusion-plugin-reports/src/dashboard/components/ReportEmptyState.tsx +3 -0
- package/dist/plugins/fusion-plugin-reports/src/dashboard/components/ReportFiltersBar.tsx +19 -0
- package/dist/plugins/fusion-plugin-reports/src/dashboard/components/ReportListItem.tsx +8 -0
- package/dist/plugins/fusion-plugin-reports/src/dashboard/components/ShareBlocksPanel.css +29 -0
- package/dist/plugins/fusion-plugin-reports/src/dashboard/components/ShareBlocksPanel.tsx +43 -0
- package/dist/plugins/fusion-plugin-reports/src/dashboard/components/__tests__/ReportApprovalPanel.test.tsx +38 -0
- package/dist/plugins/fusion-plugin-reports/src/dashboard/components/__tests__/ShareBlocksPanel.test.tsx +24 -0
- package/dist/plugins/fusion-plugin-reports/src/dashboard/test-setup.ts +18 -0
- package/dist/plugins/fusion-plugin-reports/src/dashboard/types.ts +22 -0
- package/dist/plugins/fusion-plugin-reports/src/dashboard/useReportPreview.ts +44 -0
- package/dist/plugins/fusion-plugin-reports/src/dashboard/useReportSectionDiff.ts +59 -0
- package/dist/plugins/fusion-plugin-reports/src/dashboard/useReports.ts +71 -0
- package/dist/plugins/fusion-plugin-reports/src/dashboard/useViewportMode.ts +13 -0
- package/dist/plugins/fusion-plugin-reports/src/dashboard-view.tsx +6 -0
- package/dist/plugins/fusion-plugin-reports/src/index.ts +48 -2
- package/dist/plugins/fusion-plugin-reports/src/pipeline.ts +58 -0
- package/dist/plugins/fusion-plugin-reports/src/render/__tests__/escape.test.ts +20 -0
- package/dist/plugins/fusion-plugin-reports/src/render/__tests__/html-template.test.ts +110 -0
- package/dist/plugins/fusion-plugin-reports/src/render/__tests__/standalone-html.test.ts +66 -0
- package/dist/plugins/fusion-plugin-reports/src/render/escape.ts +12 -0
- package/dist/plugins/fusion-plugin-reports/src/render/html-styles.ts +40 -0
- package/dist/plugins/fusion-plugin-reports/src/render/html-template.ts +137 -0
- package/dist/plugins/fusion-plugin-reports/src/render/index.ts +4 -0
- package/dist/plugins/fusion-plugin-reports/src/render/standalone-html.ts +75 -0
- package/dist/plugins/fusion-plugin-reports/src/report-schema.ts +31 -0
- package/dist/plugins/fusion-plugin-reports/src/routes/__tests__/report-export-routes.test.ts +104 -0
- package/dist/plugins/fusion-plugin-reports/src/routes/report-approval-routes.ts +98 -0
- package/dist/plugins/fusion-plugin-reports/src/routes/report-export-routes.ts +77 -0
- package/dist/plugins/fusion-plugin-reports/src/routes/report-list-routes.ts +72 -0
- package/dist/plugins/fusion-plugin-reports/src/runs-store.ts +69 -0
- package/dist/plugins/fusion-plugin-reports/src/share-blocks.ts +82 -0
- package/dist/plugins/fusion-plugin-reports/src/store/report-store.ts +51 -2
- package/dist/plugins/fusion-plugin-reports/src/store/report-types.ts +6 -1
- package/dist/plugins/fusion-plugin-roadmap/bundled.js +1672 -0
- package/dist/plugins/fusion-plugin-roadmap/manifest.json +1 -1
- package/dist/plugins/fusion-plugin-roadmap/package.json +4 -41
- package/dist/plugins/fusion-plugin-whatsapp-chat/package.json +1 -1
- package/package.json +2 -3
- package/skill/fusion/references/engine-tools.md +1 -1
- package/skill/fusion/references/extension-tools.md +3 -3
- package/skill/fusion/references/fusion-capabilities.md +1 -1
- package/dist/client/assets/AgentDetailView-BwJaLqZh.css +0 -1
- package/dist/client/assets/AgentDetailView-ZbHEbYRT.js +0 -18
- package/dist/client/assets/ChatView-DhPkiEGs.js +0 -1
- package/dist/client/assets/InsightsView-vp0RE8Mg.js +0 -11
- package/dist/client/assets/NodesView-DMj6HGeC.js +0 -14
- package/dist/client/assets/PluginManager-BtYKm8IT.js +0 -1
- package/dist/client/assets/ResearchView-BhWqfdV0.js +0 -1
- package/dist/client/assets/SettingsModal-BAgB4_AR.js +0 -31
- package/dist/client/assets/SettingsModal-BNSrO1M9.css +0 -1
- package/dist/client/assets/SettingsModal-DzsLquBu.css +0 -1
- package/dist/client/assets/index-Qq2JOOWx.css +0 -1
- package/dist/client/assets/index-TFYXEVpn.js +0 -692
- package/dist/plugins/fusion-plugin-roadmap/src/__tests__/api-client.test.ts +0 -101
- package/dist/plugins/fusion-plugin-roadmap/src/__tests__/index.test.ts +0 -92
- package/dist/plugins/fusion-plugin-roadmap/src/__tests__/roadmap-routes.test.ts +0 -48
- package/dist/plugins/fusion-plugin-roadmap/src/__tests__/roadmap-suggestions.test.ts +0 -31
- package/dist/plugins/fusion-plugin-roadmap/src/dashboard/RoadmapsView.css +0 -1299
- package/dist/plugins/fusion-plugin-roadmap/src/dashboard/RoadmapsView.tsx +0 -2559
- package/dist/plugins/fusion-plugin-roadmap/src/dashboard/__tests__/RoadmapsView.test.tsx +0 -1144
- package/dist/plugins/fusion-plugin-roadmap/src/dashboard/__tests__/useRoadmaps.test.ts +0 -1756
- package/dist/plugins/fusion-plugin-roadmap/src/dashboard/api.ts +0 -70
- package/dist/plugins/fusion-plugin-roadmap/src/dashboard/test-setup.ts +0 -7
- package/dist/plugins/fusion-plugin-roadmap/src/dashboard/types.ts +0 -1
- package/dist/plugins/fusion-plugin-roadmap/src/dashboard/useConfirm.ts +0 -8
- package/dist/plugins/fusion-plugin-roadmap/src/dashboard/useRoadmaps.ts +0 -1188
- package/dist/plugins/fusion-plugin-roadmap/src/dashboard/useViewportMode.ts +0 -20
- package/dist/plugins/fusion-plugin-roadmap/src/dashboard-view.tsx +0 -6
- package/dist/plugins/fusion-plugin-roadmap/src/index.ts +0 -74
- package/dist/plugins/fusion-plugin-roadmap/src/roadmap-routes.ts +0 -1
- package/dist/plugins/fusion-plugin-roadmap/src/roadmap-schema.ts +0 -41
- package/dist/plugins/fusion-plugin-roadmap/src/roadmap-suggestions.d.ts +0 -15
- package/dist/plugins/fusion-plugin-roadmap/src/roadmap-suggestions.ts +0 -15
- package/dist/plugins/fusion-plugin-roadmap/src/roadmap-types.d.ts +0 -283
- package/dist/plugins/fusion-plugin-roadmap/src/roadmap-types.d.ts.map +0 -1
- package/dist/plugins/fusion-plugin-roadmap/src/roadmap-types.js +0 -21
- package/dist/plugins/fusion-plugin-roadmap/src/roadmap-types.js.map +0 -1
- package/dist/plugins/fusion-plugin-roadmap/src/roadmap-types.ts +0 -310
- package/dist/plugins/fusion-plugin-roadmap/src/routes/roadmap-routes.d.ts +0 -5
- package/dist/plugins/fusion-plugin-roadmap/src/routes/roadmap-routes.d.ts.map +0 -1
- package/dist/plugins/fusion-plugin-roadmap/src/routes/roadmap-routes.js +0 -361
- package/dist/plugins/fusion-plugin-roadmap/src/routes/roadmap-routes.js.map +0 -1
- package/dist/plugins/fusion-plugin-roadmap/src/routes/roadmap-routes.ts +0 -408
- package/dist/plugins/fusion-plugin-roadmap/src/routes/roadmap-suggestions.d.ts +0 -68
- package/dist/plugins/fusion-plugin-roadmap/src/routes/roadmap-suggestions.d.ts.map +0 -1
- package/dist/plugins/fusion-plugin-roadmap/src/routes/roadmap-suggestions.js +0 -300
- package/dist/plugins/fusion-plugin-roadmap/src/routes/roadmap-suggestions.js.map +0 -1
- package/dist/plugins/fusion-plugin-roadmap/src/routes/roadmap-suggestions.ts +0 -381
- package/dist/plugins/fusion-plugin-roadmap/src/server/index.d.ts +0 -3
- package/dist/plugins/fusion-plugin-roadmap/src/server/index.ts +0 -1
- package/dist/plugins/fusion-plugin-roadmap/src/store/__tests__/roadmap-handoff.test.ts +0 -445
- package/dist/plugins/fusion-plugin-roadmap/src/store/__tests__/roadmap-ordering.test.ts +0 -334
- package/dist/plugins/fusion-plugin-roadmap/src/store/__tests__/roadmap-store.test.ts +0 -1318
- package/dist/plugins/fusion-plugin-roadmap/src/store/roadmap-handoff.ts +0 -163
- package/dist/plugins/fusion-plugin-roadmap/src/store/roadmap-ordering.d.ts +0 -37
- package/dist/plugins/fusion-plugin-roadmap/src/store/roadmap-ordering.d.ts.map +0 -1
- package/dist/plugins/fusion-plugin-roadmap/src/store/roadmap-ordering.js +0 -188
- package/dist/plugins/fusion-plugin-roadmap/src/store/roadmap-ordering.js.map +0 -1
- package/dist/plugins/fusion-plugin-roadmap/src/store/roadmap-ordering.ts +0 -311
- package/dist/plugins/fusion-plugin-roadmap/src/store/roadmap-store.d.ts +0 -299
- package/dist/plugins/fusion-plugin-roadmap/src/store/roadmap-store.d.ts.map +0 -1
- package/dist/plugins/fusion-plugin-roadmap/src/store/roadmap-store.js +0 -765
- package/dist/plugins/fusion-plugin-roadmap/src/store/roadmap-store.js.map +0 -1
- package/dist/plugins/fusion-plugin-roadmap/src/store/roadmap-store.ts +0 -1001
|
@@ -0,0 +1,1672 @@
|
|
|
1
|
+
// ../plugin-sdk/src/index.ts
|
|
2
|
+
function definePlugin(plugin2) {
|
|
3
|
+
return plugin2;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
// ../../plugins/fusion-plugin-roadmap/src/store/roadmap-store.js
|
|
7
|
+
import { EventEmitter } from "node:events";
|
|
8
|
+
|
|
9
|
+
// ../../plugins/fusion-plugin-roadmap/src/store/roadmap-ordering.js
|
|
10
|
+
function compareOrderedEntities(a, b) {
|
|
11
|
+
if (a.orderIndex !== b.orderIndex) {
|
|
12
|
+
return a.orderIndex - b.orderIndex;
|
|
13
|
+
}
|
|
14
|
+
if (a.createdAt !== b.createdAt) {
|
|
15
|
+
return a.createdAt.localeCompare(b.createdAt);
|
|
16
|
+
}
|
|
17
|
+
return a.id.localeCompare(b.id);
|
|
18
|
+
}
|
|
19
|
+
function clampInsertionIndex(targetIndex, length) {
|
|
20
|
+
if (!Number.isFinite(targetIndex)) {
|
|
21
|
+
return length;
|
|
22
|
+
}
|
|
23
|
+
const normalized = Math.trunc(targetIndex);
|
|
24
|
+
if (normalized < 0) {
|
|
25
|
+
return 0;
|
|
26
|
+
}
|
|
27
|
+
if (normalized > length) {
|
|
28
|
+
return length;
|
|
29
|
+
}
|
|
30
|
+
return normalized;
|
|
31
|
+
}
|
|
32
|
+
function assertScopedRoadmapMilestones(milestones, roadmapId) {
|
|
33
|
+
for (const milestone of milestones) {
|
|
34
|
+
if (milestone.roadmapId !== roadmapId) {
|
|
35
|
+
throw new Error(`Milestone ${milestone.id} does not belong to roadmap ${roadmapId}`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
function assertScopedMilestoneFeatures(features, milestoneId) {
|
|
40
|
+
for (const feature of features) {
|
|
41
|
+
if (feature.milestoneId !== milestoneId) {
|
|
42
|
+
throw new Error(`Feature ${feature.id} does not belong to milestone ${milestoneId}`);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
function assertScopedMoveFeatures(features, fromMilestoneId, toMilestoneId) {
|
|
47
|
+
const validMilestoneIds = /* @__PURE__ */ new Set([fromMilestoneId, toMilestoneId]);
|
|
48
|
+
for (const feature of features) {
|
|
49
|
+
if (!validMilestoneIds.has(feature.milestoneId)) {
|
|
50
|
+
throw new Error(`Feature ${feature.id} is outside the affected milestone scope (${fromMilestoneId} \u2192 ${toMilestoneId})`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
function assertExactIdSet(entityLabel, actualIds, orderedIds) {
|
|
55
|
+
const requestedIds = /* @__PURE__ */ new Set();
|
|
56
|
+
for (const id of orderedIds) {
|
|
57
|
+
if (requestedIds.has(id)) {
|
|
58
|
+
throw new Error(`Duplicate ${entityLabel} id in requested order: ${id}`);
|
|
59
|
+
}
|
|
60
|
+
requestedIds.add(id);
|
|
61
|
+
}
|
|
62
|
+
if (actualIds.length !== orderedIds.length) {
|
|
63
|
+
throw new Error(`Expected ${actualIds.length} ${entityLabel} ids but received ${orderedIds.length}`);
|
|
64
|
+
}
|
|
65
|
+
const actualIdSet = new Set(actualIds);
|
|
66
|
+
for (const id of orderedIds) {
|
|
67
|
+
if (!actualIdSet.has(id)) {
|
|
68
|
+
throw new Error(`${capitalize(entityLabel)} ${id} not found in scoped list`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
for (const id of actualIds) {
|
|
72
|
+
if (!requestedIds.has(id)) {
|
|
73
|
+
throw new Error(`Missing ${entityLabel} id in requested order: ${id}`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
function capitalize(value) {
|
|
78
|
+
return value.charAt(0).toUpperCase() + value.slice(1);
|
|
79
|
+
}
|
|
80
|
+
function assignContiguousOrder(items) {
|
|
81
|
+
return items.map((item, orderIndex) => {
|
|
82
|
+
if (item.orderIndex === orderIndex) {
|
|
83
|
+
return { ...item };
|
|
84
|
+
}
|
|
85
|
+
return {
|
|
86
|
+
...item,
|
|
87
|
+
orderIndex
|
|
88
|
+
};
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
function normalizeRoadmapMilestoneOrder(milestones) {
|
|
92
|
+
if (milestones.length === 0) {
|
|
93
|
+
return [];
|
|
94
|
+
}
|
|
95
|
+
assertScopedRoadmapMilestones(milestones, milestones[0].roadmapId);
|
|
96
|
+
return assignContiguousOrder([...milestones].sort(compareOrderedEntities));
|
|
97
|
+
}
|
|
98
|
+
function applyRoadmapMilestoneReorder(milestones, input) {
|
|
99
|
+
assertScopedRoadmapMilestones(milestones, input.roadmapId);
|
|
100
|
+
const normalized = normalizeRoadmapMilestoneOrder(milestones);
|
|
101
|
+
const ids = normalized.map((milestone) => milestone.id);
|
|
102
|
+
assertExactIdSet("milestone", ids, input.orderedMilestoneIds);
|
|
103
|
+
const byId = new Map(normalized.map((milestone) => [milestone.id, milestone]));
|
|
104
|
+
return assignContiguousOrder(input.orderedMilestoneIds.map((id) => byId.get(id)));
|
|
105
|
+
}
|
|
106
|
+
function normalizeRoadmapFeatureOrder(features) {
|
|
107
|
+
if (features.length === 0) {
|
|
108
|
+
return [];
|
|
109
|
+
}
|
|
110
|
+
assertScopedMilestoneFeatures(features, features[0].milestoneId);
|
|
111
|
+
return assignContiguousOrder([...features].sort(compareOrderedEntities));
|
|
112
|
+
}
|
|
113
|
+
function applyRoadmapFeatureReorder(features, input) {
|
|
114
|
+
assertScopedMilestoneFeatures(features, input.milestoneId);
|
|
115
|
+
const normalized = normalizeRoadmapFeatureOrder(features);
|
|
116
|
+
const ids = normalized.map((feature) => feature.id);
|
|
117
|
+
assertExactIdSet("feature", ids, input.orderedFeatureIds);
|
|
118
|
+
const byId = new Map(normalized.map((feature) => [feature.id, feature]));
|
|
119
|
+
return assignContiguousOrder(input.orderedFeatureIds.map((id) => byId.get(id)));
|
|
120
|
+
}
|
|
121
|
+
function moveRoadmapFeature(features, input) {
|
|
122
|
+
assertScopedMoveFeatures(features, input.fromMilestoneId, input.toMilestoneId);
|
|
123
|
+
const existingFeature = features.find((feature) => feature.id === input.featureId);
|
|
124
|
+
if (!existingFeature) {
|
|
125
|
+
throw new Error(`Feature ${input.featureId} not found in affected milestone scope`);
|
|
126
|
+
}
|
|
127
|
+
if (existingFeature.milestoneId !== input.fromMilestoneId) {
|
|
128
|
+
throw new Error(`Feature ${input.featureId} does not belong to milestone ${input.fromMilestoneId}`);
|
|
129
|
+
}
|
|
130
|
+
const sourceFeatures = normalizeRoadmapFeatureOrder(features.filter((feature) => feature.milestoneId === input.fromMilestoneId));
|
|
131
|
+
const sourceWithoutFeature = sourceFeatures.filter((feature) => feature.id !== input.featureId);
|
|
132
|
+
if (input.fromMilestoneId === input.toMilestoneId) {
|
|
133
|
+
const insertionIndex2 = clampInsertionIndex(input.targetOrderIndex, sourceWithoutFeature.length);
|
|
134
|
+
const reordered = [...sourceWithoutFeature];
|
|
135
|
+
reordered.splice(insertionIndex2, 0, {
|
|
136
|
+
...existingFeature,
|
|
137
|
+
milestoneId: input.toMilestoneId,
|
|
138
|
+
orderIndex: insertionIndex2
|
|
139
|
+
});
|
|
140
|
+
const normalized = assignContiguousOrder(reordered);
|
|
141
|
+
const movedFeature2 = normalized.find((feature) => feature.id === input.featureId);
|
|
142
|
+
return {
|
|
143
|
+
movedFeature: movedFeature2,
|
|
144
|
+
affectedFeatures: normalized,
|
|
145
|
+
sourceMilestoneFeatures: normalized,
|
|
146
|
+
targetMilestoneFeatures: normalized
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
const targetFeatures = normalizeRoadmapFeatureOrder(features.filter((feature) => feature.milestoneId === input.toMilestoneId));
|
|
150
|
+
const insertionIndex = clampInsertionIndex(input.targetOrderIndex, targetFeatures.length);
|
|
151
|
+
const targetWithInsertedFeature = [...targetFeatures];
|
|
152
|
+
targetWithInsertedFeature.splice(insertionIndex, 0, {
|
|
153
|
+
...existingFeature,
|
|
154
|
+
milestoneId: input.toMilestoneId,
|
|
155
|
+
orderIndex: insertionIndex
|
|
156
|
+
});
|
|
157
|
+
const normalizedSource = assignContiguousOrder(sourceWithoutFeature);
|
|
158
|
+
const normalizedTarget = assignContiguousOrder(targetWithInsertedFeature);
|
|
159
|
+
const movedFeature = normalizedTarget.find((feature) => feature.id === input.featureId);
|
|
160
|
+
return {
|
|
161
|
+
movedFeature,
|
|
162
|
+
affectedFeatures: [...normalizedSource, ...normalizedTarget],
|
|
163
|
+
sourceMilestoneFeatures: normalizedSource,
|
|
164
|
+
targetMilestoneFeatures: normalizedTarget
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// ../../plugins/fusion-plugin-roadmap/src/store/roadmap-store.js
|
|
169
|
+
var RoadmapStore = class extends EventEmitter {
|
|
170
|
+
db;
|
|
171
|
+
/**
|
|
172
|
+
* Creates a new RoadmapStore instance.
|
|
173
|
+
*
|
|
174
|
+
* @param db - Shared Database instance (same instance used by TaskStore)
|
|
175
|
+
*/
|
|
176
|
+
constructor(db) {
|
|
177
|
+
super();
|
|
178
|
+
this.db = db;
|
|
179
|
+
this.setMaxListeners(50);
|
|
180
|
+
this.ensureSchema();
|
|
181
|
+
}
|
|
182
|
+
ensureSchema() {
|
|
183
|
+
this.db.exec(`
|
|
184
|
+
CREATE TABLE IF NOT EXISTS roadmaps (
|
|
185
|
+
id TEXT PRIMARY KEY,
|
|
186
|
+
title TEXT NOT NULL,
|
|
187
|
+
description TEXT,
|
|
188
|
+
createdAt TEXT NOT NULL,
|
|
189
|
+
updatedAt TEXT NOT NULL
|
|
190
|
+
);
|
|
191
|
+
|
|
192
|
+
CREATE TABLE IF NOT EXISTS roadmap_milestones (
|
|
193
|
+
id TEXT PRIMARY KEY,
|
|
194
|
+
roadmapId TEXT NOT NULL,
|
|
195
|
+
title TEXT NOT NULL,
|
|
196
|
+
description TEXT,
|
|
197
|
+
orderIndex INTEGER NOT NULL,
|
|
198
|
+
createdAt TEXT NOT NULL,
|
|
199
|
+
updatedAt TEXT NOT NULL,
|
|
200
|
+
FOREIGN KEY (roadmapId) REFERENCES roadmaps(id) ON DELETE CASCADE
|
|
201
|
+
);
|
|
202
|
+
|
|
203
|
+
CREATE TABLE IF NOT EXISTS roadmap_features (
|
|
204
|
+
id TEXT PRIMARY KEY,
|
|
205
|
+
milestoneId TEXT NOT NULL,
|
|
206
|
+
title TEXT NOT NULL,
|
|
207
|
+
description TEXT,
|
|
208
|
+
orderIndex INTEGER NOT NULL,
|
|
209
|
+
createdAt TEXT NOT NULL,
|
|
210
|
+
updatedAt TEXT NOT NULL,
|
|
211
|
+
FOREIGN KEY (milestoneId) REFERENCES roadmap_milestones(id) ON DELETE CASCADE
|
|
212
|
+
);
|
|
213
|
+
|
|
214
|
+
CREATE INDEX IF NOT EXISTS idxRoadmapMilestonesRoadmapOrder
|
|
215
|
+
ON roadmap_milestones(roadmapId, orderIndex, createdAt, id);
|
|
216
|
+
|
|
217
|
+
CREATE INDEX IF NOT EXISTS idxRoadmapFeaturesMilestoneOrder
|
|
218
|
+
ON roadmap_features(milestoneId, orderIndex, createdAt, id);
|
|
219
|
+
`);
|
|
220
|
+
}
|
|
221
|
+
// ── ID Generators ───────────────────────────────────────────────────
|
|
222
|
+
generateRoadmapId() {
|
|
223
|
+
const timestamp = Date.now();
|
|
224
|
+
const random = Math.random().toString(36).substring(2, 6).toUpperCase();
|
|
225
|
+
return `RM-${timestamp.toString(36).toUpperCase()}-${random}`;
|
|
226
|
+
}
|
|
227
|
+
generateMilestoneId() {
|
|
228
|
+
const timestamp = Date.now();
|
|
229
|
+
const random = Math.random().toString(36).substring(2, 6).toUpperCase();
|
|
230
|
+
return `RMS-${timestamp.toString(36).toUpperCase()}-${random}`;
|
|
231
|
+
}
|
|
232
|
+
generateFeatureId() {
|
|
233
|
+
const timestamp = Date.now();
|
|
234
|
+
const random = Math.random().toString(36).substring(2, 6).toUpperCase();
|
|
235
|
+
return `RF-${timestamp.toString(36).toUpperCase()}-${random}`;
|
|
236
|
+
}
|
|
237
|
+
// ── Row-to-Object Converters ───────────────────────────────────────
|
|
238
|
+
rowToRoadmap(row) {
|
|
239
|
+
return {
|
|
240
|
+
id: row.id,
|
|
241
|
+
title: row.title,
|
|
242
|
+
description: row.description || void 0,
|
|
243
|
+
createdAt: row.createdAt,
|
|
244
|
+
updatedAt: row.updatedAt
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
rowToMilestone(row) {
|
|
248
|
+
return {
|
|
249
|
+
id: row.id,
|
|
250
|
+
roadmapId: row.roadmapId,
|
|
251
|
+
title: row.title,
|
|
252
|
+
description: row.description || void 0,
|
|
253
|
+
orderIndex: row.orderIndex,
|
|
254
|
+
createdAt: row.createdAt,
|
|
255
|
+
updatedAt: row.updatedAt
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
rowToFeature(row) {
|
|
259
|
+
return {
|
|
260
|
+
id: row.id,
|
|
261
|
+
milestoneId: row.milestoneId,
|
|
262
|
+
title: row.title,
|
|
263
|
+
description: row.description || void 0,
|
|
264
|
+
orderIndex: row.orderIndex,
|
|
265
|
+
createdAt: row.createdAt,
|
|
266
|
+
updatedAt: row.updatedAt
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
// ── Roadmap CRUD ─────────────────────────────────────────────────
|
|
270
|
+
/**
|
|
271
|
+
* Create a new roadmap.
|
|
272
|
+
*
|
|
273
|
+
* @param input - Roadmap creation input
|
|
274
|
+
* @returns The created roadmap
|
|
275
|
+
*/
|
|
276
|
+
createRoadmap(input) {
|
|
277
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
278
|
+
const id = this.generateRoadmapId();
|
|
279
|
+
const roadmap = {
|
|
280
|
+
id,
|
|
281
|
+
title: input.title,
|
|
282
|
+
description: input.description,
|
|
283
|
+
createdAt: now,
|
|
284
|
+
updatedAt: now
|
|
285
|
+
};
|
|
286
|
+
this.db.prepare(`
|
|
287
|
+
INSERT INTO roadmaps (id, title, description, createdAt, updatedAt)
|
|
288
|
+
VALUES (?, ?, ?, ?, ?)
|
|
289
|
+
`).run(roadmap.id, roadmap.title, roadmap.description ?? null, roadmap.createdAt, roadmap.updatedAt);
|
|
290
|
+
this.db.bumpLastModified();
|
|
291
|
+
this.emit("roadmap:created", roadmap);
|
|
292
|
+
return roadmap;
|
|
293
|
+
}
|
|
294
|
+
/**
|
|
295
|
+
* Get a roadmap by ID.
|
|
296
|
+
*
|
|
297
|
+
* @param id - Roadmap ID
|
|
298
|
+
* @returns The roadmap, or undefined if not found
|
|
299
|
+
*/
|
|
300
|
+
getRoadmap(id) {
|
|
301
|
+
const row = this.db.prepare("SELECT * FROM roadmaps WHERE id = ?").get(id);
|
|
302
|
+
if (!row)
|
|
303
|
+
return void 0;
|
|
304
|
+
return this.rowToRoadmap(row);
|
|
305
|
+
}
|
|
306
|
+
/**
|
|
307
|
+
* List all roadmaps, ordered by creation date (newest first).
|
|
308
|
+
*
|
|
309
|
+
* @returns Array of roadmaps
|
|
310
|
+
*/
|
|
311
|
+
listRoadmaps() {
|
|
312
|
+
const rows = this.db.prepare("SELECT * FROM roadmaps ORDER BY createdAt DESC").all();
|
|
313
|
+
return rows.map((row) => this.rowToRoadmap(row));
|
|
314
|
+
}
|
|
315
|
+
/**
|
|
316
|
+
* Update a roadmap.
|
|
317
|
+
*
|
|
318
|
+
* @param id - Roadmap ID
|
|
319
|
+
* @param updates - Partial roadmap updates
|
|
320
|
+
* @returns The updated roadmap
|
|
321
|
+
* @throws Error if roadmap not found
|
|
322
|
+
*/
|
|
323
|
+
updateRoadmap(id, updates) {
|
|
324
|
+
const roadmap = this.getRoadmap(id);
|
|
325
|
+
if (!roadmap) {
|
|
326
|
+
throw new Error(`Roadmap ${id} not found`);
|
|
327
|
+
}
|
|
328
|
+
const updated = {
|
|
329
|
+
...roadmap,
|
|
330
|
+
...updates,
|
|
331
|
+
id,
|
|
332
|
+
// Prevent changing ID
|
|
333
|
+
createdAt: roadmap.createdAt,
|
|
334
|
+
// Prevent changing creation time
|
|
335
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
336
|
+
};
|
|
337
|
+
this.db.prepare(`
|
|
338
|
+
UPDATE roadmaps SET
|
|
339
|
+
title = ?,
|
|
340
|
+
description = ?,
|
|
341
|
+
updatedAt = ?
|
|
342
|
+
WHERE id = ?
|
|
343
|
+
`).run(updated.title, updated.description ?? null, updated.updatedAt, updated.id);
|
|
344
|
+
this.db.bumpLastModified();
|
|
345
|
+
this.emit("roadmap:updated", updated);
|
|
346
|
+
return updated;
|
|
347
|
+
}
|
|
348
|
+
/**
|
|
349
|
+
* Delete a roadmap and all its milestones/features (cascading).
|
|
350
|
+
*
|
|
351
|
+
* @param id - Roadmap ID
|
|
352
|
+
* @throws Error if roadmap not found
|
|
353
|
+
*/
|
|
354
|
+
deleteRoadmap(id) {
|
|
355
|
+
const roadmap = this.getRoadmap(id);
|
|
356
|
+
if (!roadmap) {
|
|
357
|
+
throw new Error(`Roadmap ${id} not found`);
|
|
358
|
+
}
|
|
359
|
+
this.db.prepare("DELETE FROM roadmaps WHERE id = ?").run(id);
|
|
360
|
+
this.db.bumpLastModified();
|
|
361
|
+
this.emit("roadmap:deleted", id);
|
|
362
|
+
}
|
|
363
|
+
// ── Milestone CRUD ────────────────────────────────────────────────
|
|
364
|
+
/**
|
|
365
|
+
* Add a milestone to a roadmap.
|
|
366
|
+
* Automatically computes the orderIndex (max + 1).
|
|
367
|
+
*
|
|
368
|
+
* @param roadmapId - Parent roadmap ID
|
|
369
|
+
* @param input - Milestone creation input
|
|
370
|
+
* @returns The created milestone
|
|
371
|
+
* @throws Error if roadmap not found
|
|
372
|
+
*/
|
|
373
|
+
createMilestone(roadmapId, input) {
|
|
374
|
+
const roadmap = this.getRoadmap(roadmapId);
|
|
375
|
+
if (!roadmap) {
|
|
376
|
+
throw new Error(`Roadmap ${roadmapId} not found`);
|
|
377
|
+
}
|
|
378
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
379
|
+
const id = this.generateMilestoneId();
|
|
380
|
+
const existingMilestones = this.listMilestones(roadmapId);
|
|
381
|
+
const orderIndex = existingMilestones.length > 0 ? Math.max(...existingMilestones.map((m) => m.orderIndex)) + 1 : 0;
|
|
382
|
+
const milestone = {
|
|
383
|
+
id,
|
|
384
|
+
roadmapId,
|
|
385
|
+
title: input.title,
|
|
386
|
+
description: input.description,
|
|
387
|
+
orderIndex,
|
|
388
|
+
createdAt: now,
|
|
389
|
+
updatedAt: now
|
|
390
|
+
};
|
|
391
|
+
this.db.prepare(`
|
|
392
|
+
INSERT INTO roadmap_milestones (id, roadmapId, title, description, orderIndex, createdAt, updatedAt)
|
|
393
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
394
|
+
`).run(milestone.id, milestone.roadmapId, milestone.title, milestone.description ?? null, milestone.orderIndex, milestone.createdAt, milestone.updatedAt);
|
|
395
|
+
this.db.bumpLastModified();
|
|
396
|
+
this.emit("milestone:created", milestone);
|
|
397
|
+
return milestone;
|
|
398
|
+
}
|
|
399
|
+
/**
|
|
400
|
+
* Get a milestone by ID.
|
|
401
|
+
*
|
|
402
|
+
* @param id - Milestone ID
|
|
403
|
+
* @returns The milestone, or undefined if not found
|
|
404
|
+
*/
|
|
405
|
+
getMilestone(id) {
|
|
406
|
+
const row = this.db.prepare("SELECT * FROM roadmap_milestones WHERE id = ?").get(id);
|
|
407
|
+
if (!row)
|
|
408
|
+
return void 0;
|
|
409
|
+
return this.rowToMilestone(row);
|
|
410
|
+
}
|
|
411
|
+
/**
|
|
412
|
+
* List milestones for a roadmap, ordered deterministically.
|
|
413
|
+
*
|
|
414
|
+
* Uses deterministic ordering: ORDER BY orderIndex ASC, createdAt ASC, id ASC
|
|
415
|
+
* to ensure consistent results when stored order data is incomplete or conflicting.
|
|
416
|
+
*
|
|
417
|
+
* @param roadmapId - Roadmap ID
|
|
418
|
+
* @returns Array of milestones in deterministic order
|
|
419
|
+
*/
|
|
420
|
+
listMilestones(roadmapId) {
|
|
421
|
+
const rows = this.db.prepare("SELECT * FROM roadmap_milestones WHERE roadmapId = ? ORDER BY orderIndex ASC, createdAt ASC, id ASC").all(roadmapId);
|
|
422
|
+
return rows.map((row) => this.rowToMilestone(row));
|
|
423
|
+
}
|
|
424
|
+
/**
|
|
425
|
+
* Update a milestone.
|
|
426
|
+
*
|
|
427
|
+
* @param id - Milestone ID
|
|
428
|
+
* @param updates - Partial milestone updates
|
|
429
|
+
* @returns The updated milestone
|
|
430
|
+
* @throws Error if milestone not found
|
|
431
|
+
*/
|
|
432
|
+
updateMilestone(id, updates) {
|
|
433
|
+
const milestone = this.getMilestone(id);
|
|
434
|
+
if (!milestone) {
|
|
435
|
+
throw new Error(`Milestone ${id} not found`);
|
|
436
|
+
}
|
|
437
|
+
const updated = {
|
|
438
|
+
...milestone,
|
|
439
|
+
...updates,
|
|
440
|
+
id,
|
|
441
|
+
// Prevent changing ID
|
|
442
|
+
roadmapId: milestone.roadmapId,
|
|
443
|
+
// Prevent moving to different roadmap
|
|
444
|
+
createdAt: milestone.createdAt,
|
|
445
|
+
// Prevent changing creation time
|
|
446
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
447
|
+
};
|
|
448
|
+
this.db.prepare(`
|
|
449
|
+
UPDATE roadmap_milestones SET
|
|
450
|
+
title = ?,
|
|
451
|
+
description = ?,
|
|
452
|
+
updatedAt = ?
|
|
453
|
+
WHERE id = ?
|
|
454
|
+
`).run(updated.title, updated.description ?? null, updated.updatedAt, updated.id);
|
|
455
|
+
this.db.bumpLastModified();
|
|
456
|
+
this.emit("milestone:updated", updated);
|
|
457
|
+
return updated;
|
|
458
|
+
}
|
|
459
|
+
/**
|
|
460
|
+
* Delete a milestone and all its features (cascading).
|
|
461
|
+
*
|
|
462
|
+
* @param id - Milestone ID
|
|
463
|
+
* @throws Error if milestone not found
|
|
464
|
+
*/
|
|
465
|
+
deleteMilestone(id) {
|
|
466
|
+
const milestone = this.getMilestone(id);
|
|
467
|
+
if (!milestone) {
|
|
468
|
+
throw new Error(`Milestone ${id} not found`);
|
|
469
|
+
}
|
|
470
|
+
this.db.prepare("DELETE FROM roadmap_milestones WHERE id = ?").run(id);
|
|
471
|
+
this.db.bumpLastModified();
|
|
472
|
+
this.emit("milestone:deleted", id);
|
|
473
|
+
}
|
|
474
|
+
// ── Feature CRUD ─────────────────────────────────────────────────
|
|
475
|
+
/**
|
|
476
|
+
* Add a feature to a milestone.
|
|
477
|
+
* Automatically computes the orderIndex (max + 1).
|
|
478
|
+
*
|
|
479
|
+
* @param milestoneId - Parent milestone ID
|
|
480
|
+
* @param input - Feature creation input
|
|
481
|
+
* @returns The created feature
|
|
482
|
+
* @throws Error if milestone not found
|
|
483
|
+
*/
|
|
484
|
+
createFeature(milestoneId, input) {
|
|
485
|
+
const milestone = this.getMilestone(milestoneId);
|
|
486
|
+
if (!milestone) {
|
|
487
|
+
throw new Error(`Milestone ${milestoneId} not found`);
|
|
488
|
+
}
|
|
489
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
490
|
+
const id = this.generateFeatureId();
|
|
491
|
+
const existingFeatures = this.listFeatures(milestoneId);
|
|
492
|
+
const orderIndex = existingFeatures.length > 0 ? Math.max(...existingFeatures.map((f) => f.orderIndex)) + 1 : 0;
|
|
493
|
+
const feature = {
|
|
494
|
+
id,
|
|
495
|
+
milestoneId,
|
|
496
|
+
title: input.title,
|
|
497
|
+
description: input.description,
|
|
498
|
+
orderIndex,
|
|
499
|
+
createdAt: now,
|
|
500
|
+
updatedAt: now
|
|
501
|
+
};
|
|
502
|
+
this.db.prepare(`
|
|
503
|
+
INSERT INTO roadmap_features (id, milestoneId, title, description, orderIndex, createdAt, updatedAt)
|
|
504
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
505
|
+
`).run(feature.id, feature.milestoneId, feature.title, feature.description ?? null, feature.orderIndex, feature.createdAt, feature.updatedAt);
|
|
506
|
+
this.db.bumpLastModified();
|
|
507
|
+
this.emit("feature:created", feature);
|
|
508
|
+
return feature;
|
|
509
|
+
}
|
|
510
|
+
/**
|
|
511
|
+
* Get a feature by ID.
|
|
512
|
+
*
|
|
513
|
+
* @param id - Feature ID
|
|
514
|
+
* @returns The feature, or undefined if not found
|
|
515
|
+
*/
|
|
516
|
+
getFeature(id) {
|
|
517
|
+
const row = this.db.prepare("SELECT * FROM roadmap_features WHERE id = ?").get(id);
|
|
518
|
+
if (!row)
|
|
519
|
+
return void 0;
|
|
520
|
+
return this.rowToFeature(row);
|
|
521
|
+
}
|
|
522
|
+
/**
|
|
523
|
+
* List features for a milestone, ordered deterministically.
|
|
524
|
+
*
|
|
525
|
+
* Uses deterministic ordering: ORDER BY orderIndex ASC, createdAt ASC, id ASC
|
|
526
|
+
* to ensure consistent results when stored order data is incomplete or conflicting.
|
|
527
|
+
*
|
|
528
|
+
* @param milestoneId - Milestone ID
|
|
529
|
+
* @returns Array of features in deterministic order
|
|
530
|
+
*/
|
|
531
|
+
listFeatures(milestoneId) {
|
|
532
|
+
const rows = this.db.prepare("SELECT * FROM roadmap_features WHERE milestoneId = ? ORDER BY orderIndex ASC, createdAt ASC, id ASC").all(milestoneId);
|
|
533
|
+
return rows.map((row) => this.rowToFeature(row));
|
|
534
|
+
}
|
|
535
|
+
/**
|
|
536
|
+
* Update a feature.
|
|
537
|
+
*
|
|
538
|
+
* @param id - Feature ID
|
|
539
|
+
* @param updates - Partial feature updates
|
|
540
|
+
* @returns The updated feature
|
|
541
|
+
* @throws Error if feature not found
|
|
542
|
+
*/
|
|
543
|
+
updateFeature(id, updates) {
|
|
544
|
+
const feature = this.getFeature(id);
|
|
545
|
+
if (!feature) {
|
|
546
|
+
throw new Error(`Feature ${id} not found`);
|
|
547
|
+
}
|
|
548
|
+
const updated = {
|
|
549
|
+
...feature,
|
|
550
|
+
...updates,
|
|
551
|
+
id,
|
|
552
|
+
// Prevent changing ID
|
|
553
|
+
milestoneId: feature.milestoneId,
|
|
554
|
+
// Prevent moving via update (use moveFeature instead)
|
|
555
|
+
createdAt: feature.createdAt,
|
|
556
|
+
// Prevent changing creation time
|
|
557
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
558
|
+
};
|
|
559
|
+
this.db.prepare(`
|
|
560
|
+
UPDATE roadmap_features SET
|
|
561
|
+
title = ?,
|
|
562
|
+
description = ?,
|
|
563
|
+
updatedAt = ?
|
|
564
|
+
WHERE id = ?
|
|
565
|
+
`).run(updated.title, updated.description ?? null, updated.updatedAt, updated.id);
|
|
566
|
+
this.db.bumpLastModified();
|
|
567
|
+
this.emit("feature:updated", updated);
|
|
568
|
+
return updated;
|
|
569
|
+
}
|
|
570
|
+
/**
|
|
571
|
+
* Delete a feature.
|
|
572
|
+
*
|
|
573
|
+
* @param id - Feature ID
|
|
574
|
+
* @throws Error if feature not found
|
|
575
|
+
*/
|
|
576
|
+
deleteFeature(id) {
|
|
577
|
+
const feature = this.getFeature(id);
|
|
578
|
+
if (!feature) {
|
|
579
|
+
throw new Error(`Feature ${id} not found`);
|
|
580
|
+
}
|
|
581
|
+
this.db.prepare("DELETE FROM roadmap_features WHERE id = ?").run(id);
|
|
582
|
+
this.db.bumpLastModified();
|
|
583
|
+
this.emit("feature:deleted", feature);
|
|
584
|
+
}
|
|
585
|
+
// ── Reorder Operations ────────────────────────────────────────────
|
|
586
|
+
/**
|
|
587
|
+
* Reorder milestones within a roadmap.
|
|
588
|
+
*
|
|
589
|
+
* Applies an explicit reorder input and persists the full normalized order.
|
|
590
|
+
* The input must contain all milestone IDs exactly once.
|
|
591
|
+
*
|
|
592
|
+
* @param input - Reorder input with complete milestone ID list
|
|
593
|
+
* @returns The reordered milestones in their new order
|
|
594
|
+
* @throws Error if milestone set is incomplete, duplicate, or not found
|
|
595
|
+
*/
|
|
596
|
+
reorderMilestones(input) {
|
|
597
|
+
const roadmap = this.getRoadmap(input.roadmapId);
|
|
598
|
+
if (!roadmap) {
|
|
599
|
+
throw new Error(`Roadmap ${input.roadmapId} not found`);
|
|
600
|
+
}
|
|
601
|
+
const milestones = this.listMilestones(input.roadmapId);
|
|
602
|
+
const reordered = applyRoadmapMilestoneReorder(milestones, input);
|
|
603
|
+
this.db.transaction(() => {
|
|
604
|
+
for (const milestone of reordered) {
|
|
605
|
+
this.db.prepare(`
|
|
606
|
+
UPDATE roadmap_milestones SET orderIndex = ?, updatedAt = ? WHERE id = ?
|
|
607
|
+
`).run(milestone.orderIndex, (/* @__PURE__ */ new Date()).toISOString(), milestone.id);
|
|
608
|
+
}
|
|
609
|
+
});
|
|
610
|
+
this.db.bumpLastModified();
|
|
611
|
+
this.emit("milestone:reordered", { roadmapId: input.roadmapId, milestones: reordered });
|
|
612
|
+
return reordered;
|
|
613
|
+
}
|
|
614
|
+
/**
|
|
615
|
+
* Reorder features within a milestone.
|
|
616
|
+
*
|
|
617
|
+
* Applies an explicit reorder input and persists the full normalized order.
|
|
618
|
+
* The input must contain all feature IDs for the milestone exactly once.
|
|
619
|
+
*
|
|
620
|
+
* @param input - Reorder input with complete feature ID list
|
|
621
|
+
* @returns The reordered features in their new order
|
|
622
|
+
* @throws Error if feature set is incomplete, duplicate, or not found
|
|
623
|
+
*/
|
|
624
|
+
reorderFeatures(input) {
|
|
625
|
+
const milestone = this.getMilestone(input.milestoneId);
|
|
626
|
+
if (!milestone) {
|
|
627
|
+
throw new Error(`Milestone ${input.milestoneId} not found`);
|
|
628
|
+
}
|
|
629
|
+
if (milestone.roadmapId !== input.roadmapId) {
|
|
630
|
+
throw new Error(`Milestone ${input.milestoneId} does not belong to roadmap ${input.roadmapId}`);
|
|
631
|
+
}
|
|
632
|
+
const features = this.listFeatures(input.milestoneId);
|
|
633
|
+
const reordered = applyRoadmapFeatureReorder(features, input);
|
|
634
|
+
this.db.transaction(() => {
|
|
635
|
+
for (const feature of reordered) {
|
|
636
|
+
this.db.prepare(`
|
|
637
|
+
UPDATE roadmap_features SET orderIndex = ?, updatedAt = ? WHERE id = ?
|
|
638
|
+
`).run(feature.orderIndex, (/* @__PURE__ */ new Date()).toISOString(), feature.id);
|
|
639
|
+
}
|
|
640
|
+
});
|
|
641
|
+
this.db.bumpLastModified();
|
|
642
|
+
this.emit("feature:reordered", { milestoneId: input.milestoneId, features: reordered });
|
|
643
|
+
return reordered;
|
|
644
|
+
}
|
|
645
|
+
/**
|
|
646
|
+
* Move a feature, including cross-milestone moves.
|
|
647
|
+
*
|
|
648
|
+
* Atomically renumbers both the source and destination milestone scopes.
|
|
649
|
+
*
|
|
650
|
+
* @param input - Move input with source/destination milestone info
|
|
651
|
+
* @returns The moved feature and both affected milestone feature lists
|
|
652
|
+
* @throws Error if feature or milestone not found, or scope validation fails
|
|
653
|
+
*/
|
|
654
|
+
moveFeature(input) {
|
|
655
|
+
const roadmap = this.getRoadmap(input.roadmapId);
|
|
656
|
+
if (!roadmap) {
|
|
657
|
+
throw new Error(`Roadmap ${input.roadmapId} not found`);
|
|
658
|
+
}
|
|
659
|
+
const fromMilestone = this.getMilestone(input.fromMilestoneId);
|
|
660
|
+
const toMilestone = this.getMilestone(input.toMilestoneId);
|
|
661
|
+
if (!fromMilestone) {
|
|
662
|
+
throw new Error(`Source milestone ${input.fromMilestoneId} not found`);
|
|
663
|
+
}
|
|
664
|
+
if (!toMilestone) {
|
|
665
|
+
throw new Error(`Destination milestone ${input.toMilestoneId} not found`);
|
|
666
|
+
}
|
|
667
|
+
if (fromMilestone.roadmapId !== input.roadmapId) {
|
|
668
|
+
throw new Error(`Source milestone ${input.fromMilestoneId} does not belong to roadmap ${input.roadmapId}`);
|
|
669
|
+
}
|
|
670
|
+
if (toMilestone.roadmapId !== input.roadmapId) {
|
|
671
|
+
throw new Error(`Destination milestone ${input.toMilestoneId} does not belong to roadmap ${input.roadmapId}`);
|
|
672
|
+
}
|
|
673
|
+
const sourceFeatures = this.listFeatures(input.fromMilestoneId);
|
|
674
|
+
const targetFeatures = this.listFeatures(input.toMilestoneId);
|
|
675
|
+
const allFeatures = input.fromMilestoneId === input.toMilestoneId ? sourceFeatures : [...sourceFeatures, ...targetFeatures];
|
|
676
|
+
const result = moveRoadmapFeature(allFeatures, input);
|
|
677
|
+
this.db.transaction(() => {
|
|
678
|
+
for (const feature of result.affectedFeatures) {
|
|
679
|
+
this.db.prepare(`
|
|
680
|
+
UPDATE roadmap_features SET milestoneId = ?, orderIndex = ?, updatedAt = ? WHERE id = ?
|
|
681
|
+
`).run(feature.milestoneId, feature.orderIndex, (/* @__PURE__ */ new Date()).toISOString(), feature.id);
|
|
682
|
+
}
|
|
683
|
+
});
|
|
684
|
+
this.db.bumpLastModified();
|
|
685
|
+
this.emit("feature:moved", {
|
|
686
|
+
feature: result.movedFeature,
|
|
687
|
+
fromMilestoneId: input.fromMilestoneId,
|
|
688
|
+
toMilestoneId: input.toMilestoneId
|
|
689
|
+
});
|
|
690
|
+
return {
|
|
691
|
+
movedFeature: result.movedFeature,
|
|
692
|
+
sourceMilestoneFeatures: result.sourceMilestoneFeatures,
|
|
693
|
+
targetMilestoneFeatures: result.targetMilestoneFeatures
|
|
694
|
+
};
|
|
695
|
+
}
|
|
696
|
+
// ── Hierarchy Operations ───────────────────────────────────────────
|
|
697
|
+
/**
|
|
698
|
+
* Get a milestone with all of its features in deterministic order.
|
|
699
|
+
*
|
|
700
|
+
* @param id - Milestone ID
|
|
701
|
+
* @returns The milestone with features, or undefined if not found
|
|
702
|
+
*/
|
|
703
|
+
getMilestoneWithFeatures(id) {
|
|
704
|
+
const milestone = this.getMilestone(id);
|
|
705
|
+
if (!milestone)
|
|
706
|
+
return void 0;
|
|
707
|
+
return {
|
|
708
|
+
...milestone,
|
|
709
|
+
features: this.listFeatures(id)
|
|
710
|
+
};
|
|
711
|
+
}
|
|
712
|
+
/**
|
|
713
|
+
* Get a roadmap with its full hierarchy (milestones → features).
|
|
714
|
+
*
|
|
715
|
+
* @param id - Roadmap ID
|
|
716
|
+
* @returns The roadmap with hierarchy, or undefined if not found
|
|
717
|
+
*/
|
|
718
|
+
getRoadmapWithHierarchy(id) {
|
|
719
|
+
const roadmap = this.getRoadmap(id);
|
|
720
|
+
if (!roadmap)
|
|
721
|
+
return void 0;
|
|
722
|
+
return {
|
|
723
|
+
...roadmap,
|
|
724
|
+
milestones: this.listMilestones(id).map((milestone) => ({
|
|
725
|
+
...milestone,
|
|
726
|
+
features: this.listFeatures(milestone.id)
|
|
727
|
+
}))
|
|
728
|
+
};
|
|
729
|
+
}
|
|
730
|
+
// ── Export / Handoff Operations ────────────────────────────────────
|
|
731
|
+
/**
|
|
732
|
+
* Get a flat export bundle for a roadmap.
|
|
733
|
+
*
|
|
734
|
+
* Returns all roadmap data in a flat structure suitable for persistence,
|
|
735
|
+
* APIs, import/export, and sync jobs. Entities are separated so downstream
|
|
736
|
+
* persistence layers can upsert by table/collection.
|
|
737
|
+
*
|
|
738
|
+
* @param roadmapId - Roadmap ID
|
|
739
|
+
* @returns The export bundle with ordered entities
|
|
740
|
+
* @throws Error if roadmap not found
|
|
741
|
+
*/
|
|
742
|
+
getRoadmapExport(roadmapId) {
|
|
743
|
+
const roadmap = this.getRoadmap(roadmapId);
|
|
744
|
+
if (!roadmap) {
|
|
745
|
+
throw new Error(`Roadmap ${roadmapId} not found`);
|
|
746
|
+
}
|
|
747
|
+
const milestones = this.listMilestones(roadmapId);
|
|
748
|
+
const allFeatures = [];
|
|
749
|
+
for (const milestone of milestones) {
|
|
750
|
+
const features = this.listFeatures(milestone.id);
|
|
751
|
+
allFeatures.push(...features);
|
|
752
|
+
}
|
|
753
|
+
return {
|
|
754
|
+
roadmap,
|
|
755
|
+
milestones,
|
|
756
|
+
features: allFeatures
|
|
757
|
+
};
|
|
758
|
+
}
|
|
759
|
+
/**
|
|
760
|
+
* Get a mission planning handoff payload for a roadmap.
|
|
761
|
+
*
|
|
762
|
+
* Converts the roadmap into a mission planning structure while preserving
|
|
763
|
+
* source IDs and deterministic order. Does not couple to MissionStore internals.
|
|
764
|
+
*
|
|
765
|
+
* @param roadmapId - Roadmap ID
|
|
766
|
+
* @returns The mission planning handoff payload
|
|
767
|
+
* @throws Error if roadmap not found
|
|
768
|
+
*/
|
|
769
|
+
getRoadmapMissionHandoff(roadmapId) {
|
|
770
|
+
const roadmap = this.getRoadmap(roadmapId);
|
|
771
|
+
if (!roadmap) {
|
|
772
|
+
throw new Error(`Roadmap ${roadmapId} not found`);
|
|
773
|
+
}
|
|
774
|
+
const milestones = this.listMilestones(roadmapId);
|
|
775
|
+
return {
|
|
776
|
+
sourceRoadmapId: roadmap.id,
|
|
777
|
+
title: roadmap.title,
|
|
778
|
+
description: roadmap.description,
|
|
779
|
+
milestones: milestones.map((milestone) => {
|
|
780
|
+
const features = this.listFeatures(milestone.id);
|
|
781
|
+
return {
|
|
782
|
+
sourceMilestoneId: milestone.id,
|
|
783
|
+
title: milestone.title,
|
|
784
|
+
description: milestone.description,
|
|
785
|
+
orderIndex: milestone.orderIndex,
|
|
786
|
+
features: features.map((feature) => ({
|
|
787
|
+
sourceFeatureId: feature.id,
|
|
788
|
+
title: feature.title,
|
|
789
|
+
description: feature.description,
|
|
790
|
+
orderIndex: feature.orderIndex
|
|
791
|
+
}))
|
|
792
|
+
};
|
|
793
|
+
})
|
|
794
|
+
};
|
|
795
|
+
}
|
|
796
|
+
/**
|
|
797
|
+
* Get a task planning handoff payload for a single roadmap feature.
|
|
798
|
+
*
|
|
799
|
+
* Returns a self-contained handoff payload for converting a roadmap feature
|
|
800
|
+
* into task planning flows without coupling to MissionStore internals.
|
|
801
|
+
*
|
|
802
|
+
* @param roadmapId - Parent roadmap ID (for validation)
|
|
803
|
+
* @param milestoneId - Parent milestone ID (for validation)
|
|
804
|
+
* @param featureId - Feature ID to generate handoff for
|
|
805
|
+
* @returns The task planning handoff payload
|
|
806
|
+
* @throws Error if any entity is not found or if ownership validation fails
|
|
807
|
+
*/
|
|
808
|
+
getRoadmapFeatureHandoff(roadmapId, milestoneId, featureId) {
|
|
809
|
+
const roadmap = this.getRoadmap(roadmapId);
|
|
810
|
+
if (!roadmap) {
|
|
811
|
+
throw new Error(`Roadmap ${roadmapId} not found`);
|
|
812
|
+
}
|
|
813
|
+
const milestone = this.getMilestone(milestoneId);
|
|
814
|
+
if (!milestone) {
|
|
815
|
+
throw new Error(`Milestone ${milestoneId} not found`);
|
|
816
|
+
}
|
|
817
|
+
if (milestone.roadmapId !== roadmapId) {
|
|
818
|
+
throw new Error(`Milestone ${milestoneId} does not belong to roadmap ${roadmapId}`);
|
|
819
|
+
}
|
|
820
|
+
const feature = this.getFeature(featureId);
|
|
821
|
+
if (!feature) {
|
|
822
|
+
throw new Error(`Feature ${featureId} not found`);
|
|
823
|
+
}
|
|
824
|
+
if (feature.milestoneId !== milestoneId) {
|
|
825
|
+
throw new Error(`Feature ${featureId} does not belong to milestone ${milestoneId}`);
|
|
826
|
+
}
|
|
827
|
+
const source = {
|
|
828
|
+
roadmapId: roadmap.id,
|
|
829
|
+
milestoneId: milestone.id,
|
|
830
|
+
featureId: feature.id,
|
|
831
|
+
roadmapTitle: roadmap.title,
|
|
832
|
+
milestoneTitle: milestone.title,
|
|
833
|
+
milestoneOrderIndex: milestone.orderIndex,
|
|
834
|
+
featureOrderIndex: feature.orderIndex
|
|
835
|
+
};
|
|
836
|
+
return {
|
|
837
|
+
source,
|
|
838
|
+
title: feature.title,
|
|
839
|
+
description: feature.description
|
|
840
|
+
};
|
|
841
|
+
}
|
|
842
|
+
/**
|
|
843
|
+
* Get a mission planning handoff payload for a roadmap.
|
|
844
|
+
*
|
|
845
|
+
* Alias for getRoadmapMissionHandoff() for API consistency.
|
|
846
|
+
* Converts the roadmap into a mission planning structure while preserving
|
|
847
|
+
* source IDs and deterministic order.
|
|
848
|
+
*
|
|
849
|
+
* @param roadmapId - Roadmap ID
|
|
850
|
+
* @returns The mission planning handoff payload
|
|
851
|
+
* @throws Error if roadmap not found
|
|
852
|
+
*/
|
|
853
|
+
getMissionPlanningHandoff(roadmapId) {
|
|
854
|
+
return this.getRoadmapMissionHandoff(roadmapId);
|
|
855
|
+
}
|
|
856
|
+
/**
|
|
857
|
+
* List all task planning handoff payloads for a roadmap.
|
|
858
|
+
*
|
|
859
|
+
* Returns a flat list of all feature handoffs in deterministic order
|
|
860
|
+
* (milestone order index, then feature order index).
|
|
861
|
+
*
|
|
862
|
+
* @param roadmapId - Roadmap ID
|
|
863
|
+
* @returns Array of task planning handoff payloads for all features
|
|
864
|
+
* @throws Error if roadmap not found
|
|
865
|
+
*/
|
|
866
|
+
listFeatureTaskPlanningHandoffs(roadmapId) {
|
|
867
|
+
const roadmap = this.getRoadmap(roadmapId);
|
|
868
|
+
if (!roadmap) {
|
|
869
|
+
throw new Error(`Roadmap ${roadmapId} not found`);
|
|
870
|
+
}
|
|
871
|
+
const milestones = this.listMilestones(roadmapId);
|
|
872
|
+
const handoffs = [];
|
|
873
|
+
for (const milestone of milestones) {
|
|
874
|
+
const features = this.listFeatures(milestone.id);
|
|
875
|
+
for (const feature of features) {
|
|
876
|
+
const source = {
|
|
877
|
+
roadmapId: roadmap.id,
|
|
878
|
+
milestoneId: milestone.id,
|
|
879
|
+
featureId: feature.id,
|
|
880
|
+
roadmapTitle: roadmap.title,
|
|
881
|
+
milestoneTitle: milestone.title,
|
|
882
|
+
milestoneOrderIndex: milestone.orderIndex,
|
|
883
|
+
featureOrderIndex: feature.orderIndex
|
|
884
|
+
};
|
|
885
|
+
handoffs.push({
|
|
886
|
+
source,
|
|
887
|
+
title: feature.title,
|
|
888
|
+
description: feature.description
|
|
889
|
+
});
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
return handoffs;
|
|
893
|
+
}
|
|
894
|
+
};
|
|
895
|
+
|
|
896
|
+
// ../../plugins/fusion-plugin-roadmap/src/routes/roadmap-suggestions.js
|
|
897
|
+
var injectedCreateAiSession;
|
|
898
|
+
var MILESTONE_SUGGESTION_SYSTEM_PROMPT = `You are a milestone planning assistant for a product roadmap system.
|
|
899
|
+
|
|
900
|
+
Your job is to suggest logical milestones that would help achieve a user's roadmap goal.
|
|
901
|
+
|
|
902
|
+
## Guidelines
|
|
903
|
+
|
|
904
|
+
1. **Think about phases**: Break the goal into logical phases
|
|
905
|
+
2. **Use clear titles**: Milestone titles should be concise and descriptive
|
|
906
|
+
3. **Add context**: Include a brief description explaining what this milestone encompasses
|
|
907
|
+
4. **Order matters**: List milestones in the order they should be completed
|
|
908
|
+
5. **Realistic scope**: Each milestone should be achievable in 2-4 weeks
|
|
909
|
+
|
|
910
|
+
## Output Format
|
|
911
|
+
|
|
912
|
+
Respond with ONLY a valid JSON array of milestone suggestions.`;
|
|
913
|
+
var MAX_GOAL_PROMPT_LENGTH = 4e3;
|
|
914
|
+
var SUGGESTION_TIMEOUT_MS = 12e4;
|
|
915
|
+
var DEFAULT_SUGGESTION_COUNT = 5;
|
|
916
|
+
var MAX_SUGGESTION_COUNT = 10;
|
|
917
|
+
var MIN_SUGGESTION_COUNT = 1;
|
|
918
|
+
var MAX_PARSE_RETRIES = 1;
|
|
919
|
+
function validateSuggestionInput(input) {
|
|
920
|
+
if (!input || typeof input !== "object") {
|
|
921
|
+
throw new ValidationError("Request body must be an object");
|
|
922
|
+
}
|
|
923
|
+
const { goalPrompt, count } = input;
|
|
924
|
+
if (typeof goalPrompt !== "string" || !goalPrompt.trim()) {
|
|
925
|
+
throw new ValidationError("goalPrompt is required and must be a non-empty string");
|
|
926
|
+
}
|
|
927
|
+
if (goalPrompt.length > MAX_GOAL_PROMPT_LENGTH) {
|
|
928
|
+
throw new ValidationError(`goalPrompt exceeds maximum length of ${MAX_GOAL_PROMPT_LENGTH} characters`);
|
|
929
|
+
}
|
|
930
|
+
if (count !== void 0) {
|
|
931
|
+
if (typeof count !== "number" || !Number.isInteger(count))
|
|
932
|
+
throw new ValidationError("count must be an integer");
|
|
933
|
+
if (count < MIN_SUGGESTION_COUNT || count > MAX_SUGGESTION_COUNT) {
|
|
934
|
+
throw new ValidationError(`count must be between ${MIN_SUGGESTION_COUNT} and ${MAX_SUGGESTION_COUNT}`);
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
function extractJsonCandidate(text) {
|
|
939
|
+
if (!text || !text.trim())
|
|
940
|
+
return null;
|
|
941
|
+
const codeBlockMatch = text.match(/```(?:json)?\s*([\s\S]*?)\s*```/);
|
|
942
|
+
const source = codeBlockMatch?.[1]?.trim() || text.trim();
|
|
943
|
+
const startIndex = source.indexOf("[");
|
|
944
|
+
if (startIndex < 0)
|
|
945
|
+
return null;
|
|
946
|
+
let depth = 0;
|
|
947
|
+
let inString = false;
|
|
948
|
+
let escaped = false;
|
|
949
|
+
for (let index = startIndex; index < source.length; index++) {
|
|
950
|
+
const char = source[index];
|
|
951
|
+
if (inString) {
|
|
952
|
+
if (escaped) {
|
|
953
|
+
escaped = false;
|
|
954
|
+
} else if (char === "\\") {
|
|
955
|
+
escaped = true;
|
|
956
|
+
} else if (char === '"') {
|
|
957
|
+
inString = false;
|
|
958
|
+
}
|
|
959
|
+
continue;
|
|
960
|
+
}
|
|
961
|
+
if (char === '"') {
|
|
962
|
+
inString = true;
|
|
963
|
+
continue;
|
|
964
|
+
}
|
|
965
|
+
if (char === "[")
|
|
966
|
+
depth++;
|
|
967
|
+
if (char === "]") {
|
|
968
|
+
depth--;
|
|
969
|
+
if (depth === 0) {
|
|
970
|
+
return source.slice(startIndex, index + 1).trim();
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
return source.slice(startIndex).trim();
|
|
975
|
+
}
|
|
976
|
+
function repairJson(text) {
|
|
977
|
+
let repaired = text.replace(/,\s*([}\]])/g, "$1");
|
|
978
|
+
let depthBraces = 0;
|
|
979
|
+
let depthBrackets = 0;
|
|
980
|
+
for (const ch of repaired) {
|
|
981
|
+
if (ch === "{")
|
|
982
|
+
depthBraces++;
|
|
983
|
+
if (ch === "}")
|
|
984
|
+
depthBraces--;
|
|
985
|
+
if (ch === "[")
|
|
986
|
+
depthBrackets++;
|
|
987
|
+
if (ch === "]")
|
|
988
|
+
depthBrackets--;
|
|
989
|
+
}
|
|
990
|
+
repaired += "]".repeat(Math.max(0, depthBrackets));
|
|
991
|
+
repaired += "}".repeat(Math.max(0, depthBraces));
|
|
992
|
+
return repaired;
|
|
993
|
+
}
|
|
994
|
+
function parseMilestoneSuggestions(text) {
|
|
995
|
+
const candidate = extractJsonCandidate(text);
|
|
996
|
+
if (!candidate)
|
|
997
|
+
throw new ParseError("AI returned no valid JSON. Please try again.");
|
|
998
|
+
let parsed;
|
|
999
|
+
try {
|
|
1000
|
+
parsed = JSON.parse(candidate);
|
|
1001
|
+
} catch {
|
|
1002
|
+
parsed = JSON.parse(repairJson(candidate));
|
|
1003
|
+
}
|
|
1004
|
+
if (!Array.isArray(parsed)) {
|
|
1005
|
+
throw new ParseError("AI response must be a JSON array of milestone suggestions");
|
|
1006
|
+
}
|
|
1007
|
+
const suggestions = [];
|
|
1008
|
+
for (const item of parsed) {
|
|
1009
|
+
if (!item || typeof item !== "object")
|
|
1010
|
+
continue;
|
|
1011
|
+
const row = item;
|
|
1012
|
+
if (typeof row.title !== "string" || !row.title.trim())
|
|
1013
|
+
continue;
|
|
1014
|
+
suggestions.push({
|
|
1015
|
+
title: row.title.trim(),
|
|
1016
|
+
description: typeof row.description === "string" && row.description.trim() ? row.description.trim() : void 0
|
|
1017
|
+
});
|
|
1018
|
+
}
|
|
1019
|
+
if (suggestions.length === 0)
|
|
1020
|
+
throw new ParseError("AI returned no valid milestone suggestions");
|
|
1021
|
+
return suggestions;
|
|
1022
|
+
}
|
|
1023
|
+
function pickFactory(explicit) {
|
|
1024
|
+
return explicit ?? injectedCreateAiSession;
|
|
1025
|
+
}
|
|
1026
|
+
async function runPrompt(createAiSession, options, prompt) {
|
|
1027
|
+
const agent = await createAiSession(options);
|
|
1028
|
+
await agent.session.prompt(prompt);
|
|
1029
|
+
const lastMessage = agent.session.state.messages.filter((m) => m.role === "assistant").pop();
|
|
1030
|
+
let text = "";
|
|
1031
|
+
if (lastMessage?.content) {
|
|
1032
|
+
if (typeof lastMessage.content === "string")
|
|
1033
|
+
text = lastMessage.content;
|
|
1034
|
+
else {
|
|
1035
|
+
text = lastMessage.content.filter((c) => c.type === "text").map((c) => c.text).join("");
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
return { text, dispose: agent.session.dispose };
|
|
1039
|
+
}
|
|
1040
|
+
async function generateMilestoneSuggestions(goalPrompt, count = DEFAULT_SUGGESTION_COUNT, rootDir, modelProvider, modelId, createAiSession) {
|
|
1041
|
+
const factory = pickFactory(createAiSession);
|
|
1042
|
+
if (!factory)
|
|
1043
|
+
throw new ServiceUnavailableError("AI service is not available");
|
|
1044
|
+
if (!rootDir)
|
|
1045
|
+
throw new Error("rootDir is required for AI-powered suggestion generation");
|
|
1046
|
+
const result = await Promise.race([
|
|
1047
|
+
(async () => {
|
|
1048
|
+
let dispose;
|
|
1049
|
+
try {
|
|
1050
|
+
let response = await runPrompt(factory, {
|
|
1051
|
+
cwd: rootDir,
|
|
1052
|
+
systemPrompt: MILESTONE_SUGGESTION_SYSTEM_PROMPT,
|
|
1053
|
+
tools: "readonly",
|
|
1054
|
+
...modelProvider && modelId ? { defaultProvider: modelProvider, defaultModelId: modelId } : {}
|
|
1055
|
+
}, `Please suggest ${count} milestones for the following roadmap goal:
|
|
1056
|
+
|
|
1057
|
+
${goalPrompt.trim()}`);
|
|
1058
|
+
dispose = response.dispose;
|
|
1059
|
+
let suggestions;
|
|
1060
|
+
let lastError;
|
|
1061
|
+
for (let attempt = 0; attempt <= MAX_PARSE_RETRIES; attempt++) {
|
|
1062
|
+
try {
|
|
1063
|
+
suggestions = parseMilestoneSuggestions(response.text);
|
|
1064
|
+
break;
|
|
1065
|
+
} catch (error) {
|
|
1066
|
+
lastError = error instanceof Error ? error : new Error(String(error));
|
|
1067
|
+
if (attempt === MAX_PARSE_RETRIES)
|
|
1068
|
+
break;
|
|
1069
|
+
response = await runPrompt(factory, {
|
|
1070
|
+
cwd: rootDir,
|
|
1071
|
+
systemPrompt: MILESTONE_SUGGESTION_SYSTEM_PROMPT,
|
|
1072
|
+
tools: "readonly",
|
|
1073
|
+
...modelProvider && modelId ? { defaultProvider: modelProvider, defaultModelId: modelId } : {}
|
|
1074
|
+
}, "Your previous response could not be parsed as JSON. Respond with only a JSON array.");
|
|
1075
|
+
dispose = response.dispose;
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
if (!suggestions) {
|
|
1079
|
+
throw new ParseError(`Failed to parse AI response after ${MAX_PARSE_RETRIES + 1} attempts: ${lastError?.message ?? "Unknown error"}`);
|
|
1080
|
+
}
|
|
1081
|
+
return suggestions.slice(0, count);
|
|
1082
|
+
} finally {
|
|
1083
|
+
dispose?.();
|
|
1084
|
+
}
|
|
1085
|
+
})(),
|
|
1086
|
+
new Promise((_, reject) => globalThis.setTimeout(() => reject(new ServiceUnavailableError("AI suggestion generation timed out. Please try again.")), SUGGESTION_TIMEOUT_MS))
|
|
1087
|
+
]);
|
|
1088
|
+
return result;
|
|
1089
|
+
}
|
|
1090
|
+
var FEATURE_SUGGESTION_SYSTEM_PROMPT = `You are a feature planning assistant for a product roadmap system.`;
|
|
1091
|
+
var MAX_FEATURE_PROMPT_LENGTH = 2e3;
|
|
1092
|
+
function validateFeatureSuggestionInput(input) {
|
|
1093
|
+
if (!input || typeof input !== "object" || Array.isArray(input))
|
|
1094
|
+
throw new ValidationError("Request body must be an object");
|
|
1095
|
+
const { prompt, count } = input;
|
|
1096
|
+
if (prompt !== void 0) {
|
|
1097
|
+
if (typeof prompt !== "string")
|
|
1098
|
+
throw new ValidationError("prompt must be a string");
|
|
1099
|
+
if (prompt.length > MAX_FEATURE_PROMPT_LENGTH)
|
|
1100
|
+
throw new ValidationError(`prompt exceeds maximum length of ${MAX_FEATURE_PROMPT_LENGTH} characters`);
|
|
1101
|
+
}
|
|
1102
|
+
if (count !== void 0) {
|
|
1103
|
+
if (typeof count !== "number" || !Number.isInteger(count))
|
|
1104
|
+
throw new ValidationError("count must be an integer");
|
|
1105
|
+
if (count < MIN_SUGGESTION_COUNT || count > MAX_SUGGESTION_COUNT) {
|
|
1106
|
+
throw new ValidationError(`count must be between ${MIN_SUGGESTION_COUNT} and ${MAX_SUGGESTION_COUNT}`);
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
function buildMilestoneContextString(context) {
|
|
1111
|
+
const lines = [];
|
|
1112
|
+
lines.push(`Roadmap: ${context.roadmapTitle}`);
|
|
1113
|
+
if (context.roadmapDescription)
|
|
1114
|
+
lines.push(`Description: ${context.roadmapDescription}`);
|
|
1115
|
+
lines.push("", `Milestone: ${context.milestoneTitle}`);
|
|
1116
|
+
if (context.milestoneDescription)
|
|
1117
|
+
lines.push(`Description: ${context.milestoneDescription}`);
|
|
1118
|
+
if (context.existingFeatureTitles.length > 0) {
|
|
1119
|
+
lines.push("", "Existing features in this milestone:");
|
|
1120
|
+
for (const title of context.existingFeatureTitles)
|
|
1121
|
+
lines.push(` - ${title}`);
|
|
1122
|
+
}
|
|
1123
|
+
return lines.join("\n");
|
|
1124
|
+
}
|
|
1125
|
+
function parseFeatureSuggestions(text) {
|
|
1126
|
+
return parseMilestoneSuggestions(text);
|
|
1127
|
+
}
|
|
1128
|
+
async function generateFeatureSuggestions(context, count = DEFAULT_SUGGESTION_COUNT, prompt, rootDir, modelProvider, modelId, createAiSession) {
|
|
1129
|
+
const factory = pickFactory(createAiSession);
|
|
1130
|
+
if (!factory)
|
|
1131
|
+
throw new ServiceUnavailableError("AI service is not available");
|
|
1132
|
+
if (!rootDir)
|
|
1133
|
+
throw new Error("rootDir is required for AI-powered suggestion generation");
|
|
1134
|
+
const systemPrompt = `${FEATURE_SUGGESTION_SYSTEM_PROMPT}
|
|
1135
|
+
|
|
1136
|
+
${buildMilestoneContextString(context)}`;
|
|
1137
|
+
const userMessage = prompt?.trim() ? `Please suggest ${count} features for the milestone described above.
|
|
1138
|
+
|
|
1139
|
+
Additional guidance:
|
|
1140
|
+
${prompt.trim()}` : `Please suggest ${count} features for the milestone described above.`;
|
|
1141
|
+
const result = await Promise.race([
|
|
1142
|
+
(async () => {
|
|
1143
|
+
const { text, dispose } = await runPrompt(factory, {
|
|
1144
|
+
cwd: rootDir,
|
|
1145
|
+
systemPrompt,
|
|
1146
|
+
tools: "readonly",
|
|
1147
|
+
...modelProvider && modelId ? { defaultProvider: modelProvider, defaultModelId: modelId } : {}
|
|
1148
|
+
}, userMessage);
|
|
1149
|
+
try {
|
|
1150
|
+
return parseFeatureSuggestions(text).slice(0, count);
|
|
1151
|
+
} finally {
|
|
1152
|
+
dispose?.();
|
|
1153
|
+
}
|
|
1154
|
+
})(),
|
|
1155
|
+
new Promise((_, reject) => globalThis.setTimeout(() => reject(new ServiceUnavailableError("AI suggestion generation timed out. Please try again.")), SUGGESTION_TIMEOUT_MS))
|
|
1156
|
+
]);
|
|
1157
|
+
return result;
|
|
1158
|
+
}
|
|
1159
|
+
var ValidationError = class extends Error {
|
|
1160
|
+
constructor(message) {
|
|
1161
|
+
super(message);
|
|
1162
|
+
this.name = "ValidationError";
|
|
1163
|
+
}
|
|
1164
|
+
};
|
|
1165
|
+
var ParseError = class extends Error {
|
|
1166
|
+
constructor(message) {
|
|
1167
|
+
super(message);
|
|
1168
|
+
this.name = "ParseError";
|
|
1169
|
+
}
|
|
1170
|
+
};
|
|
1171
|
+
var ServiceUnavailableError = class extends Error {
|
|
1172
|
+
constructor(message) {
|
|
1173
|
+
super(message);
|
|
1174
|
+
this.name = "ServiceUnavailableError";
|
|
1175
|
+
}
|
|
1176
|
+
};
|
|
1177
|
+
|
|
1178
|
+
// ../../plugins/fusion-plugin-roadmap/src/routes/roadmap-routes.js
|
|
1179
|
+
var roadmapStoreCache = /* @__PURE__ */ new WeakMap();
|
|
1180
|
+
function getRoadmapStore(ctx) {
|
|
1181
|
+
const taskStoreWithRoadmaps = ctx.taskStore;
|
|
1182
|
+
if (typeof taskStoreWithRoadmaps.getRoadmapStore === "function") {
|
|
1183
|
+
return taskStoreWithRoadmaps.getRoadmapStore();
|
|
1184
|
+
}
|
|
1185
|
+
const key = ctx.taskStore;
|
|
1186
|
+
const cached = roadmapStoreCache.get(key);
|
|
1187
|
+
if (cached)
|
|
1188
|
+
return cached;
|
|
1189
|
+
const store = new RoadmapStore(ctx.taskStore.getDatabase());
|
|
1190
|
+
roadmapStoreCache.set(key, store);
|
|
1191
|
+
return store;
|
|
1192
|
+
}
|
|
1193
|
+
function asRequest(req) {
|
|
1194
|
+
return req;
|
|
1195
|
+
}
|
|
1196
|
+
function badRequest(message) {
|
|
1197
|
+
return { status: 400, body: { error: message } };
|
|
1198
|
+
}
|
|
1199
|
+
function notFound(message) {
|
|
1200
|
+
return { status: 404, body: { error: message } };
|
|
1201
|
+
}
|
|
1202
|
+
function serverError(message) {
|
|
1203
|
+
return { status: 500, body: { error: message } };
|
|
1204
|
+
}
|
|
1205
|
+
function noContent() {
|
|
1206
|
+
return { status: 204 };
|
|
1207
|
+
}
|
|
1208
|
+
function routeHandler(handler) {
|
|
1209
|
+
return async (req, ctx) => {
|
|
1210
|
+
const roadmapStore = getRoadmapStore(ctx);
|
|
1211
|
+
try {
|
|
1212
|
+
return await handler(asRequest(req), ctx, roadmapStore);
|
|
1213
|
+
} catch (error) {
|
|
1214
|
+
if (error instanceof Error && error.message.toLowerCase().includes("not found")) {
|
|
1215
|
+
return notFound(error.message);
|
|
1216
|
+
}
|
|
1217
|
+
return serverError(error instanceof Error ? error.message : "Internal server error");
|
|
1218
|
+
}
|
|
1219
|
+
};
|
|
1220
|
+
}
|
|
1221
|
+
function validateTitle(title) {
|
|
1222
|
+
if (!title || typeof title !== "string" || !title.trim()) {
|
|
1223
|
+
throw new Error("title is required");
|
|
1224
|
+
}
|
|
1225
|
+
if (title.length > 200) {
|
|
1226
|
+
throw new Error("title must not exceed 200 characters");
|
|
1227
|
+
}
|
|
1228
|
+
return title.trim();
|
|
1229
|
+
}
|
|
1230
|
+
function validateDescription(desc) {
|
|
1231
|
+
if (desc === void 0 || desc === null)
|
|
1232
|
+
return void 0;
|
|
1233
|
+
if (typeof desc !== "string") {
|
|
1234
|
+
throw new Error("description must be a string");
|
|
1235
|
+
}
|
|
1236
|
+
if (desc.length > 5e3) {
|
|
1237
|
+
throw new Error("description must not exceed 5000 characters");
|
|
1238
|
+
}
|
|
1239
|
+
return desc.trim() || void 0;
|
|
1240
|
+
}
|
|
1241
|
+
function validateStringArray(arr, fieldName) {
|
|
1242
|
+
if (!Array.isArray(arr)) {
|
|
1243
|
+
throw new Error(`${fieldName} must be an array`);
|
|
1244
|
+
}
|
|
1245
|
+
if (!arr.every((item) => typeof item === "string")) {
|
|
1246
|
+
throw new Error(`${fieldName} must be an array of strings`);
|
|
1247
|
+
}
|
|
1248
|
+
return arr;
|
|
1249
|
+
}
|
|
1250
|
+
function createRoadmapPluginRoutes() {
|
|
1251
|
+
return [
|
|
1252
|
+
{
|
|
1253
|
+
method: "GET",
|
|
1254
|
+
path: "/roadmaps",
|
|
1255
|
+
handler: routeHandler((_req, _ctx, roadmapStore) => roadmapStore.listRoadmaps())
|
|
1256
|
+
},
|
|
1257
|
+
{
|
|
1258
|
+
method: "POST",
|
|
1259
|
+
path: "/roadmaps",
|
|
1260
|
+
handler: routeHandler((req, _ctx, roadmapStore) => {
|
|
1261
|
+
const body = req.body;
|
|
1262
|
+
try {
|
|
1263
|
+
return {
|
|
1264
|
+
status: 201,
|
|
1265
|
+
body: roadmapStore.createRoadmap({
|
|
1266
|
+
title: validateTitle(body?.title),
|
|
1267
|
+
description: validateDescription(body?.description)
|
|
1268
|
+
})
|
|
1269
|
+
};
|
|
1270
|
+
} catch (error) {
|
|
1271
|
+
return badRequest(error instanceof Error ? error.message : "Invalid input");
|
|
1272
|
+
}
|
|
1273
|
+
})
|
|
1274
|
+
},
|
|
1275
|
+
{
|
|
1276
|
+
method: "GET",
|
|
1277
|
+
path: "/roadmaps/:roadmapId",
|
|
1278
|
+
handler: routeHandler((req, _ctx, roadmapStore) => {
|
|
1279
|
+
const roadmap = roadmapStore.getRoadmapWithHierarchy(req.params.roadmapId);
|
|
1280
|
+
return roadmap ? roadmap : notFound(`Roadmap ${req.params.roadmapId} not found`);
|
|
1281
|
+
})
|
|
1282
|
+
},
|
|
1283
|
+
{
|
|
1284
|
+
method: "PATCH",
|
|
1285
|
+
path: "/roadmaps/:roadmapId",
|
|
1286
|
+
handler: routeHandler((req, _ctx, roadmapStore) => {
|
|
1287
|
+
const body = req.body;
|
|
1288
|
+
try {
|
|
1289
|
+
return roadmapStore.updateRoadmap(req.params.roadmapId, {
|
|
1290
|
+
title: body.title !== void 0 ? validateTitle(body.title) : void 0,
|
|
1291
|
+
description: body.description !== void 0 ? validateDescription(body.description) : void 0
|
|
1292
|
+
});
|
|
1293
|
+
} catch (error) {
|
|
1294
|
+
return badRequest(error instanceof Error ? error.message : "Invalid input");
|
|
1295
|
+
}
|
|
1296
|
+
})
|
|
1297
|
+
},
|
|
1298
|
+
{ method: "DELETE", path: "/roadmaps/:roadmapId", handler: routeHandler((req, _ctx, roadmapStore) => {
|
|
1299
|
+
roadmapStore.deleteRoadmap(req.params.roadmapId);
|
|
1300
|
+
return noContent();
|
|
1301
|
+
}) },
|
|
1302
|
+
{
|
|
1303
|
+
method: "POST",
|
|
1304
|
+
path: "/roadmaps/:roadmapId/milestones",
|
|
1305
|
+
handler: routeHandler((req, _ctx, roadmapStore) => {
|
|
1306
|
+
const body = req.body;
|
|
1307
|
+
try {
|
|
1308
|
+
return {
|
|
1309
|
+
status: 201,
|
|
1310
|
+
body: roadmapStore.createMilestone(req.params.roadmapId, {
|
|
1311
|
+
title: validateTitle(body?.title),
|
|
1312
|
+
description: validateDescription(body?.description)
|
|
1313
|
+
})
|
|
1314
|
+
};
|
|
1315
|
+
} catch (error) {
|
|
1316
|
+
return badRequest(error instanceof Error ? error.message : "Invalid input");
|
|
1317
|
+
}
|
|
1318
|
+
})
|
|
1319
|
+
},
|
|
1320
|
+
{
|
|
1321
|
+
method: "POST",
|
|
1322
|
+
path: "/roadmaps/:roadmapId/milestones/reorder",
|
|
1323
|
+
handler: routeHandler((req, _ctx, roadmapStore) => {
|
|
1324
|
+
try {
|
|
1325
|
+
const body = req.body;
|
|
1326
|
+
roadmapStore.reorderMilestones({ roadmapId: req.params.roadmapId, orderedMilestoneIds: validateStringArray(body?.orderedMilestoneIds, "orderedMilestoneIds") });
|
|
1327
|
+
return noContent();
|
|
1328
|
+
} catch (error) {
|
|
1329
|
+
return badRequest(error instanceof Error ? error.message : "Invalid input");
|
|
1330
|
+
}
|
|
1331
|
+
})
|
|
1332
|
+
},
|
|
1333
|
+
{
|
|
1334
|
+
method: "PATCH",
|
|
1335
|
+
path: "/roadmaps/milestones/:milestoneId",
|
|
1336
|
+
handler: routeHandler((req, _ctx, roadmapStore) => {
|
|
1337
|
+
const body = req.body;
|
|
1338
|
+
try {
|
|
1339
|
+
return roadmapStore.updateMilestone(req.params.milestoneId, {
|
|
1340
|
+
title: body.title !== void 0 ? validateTitle(body.title) : void 0,
|
|
1341
|
+
description: body.description !== void 0 ? validateDescription(body.description) : void 0
|
|
1342
|
+
});
|
|
1343
|
+
} catch (error) {
|
|
1344
|
+
return badRequest(error instanceof Error ? error.message : "Invalid input");
|
|
1345
|
+
}
|
|
1346
|
+
})
|
|
1347
|
+
},
|
|
1348
|
+
{ method: "DELETE", path: "/roadmaps/milestones/:milestoneId", handler: routeHandler((req, _ctx, roadmapStore) => {
|
|
1349
|
+
roadmapStore.deleteMilestone(req.params.milestoneId);
|
|
1350
|
+
return noContent();
|
|
1351
|
+
}) },
|
|
1352
|
+
{
|
|
1353
|
+
method: "POST",
|
|
1354
|
+
path: "/roadmaps/milestones/:milestoneId/features",
|
|
1355
|
+
handler: routeHandler((req, _ctx, roadmapStore) => {
|
|
1356
|
+
const body = req.body;
|
|
1357
|
+
try {
|
|
1358
|
+
return {
|
|
1359
|
+
status: 201,
|
|
1360
|
+
body: roadmapStore.createFeature(req.params.milestoneId, {
|
|
1361
|
+
title: validateTitle(body?.title),
|
|
1362
|
+
description: validateDescription(body?.description)
|
|
1363
|
+
})
|
|
1364
|
+
};
|
|
1365
|
+
} catch (error) {
|
|
1366
|
+
return badRequest(error instanceof Error ? error.message : "Invalid input");
|
|
1367
|
+
}
|
|
1368
|
+
})
|
|
1369
|
+
},
|
|
1370
|
+
{
|
|
1371
|
+
method: "POST",
|
|
1372
|
+
path: "/roadmaps/milestones/:milestoneId/features/reorder",
|
|
1373
|
+
handler: routeHandler((req, _ctx, roadmapStore) => {
|
|
1374
|
+
try {
|
|
1375
|
+
const body = req.body;
|
|
1376
|
+
const milestone = roadmapStore.getMilestone(req.params.milestoneId);
|
|
1377
|
+
if (!milestone)
|
|
1378
|
+
return notFound(`Milestone ${req.params.milestoneId} not found`);
|
|
1379
|
+
roadmapStore.reorderFeatures({ roadmapId: milestone.roadmapId, milestoneId: req.params.milestoneId, orderedFeatureIds: validateStringArray(body?.orderedFeatureIds, "orderedFeatureIds") });
|
|
1380
|
+
return noContent();
|
|
1381
|
+
} catch (error) {
|
|
1382
|
+
return badRequest(error instanceof Error ? error.message : "Invalid input");
|
|
1383
|
+
}
|
|
1384
|
+
})
|
|
1385
|
+
},
|
|
1386
|
+
{
|
|
1387
|
+
method: "PATCH",
|
|
1388
|
+
path: "/roadmaps/features/:featureId",
|
|
1389
|
+
handler: routeHandler((req, _ctx, roadmapStore) => {
|
|
1390
|
+
const body = req.body;
|
|
1391
|
+
try {
|
|
1392
|
+
return roadmapStore.updateFeature(req.params.featureId, {
|
|
1393
|
+
title: body.title !== void 0 ? validateTitle(body.title) : void 0,
|
|
1394
|
+
description: body.description !== void 0 ? validateDescription(body.description) : void 0
|
|
1395
|
+
});
|
|
1396
|
+
} catch (error) {
|
|
1397
|
+
return badRequest(error instanceof Error ? error.message : "Invalid input");
|
|
1398
|
+
}
|
|
1399
|
+
})
|
|
1400
|
+
},
|
|
1401
|
+
{ method: "DELETE", path: "/roadmaps/features/:featureId", handler: routeHandler((req, _ctx, roadmapStore) => {
|
|
1402
|
+
roadmapStore.deleteFeature(req.params.featureId);
|
|
1403
|
+
return noContent();
|
|
1404
|
+
}) },
|
|
1405
|
+
{
|
|
1406
|
+
method: "POST",
|
|
1407
|
+
path: "/roadmaps/features/:featureId/move",
|
|
1408
|
+
handler: routeHandler((req, _ctx, roadmapStore) => {
|
|
1409
|
+
const body = req.body;
|
|
1410
|
+
if (!body?.targetMilestoneId)
|
|
1411
|
+
return badRequest("targetMilestoneId is required");
|
|
1412
|
+
if (typeof body.targetIndex !== "number")
|
|
1413
|
+
return badRequest("targetIndex must be a number");
|
|
1414
|
+
const feature = roadmapStore.getFeature(req.params.featureId);
|
|
1415
|
+
if (!feature)
|
|
1416
|
+
return notFound(`Feature ${req.params.featureId} not found`);
|
|
1417
|
+
const fromMilestone = roadmapStore.getMilestone(feature.milestoneId);
|
|
1418
|
+
if (!fromMilestone)
|
|
1419
|
+
return notFound(`Source milestone ${feature.milestoneId} not found`);
|
|
1420
|
+
const toMilestone = roadmapStore.getMilestone(body.targetMilestoneId);
|
|
1421
|
+
if (!toMilestone)
|
|
1422
|
+
return notFound(`Target milestone ${body.targetMilestoneId} not found`);
|
|
1423
|
+
roadmapStore.moveFeature({
|
|
1424
|
+
roadmapId: fromMilestone.roadmapId,
|
|
1425
|
+
featureId: req.params.featureId,
|
|
1426
|
+
fromMilestoneId: feature.milestoneId,
|
|
1427
|
+
toMilestoneId: body.targetMilestoneId,
|
|
1428
|
+
targetOrderIndex: body.targetIndex
|
|
1429
|
+
});
|
|
1430
|
+
return noContent();
|
|
1431
|
+
})
|
|
1432
|
+
},
|
|
1433
|
+
{
|
|
1434
|
+
method: "POST",
|
|
1435
|
+
path: "/roadmaps/:roadmapId/suggestions/milestones",
|
|
1436
|
+
handler: routeHandler(async (req, ctx, roadmapStore) => {
|
|
1437
|
+
const roadmap = roadmapStore.getRoadmap(req.params.roadmapId);
|
|
1438
|
+
if (!roadmap)
|
|
1439
|
+
return notFound(`Roadmap ${req.params.roadmapId} not found`);
|
|
1440
|
+
try {
|
|
1441
|
+
validateSuggestionInput(req.body);
|
|
1442
|
+
} catch (error) {
|
|
1443
|
+
if (error instanceof ValidationError)
|
|
1444
|
+
return badRequest(error.message);
|
|
1445
|
+
throw error;
|
|
1446
|
+
}
|
|
1447
|
+
try {
|
|
1448
|
+
const body = req.body;
|
|
1449
|
+
const suggestions = await generateMilestoneSuggestions(body.goalPrompt, body.count, ctx.taskStore.getRootDir(), void 0, void 0, ctx.createAiSession);
|
|
1450
|
+
return { suggestions };
|
|
1451
|
+
} catch (error) {
|
|
1452
|
+
if (error instanceof ParseError)
|
|
1453
|
+
return serverError(error.message);
|
|
1454
|
+
if (error instanceof ServiceUnavailableError) {
|
|
1455
|
+
return { status: 503, body: { error: error.message } };
|
|
1456
|
+
}
|
|
1457
|
+
throw error;
|
|
1458
|
+
}
|
|
1459
|
+
})
|
|
1460
|
+
},
|
|
1461
|
+
{
|
|
1462
|
+
method: "POST",
|
|
1463
|
+
path: "/roadmaps/milestones/:milestoneId/suggestions/features",
|
|
1464
|
+
handler: routeHandler(async (req, ctx, roadmapStore) => {
|
|
1465
|
+
const milestone = roadmapStore.getMilestone(req.params.milestoneId);
|
|
1466
|
+
if (!milestone)
|
|
1467
|
+
return notFound(`Milestone ${req.params.milestoneId} not found`);
|
|
1468
|
+
const roadmap = roadmapStore.getRoadmap(milestone.roadmapId);
|
|
1469
|
+
if (!roadmap)
|
|
1470
|
+
return notFound(`Roadmap ${milestone.roadmapId} not found`);
|
|
1471
|
+
try {
|
|
1472
|
+
validateFeatureSuggestionInput(req.body);
|
|
1473
|
+
} catch (error) {
|
|
1474
|
+
if (error instanceof ValidationError)
|
|
1475
|
+
return badRequest(error.message);
|
|
1476
|
+
throw error;
|
|
1477
|
+
}
|
|
1478
|
+
try {
|
|
1479
|
+
const body = req.body;
|
|
1480
|
+
const suggestions = await generateFeatureSuggestions({
|
|
1481
|
+
roadmapTitle: roadmap.title,
|
|
1482
|
+
roadmapDescription: roadmap.description,
|
|
1483
|
+
milestoneTitle: milestone.title,
|
|
1484
|
+
milestoneDescription: milestone.description,
|
|
1485
|
+
existingFeatureTitles: roadmapStore.listFeatures(milestone.id).map((feature) => feature.title)
|
|
1486
|
+
}, body.count, body.prompt, ctx.taskStore.getRootDir(), void 0, void 0, ctx.createAiSession);
|
|
1487
|
+
return { suggestions };
|
|
1488
|
+
} catch (error) {
|
|
1489
|
+
if (error instanceof ParseError)
|
|
1490
|
+
return serverError(error.message);
|
|
1491
|
+
if (error instanceof ServiceUnavailableError) {
|
|
1492
|
+
return { status: 503, body: { error: error.message } };
|
|
1493
|
+
}
|
|
1494
|
+
throw error;
|
|
1495
|
+
}
|
|
1496
|
+
})
|
|
1497
|
+
},
|
|
1498
|
+
{
|
|
1499
|
+
method: "GET",
|
|
1500
|
+
path: "/roadmaps/:roadmapId/export",
|
|
1501
|
+
handler: routeHandler((req, _ctx, roadmapStore) => roadmapStore.getRoadmapExport(req.params.roadmapId))
|
|
1502
|
+
},
|
|
1503
|
+
{
|
|
1504
|
+
method: "GET",
|
|
1505
|
+
path: "/roadmaps/:roadmapId/handoff",
|
|
1506
|
+
handler: routeHandler((req, _ctx, roadmapStore) => ({
|
|
1507
|
+
mission: roadmapStore.getMissionPlanningHandoff(req.params.roadmapId),
|
|
1508
|
+
features: roadmapStore.listFeatureTaskPlanningHandoffs(req.params.roadmapId)
|
|
1509
|
+
}))
|
|
1510
|
+
},
|
|
1511
|
+
{
|
|
1512
|
+
method: "GET",
|
|
1513
|
+
path: "/roadmaps/:roadmapId/handoff/mission",
|
|
1514
|
+
handler: routeHandler((req, _ctx, roadmapStore) => roadmapStore.getMissionPlanningHandoff(req.params.roadmapId))
|
|
1515
|
+
},
|
|
1516
|
+
{
|
|
1517
|
+
method: "GET",
|
|
1518
|
+
path: "/roadmaps/:roadmapId/milestones/:milestoneId/features/:featureId/handoff/task",
|
|
1519
|
+
handler: routeHandler((req, _ctx, roadmapStore) => roadmapStore.getRoadmapFeatureHandoff(req.params.roadmapId, req.params.milestoneId, req.params.featureId))
|
|
1520
|
+
}
|
|
1521
|
+
];
|
|
1522
|
+
}
|
|
1523
|
+
|
|
1524
|
+
// ../../plugins/fusion-plugin-roadmap/src/roadmap-schema.ts
|
|
1525
|
+
function ensureRoadmapSchema(db) {
|
|
1526
|
+
db.exec(`
|
|
1527
|
+
CREATE TABLE IF NOT EXISTS roadmaps (
|
|
1528
|
+
id TEXT PRIMARY KEY,
|
|
1529
|
+
title TEXT NOT NULL,
|
|
1530
|
+
description TEXT,
|
|
1531
|
+
createdAt TEXT NOT NULL,
|
|
1532
|
+
updatedAt TEXT NOT NULL
|
|
1533
|
+
);
|
|
1534
|
+
|
|
1535
|
+
CREATE TABLE IF NOT EXISTS roadmap_milestones (
|
|
1536
|
+
id TEXT PRIMARY KEY,
|
|
1537
|
+
roadmapId TEXT NOT NULL,
|
|
1538
|
+
title TEXT NOT NULL,
|
|
1539
|
+
description TEXT,
|
|
1540
|
+
orderIndex INTEGER NOT NULL,
|
|
1541
|
+
createdAt TEXT NOT NULL,
|
|
1542
|
+
updatedAt TEXT NOT NULL,
|
|
1543
|
+
FOREIGN KEY (roadmapId) REFERENCES roadmaps(id) ON DELETE CASCADE
|
|
1544
|
+
);
|
|
1545
|
+
|
|
1546
|
+
CREATE TABLE IF NOT EXISTS roadmap_features (
|
|
1547
|
+
id TEXT PRIMARY KEY,
|
|
1548
|
+
milestoneId TEXT NOT NULL,
|
|
1549
|
+
title TEXT NOT NULL,
|
|
1550
|
+
description TEXT,
|
|
1551
|
+
orderIndex INTEGER NOT NULL,
|
|
1552
|
+
createdAt TEXT NOT NULL,
|
|
1553
|
+
updatedAt TEXT NOT NULL,
|
|
1554
|
+
FOREIGN KEY (milestoneId) REFERENCES roadmap_milestones(id) ON DELETE CASCADE
|
|
1555
|
+
);
|
|
1556
|
+
|
|
1557
|
+
CREATE INDEX IF NOT EXISTS idxRoadmapMilestonesRoadmapOrder
|
|
1558
|
+
ON roadmap_milestones(roadmapId, orderIndex, createdAt, id);
|
|
1559
|
+
|
|
1560
|
+
CREATE INDEX IF NOT EXISTS idxRoadmapFeaturesMilestoneOrder
|
|
1561
|
+
ON roadmap_features(milestoneId, orderIndex, createdAt, id);
|
|
1562
|
+
`);
|
|
1563
|
+
}
|
|
1564
|
+
|
|
1565
|
+
// ../../plugins/fusion-plugin-roadmap/src/store/roadmap-handoff.ts
|
|
1566
|
+
function buildFeatureSourceRef(roadmap, milestone, feature) {
|
|
1567
|
+
return {
|
|
1568
|
+
roadmapId: roadmap.id,
|
|
1569
|
+
milestoneId: milestone.id,
|
|
1570
|
+
featureId: feature.id,
|
|
1571
|
+
roadmapTitle: roadmap.title,
|
|
1572
|
+
milestoneTitle: milestone.title,
|
|
1573
|
+
milestoneOrderIndex: milestone.orderIndex,
|
|
1574
|
+
featureOrderIndex: feature.orderIndex
|
|
1575
|
+
};
|
|
1576
|
+
}
|
|
1577
|
+
function mapFeatureToTaskHandoff(roadmap, milestone, feature) {
|
|
1578
|
+
return {
|
|
1579
|
+
source: buildFeatureSourceRef(roadmap, milestone, feature),
|
|
1580
|
+
title: feature.title,
|
|
1581
|
+
description: feature.description
|
|
1582
|
+
};
|
|
1583
|
+
}
|
|
1584
|
+
function mapRoadmapToMissionHandoff(roadmap, milestones, featuresByMilestoneId) {
|
|
1585
|
+
const normalizedMilestones = normalizeRoadmapMilestoneOrder(milestones);
|
|
1586
|
+
const milestoneHandoffs = normalizedMilestones.map((milestone) => {
|
|
1587
|
+
const rawFeatures = featuresByMilestoneId.get(milestone.id) ?? [];
|
|
1588
|
+
const normalizedFeatures = normalizeRoadmapFeatureOrder(rawFeatures);
|
|
1589
|
+
return {
|
|
1590
|
+
sourceMilestoneId: milestone.id,
|
|
1591
|
+
title: milestone.title,
|
|
1592
|
+
description: milestone.description,
|
|
1593
|
+
orderIndex: milestone.orderIndex,
|
|
1594
|
+
features: normalizedFeatures.map((feature) => ({
|
|
1595
|
+
sourceFeatureId: feature.id,
|
|
1596
|
+
title: feature.title,
|
|
1597
|
+
description: feature.description,
|
|
1598
|
+
orderIndex: feature.orderIndex
|
|
1599
|
+
}))
|
|
1600
|
+
};
|
|
1601
|
+
});
|
|
1602
|
+
return {
|
|
1603
|
+
sourceRoadmapId: roadmap.id,
|
|
1604
|
+
title: roadmap.title,
|
|
1605
|
+
description: roadmap.description,
|
|
1606
|
+
milestones: milestoneHandoffs
|
|
1607
|
+
};
|
|
1608
|
+
}
|
|
1609
|
+
function mapRoadmapWithHierarchyToMissionHandoff(roadmapWithHierarchy) {
|
|
1610
|
+
const featuresByMilestoneId = /* @__PURE__ */ new Map();
|
|
1611
|
+
for (const milestone of roadmapWithHierarchy.milestones) {
|
|
1612
|
+
featuresByMilestoneId.set(milestone.id, milestone.features);
|
|
1613
|
+
}
|
|
1614
|
+
return mapRoadmapToMissionHandoff(
|
|
1615
|
+
roadmapWithHierarchy,
|
|
1616
|
+
roadmapWithHierarchy.milestones,
|
|
1617
|
+
featuresByMilestoneId
|
|
1618
|
+
);
|
|
1619
|
+
}
|
|
1620
|
+
function mapAllFeaturesToTaskHandoffs(roadmap, milestones, featuresByMilestoneId) {
|
|
1621
|
+
const normalizedMilestones = normalizeRoadmapMilestoneOrder(milestones);
|
|
1622
|
+
const handoffs = [];
|
|
1623
|
+
for (const milestone of normalizedMilestones) {
|
|
1624
|
+
const rawFeatures = featuresByMilestoneId.get(milestone.id) ?? [];
|
|
1625
|
+
const normalizedFeatures = normalizeRoadmapFeatureOrder(rawFeatures);
|
|
1626
|
+
for (const feature of normalizedFeatures) {
|
|
1627
|
+
handoffs.push(mapFeatureToTaskHandoff(roadmap, milestone, feature));
|
|
1628
|
+
}
|
|
1629
|
+
}
|
|
1630
|
+
return handoffs;
|
|
1631
|
+
}
|
|
1632
|
+
|
|
1633
|
+
// ../../plugins/fusion-plugin-roadmap/src/index.ts
|
|
1634
|
+
var plugin = definePlugin({
|
|
1635
|
+
manifest: {
|
|
1636
|
+
id: "fusion-plugin-roadmap",
|
|
1637
|
+
name: "Roadmaps",
|
|
1638
|
+
version: "0.1.0",
|
|
1639
|
+
description: "Standalone roadmap planning plugin"
|
|
1640
|
+
},
|
|
1641
|
+
state: "installed",
|
|
1642
|
+
hooks: {
|
|
1643
|
+
onSchemaInit: ensureRoadmapSchema
|
|
1644
|
+
},
|
|
1645
|
+
routes: createRoadmapPluginRoutes(),
|
|
1646
|
+
dashboardViews: [
|
|
1647
|
+
{
|
|
1648
|
+
viewId: "roadmaps",
|
|
1649
|
+
label: "Roadmaps",
|
|
1650
|
+
componentPath: "./dashboard-view",
|
|
1651
|
+
icon: "Map",
|
|
1652
|
+
placement: "primary",
|
|
1653
|
+
order: 30
|
|
1654
|
+
}
|
|
1655
|
+
]
|
|
1656
|
+
});
|
|
1657
|
+
var index_default = plugin;
|
|
1658
|
+
export {
|
|
1659
|
+
RoadmapStore,
|
|
1660
|
+
applyRoadmapFeatureReorder,
|
|
1661
|
+
applyRoadmapMilestoneReorder,
|
|
1662
|
+
createRoadmapPluginRoutes,
|
|
1663
|
+
index_default as default,
|
|
1664
|
+
ensureRoadmapSchema,
|
|
1665
|
+
mapAllFeaturesToTaskHandoffs,
|
|
1666
|
+
mapFeatureToTaskHandoff,
|
|
1667
|
+
mapRoadmapToMissionHandoff,
|
|
1668
|
+
mapRoadmapWithHierarchyToMissionHandoff,
|
|
1669
|
+
moveRoadmapFeature,
|
|
1670
|
+
normalizeRoadmapFeatureOrder,
|
|
1671
|
+
normalizeRoadmapMilestoneOrder
|
|
1672
|
+
};
|