@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.
Files changed (226) hide show
  1. package/README.md +6 -0
  2. package/dist/bin.js +28004 -16888
  3. package/dist/client/assets/AgentDetailView-B7QRcHJH.css +1 -0
  4. package/dist/client/assets/AgentDetailView-DwLmRXTY.js +18 -0
  5. package/dist/client/assets/{AgentsView-B3jYk8Kt.js → AgentsView-D-N6aA0P.js} +12 -7
  6. package/dist/client/assets/ChatView-DnCdKu8Z.js +1 -0
  7. package/dist/client/assets/{DevServerView-DyGDEiBP.js → DevServerView-BiA1nYtt.js} +1 -1
  8. package/dist/client/assets/{DirectoryPicker-D5UIeIl6.js → DirectoryPicker-DvBviDG6.js} +1 -1
  9. package/dist/client/assets/{DocumentsView-DNHu1T8K.js → DocumentsView-BWXOxpuq.js} +1 -1
  10. package/dist/client/assets/{EvalsView-CpRobtDi.js → EvalsView-CJFbtL7i.js} +1 -1
  11. package/dist/client/assets/{ExperimentalAgentOnboardingModal-DOY_oZi7.js → ExperimentalAgentOnboardingModal-DuGIPd0B.js} +1 -1
  12. package/dist/client/assets/InsightsView-BBpRiolN.js +11 -0
  13. package/dist/client/assets/{MemoryView-PSc5lGJt.js → MemoryView-48LuNkKk.js} +2 -2
  14. package/dist/client/assets/NodesView-CGQWSNZM.js +14 -0
  15. package/dist/client/assets/{PiExtensionsManager-DL_QcN56.js → PiExtensionsManager-i-7UL2oh.js} +2 -2
  16. package/dist/client/assets/PluginManager-DoSAykD6.js +1 -0
  17. package/dist/client/assets/{ResearchView-BzCcDAS4.css → ResearchView-BEI4ZSGs.css} +1 -1
  18. package/dist/client/assets/ResearchView-XZuRtOxE.js +1 -0
  19. package/dist/client/assets/SettingsModal-Ci0_sqbU.css +1 -0
  20. package/dist/client/assets/{SettingsModal-CUCyaAyE.js → SettingsModal-CmeF8CN4.js} +1 -1
  21. package/dist/client/assets/SettingsModal-DBcjf9Bu.js +31 -0
  22. package/dist/client/assets/SettingsModal-DWKgRxBA.css +1 -0
  23. package/dist/client/assets/{SetupWizardModal-BKscasuh.js → SetupWizardModal-CgtvpMX9.js} +1 -1
  24. package/dist/client/assets/{SkillsView-BdELqTy7.js → SkillsView-DErYRumF.js} +1 -1
  25. package/dist/client/assets/StashRecoveryView-B_8WIQEo.css +1 -0
  26. package/dist/client/assets/StashRecoveryView-QJrNS4Vg.js +1 -0
  27. package/dist/client/assets/{TodoView-DFNGBDNV.js → TodoView-BD9NRwq0.js} +2 -2
  28. package/dist/client/assets/createLucideIcon-BazL2hk5.js +21 -0
  29. package/dist/client/assets/dashboard-view-BWGH_fAq.js +63 -0
  30. package/dist/client/assets/dashboard-view-BoTzlP8b.css +1 -0
  31. package/dist/client/assets/dashboard-view-DdGlfuu-.css +1 -0
  32. package/dist/client/assets/dashboard-view-Ws9_ZnKu.js +21 -0
  33. package/dist/client/assets/{folder-open-k1xmUMyr.js → folder-open-CHSlllzf.js} +1 -1
  34. package/dist/client/assets/index-DCovGm5b.css +1 -0
  35. package/dist/client/assets/index-bEwSVl7B.js +692 -0
  36. package/dist/client/assets/{star-ne32r3Y4.js → star-BgVwWAPz.js} +1 -1
  37. package/dist/client/assets/{upload-MS-2Gx53.js → upload-CAzycxr9.js} +1 -1
  38. package/dist/client/assets/{users-C519GSjH.js → users-CZnxCCCJ.js} +1 -1
  39. package/dist/client/index.html +2 -2
  40. package/dist/client/version.json +1 -1
  41. package/dist/droid-cli/package.json +1 -1
  42. package/dist/droid-cli/src/__tests__/index.test.ts +228 -0
  43. package/dist/extension.js +15810 -10205
  44. package/dist/pi-claude-cli/package.json +1 -1
  45. package/dist/pi-claude-cli/src/__tests__/provider.test.ts +36 -22
  46. package/dist/pi-claude-cli/src/provider.ts +7 -1
  47. package/dist/plugins/fusion-plugin-cli-printing-press/manifest.json +24 -0
  48. package/dist/plugins/fusion-plugin-cli-printing-press/package.json +44 -0
  49. package/dist/plugins/fusion-plugin-cli-printing-press/src/__tests__/TestRunnerPanel.test.tsx +99 -0
  50. package/dist/plugins/fusion-plugin-cli-printing-press/src/__tests__/config-flow.test.ts +91 -0
  51. package/dist/plugins/fusion-plugin-cli-printing-press/src/__tests__/dashboard-view.test.tsx +40 -0
  52. package/dist/plugins/fusion-plugin-cli-printing-press/src/__tests__/dashboard-views.test.ts +46 -0
  53. package/dist/plugins/fusion-plugin-cli-printing-press/src/__tests__/draft-store.test.ts +50 -0
  54. package/dist/plugins/fusion-plugin-cli-printing-press/src/__tests__/fixtures/exec-mock.ts +80 -0
  55. package/dist/plugins/fusion-plugin-cli-printing-press/src/__tests__/fixtures/fixtures.test.ts +40 -0
  56. package/dist/plugins/fusion-plugin-cli-printing-press/src/__tests__/fixtures/registry.ts +82 -0
  57. package/dist/plugins/fusion-plugin-cli-printing-press/src/__tests__/generator.test.ts +54 -0
  58. package/dist/plugins/fusion-plugin-cli-printing-press/src/__tests__/manage-view.test.tsx +98 -0
  59. package/dist/plugins/fusion-plugin-cli-printing-press/src/__tests__/manifest.test.ts +36 -0
  60. package/dist/plugins/fusion-plugin-cli-printing-press/src/__tests__/registration.test.ts +29 -0
  61. package/dist/plugins/fusion-plugin-cli-printing-press/src/__tests__/run-routes.test.ts +98 -0
  62. package/dist/plugins/fusion-plugin-cli-printing-press/src/__tests__/runner.test.ts +55 -0
  63. package/dist/plugins/fusion-plugin-cli-printing-press/src/__tests__/runtime-availability.test.ts +61 -0
  64. package/dist/plugins/fusion-plugin-cli-printing-press/src/__tests__/validation.test.ts +30 -0
  65. package/dist/plugins/fusion-plugin-cli-printing-press/src/__tests__/wizard-routes.test.ts +61 -0
  66. package/dist/plugins/fusion-plugin-cli-printing-press/src/__tests__/workflow-integration.test.ts +19 -0
  67. package/dist/plugins/fusion-plugin-cli-printing-press/src/dashboard-view.css +43 -0
  68. package/dist/plugins/fusion-plugin-cli-printing-press/src/dashboard-view.tsx +49 -0
  69. package/dist/plugins/fusion-plugin-cli-printing-press/src/generation/generator.ts +95 -0
  70. package/dist/plugins/fusion-plugin-cli-printing-press/src/generation/redact.ts +9 -0
  71. package/dist/plugins/fusion-plugin-cli-printing-press/src/generation/runner.ts +79 -0
  72. package/dist/plugins/fusion-plugin-cli-printing-press/src/generation/types.ts +31 -0
  73. package/dist/plugins/fusion-plugin-cli-printing-press/src/index.ts +58 -0
  74. package/dist/plugins/fusion-plugin-cli-printing-press/src/manage/EditDraftModal.tsx +75 -0
  75. package/dist/plugins/fusion-plugin-cli-printing-press/src/manage/useDrafts.ts +73 -0
  76. package/dist/plugins/fusion-plugin-cli-printing-press/src/manage-view.css +79 -0
  77. package/dist/plugins/fusion-plugin-cli-printing-press/src/manage-view.tsx +122 -0
  78. package/dist/plugins/fusion-plugin-cli-printing-press/src/routes/wizard-routes.ts +272 -0
  79. package/dist/plugins/fusion-plugin-cli-printing-press/src/run/TestRunnerPanel.css +70 -0
  80. package/dist/plugins/fusion-plugin-cli-printing-press/src/run/TestRunnerPanel.tsx +98 -0
  81. package/dist/plugins/fusion-plugin-cli-printing-press/src/run/useRunGeneratedCli.ts +37 -0
  82. package/dist/plugins/fusion-plugin-cli-printing-press/src/runtime/__tests__/executor-runtime-env.test.ts +191 -0
  83. package/dist/plugins/fusion-plugin-cli-printing-press/src/runtime/executor-runtime-env.ts +75 -0
  84. package/dist/plugins/fusion-plugin-cli-printing-press/src/storage/draft-store.ts +85 -0
  85. package/dist/plugins/fusion-plugin-cli-printing-press/src/store/__tests__/cli-press-store.test.ts +128 -0
  86. package/dist/plugins/fusion-plugin-cli-printing-press/src/store/__tests__/credentials.test.ts +62 -0
  87. package/dist/plugins/fusion-plugin-cli-printing-press/src/store/cli-press-store.ts +427 -0
  88. package/dist/plugins/fusion-plugin-cli-printing-press/src/store/cli-press-types.ts +110 -0
  89. package/dist/plugins/fusion-plugin-cli-printing-press/src/store/credentials.ts +95 -0
  90. package/dist/plugins/fusion-plugin-cli-printing-press/src/wizard/steps.tsx +55 -0
  91. package/dist/plugins/fusion-plugin-cli-printing-press/src/wizard/types.ts +33 -0
  92. package/dist/plugins/fusion-plugin-cli-printing-press/src/wizard/validation.ts +63 -0
  93. package/dist/plugins/fusion-plugin-cursor-runtime/package.json +1 -1
  94. package/dist/plugins/fusion-plugin-dependency-graph/package.json +1 -1
  95. package/dist/plugins/fusion-plugin-droid-runtime/package.json +1 -1
  96. package/dist/plugins/fusion-plugin-hermes-runtime/package.json +1 -1
  97. package/dist/plugins/fusion-plugin-openclaw-runtime/package.json +1 -1
  98. package/dist/plugins/fusion-plugin-paperclip-runtime/package.json +1 -1
  99. package/dist/plugins/fusion-plugin-reports/manifest.json +10 -0
  100. package/dist/plugins/fusion-plugin-reports/package.json +18 -2
  101. package/dist/plugins/fusion-plugin-reports/src/__tests__/approval.test.ts +164 -0
  102. package/dist/plugins/fusion-plugin-reports/src/__tests__/manifest.test.ts +14 -0
  103. package/dist/plugins/fusion-plugin-reports/src/__tests__/routes-approval.test.ts +109 -0
  104. package/dist/plugins/fusion-plugin-reports/src/__tests__/scaffold.test.ts +60 -0
  105. package/dist/plugins/fusion-plugin-reports/src/__tests__/share-blocks.test.ts +83 -0
  106. package/dist/plugins/fusion-plugin-reports/src/aggregation.ts +23 -0
  107. package/dist/plugins/fusion-plugin-reports/src/approval.ts +97 -0
  108. package/dist/plugins/fusion-plugin-reports/src/cadence.ts +23 -0
  109. package/dist/plugins/fusion-plugin-reports/src/dashboard/ReportsView.css +82 -0
  110. package/dist/plugins/fusion-plugin-reports/src/dashboard/ReportsView.tsx +24 -0
  111. package/dist/plugins/fusion-plugin-reports/src/dashboard/__tests__/ReportComparisonDrawer.test.tsx +12 -0
  112. package/dist/plugins/fusion-plugin-reports/src/dashboard/__tests__/ReportDetailPanel.test.tsx +12 -0
  113. package/dist/plugins/fusion-plugin-reports/src/dashboard/__tests__/ReportFiltersBar.test.tsx +14 -0
  114. package/dist/plugins/fusion-plugin-reports/src/dashboard/__tests__/ReportsView.test.tsx +27 -0
  115. package/dist/plugins/fusion-plugin-reports/src/dashboard/__tests__/api.test.ts +19 -0
  116. package/dist/plugins/fusion-plugin-reports/src/dashboard/__tests__/useReportSectionDiff.test.ts +11 -0
  117. package/dist/plugins/fusion-plugin-reports/src/dashboard/__tests__/useReports.test.ts +13 -0
  118. package/dist/plugins/fusion-plugin-reports/src/dashboard/api.ts +85 -0
  119. package/dist/plugins/fusion-plugin-reports/src/dashboard/components/ReportApprovalPanel.css +59 -0
  120. package/dist/plugins/fusion-plugin-reports/src/dashboard/components/ReportApprovalPanel.tsx +58 -0
  121. package/dist/plugins/fusion-plugin-reports/src/dashboard/components/ReportComparisonDrawer.tsx +21 -0
  122. package/dist/plugins/fusion-plugin-reports/src/dashboard/components/ReportDetailPanel.tsx +29 -0
  123. package/dist/plugins/fusion-plugin-reports/src/dashboard/components/ReportEmptyState.tsx +3 -0
  124. package/dist/plugins/fusion-plugin-reports/src/dashboard/components/ReportFiltersBar.tsx +19 -0
  125. package/dist/plugins/fusion-plugin-reports/src/dashboard/components/ReportListItem.tsx +8 -0
  126. package/dist/plugins/fusion-plugin-reports/src/dashboard/components/ShareBlocksPanel.css +29 -0
  127. package/dist/plugins/fusion-plugin-reports/src/dashboard/components/ShareBlocksPanel.tsx +43 -0
  128. package/dist/plugins/fusion-plugin-reports/src/dashboard/components/__tests__/ReportApprovalPanel.test.tsx +38 -0
  129. package/dist/plugins/fusion-plugin-reports/src/dashboard/components/__tests__/ShareBlocksPanel.test.tsx +24 -0
  130. package/dist/plugins/fusion-plugin-reports/src/dashboard/test-setup.ts +18 -0
  131. package/dist/plugins/fusion-plugin-reports/src/dashboard/types.ts +22 -0
  132. package/dist/plugins/fusion-plugin-reports/src/dashboard/useReportPreview.ts +44 -0
  133. package/dist/plugins/fusion-plugin-reports/src/dashboard/useReportSectionDiff.ts +59 -0
  134. package/dist/plugins/fusion-plugin-reports/src/dashboard/useReports.ts +71 -0
  135. package/dist/plugins/fusion-plugin-reports/src/dashboard/useViewportMode.ts +13 -0
  136. package/dist/plugins/fusion-plugin-reports/src/dashboard-view.tsx +6 -0
  137. package/dist/plugins/fusion-plugin-reports/src/index.ts +48 -2
  138. package/dist/plugins/fusion-plugin-reports/src/pipeline.ts +58 -0
  139. package/dist/plugins/fusion-plugin-reports/src/render/__tests__/escape.test.ts +20 -0
  140. package/dist/plugins/fusion-plugin-reports/src/render/__tests__/html-template.test.ts +110 -0
  141. package/dist/plugins/fusion-plugin-reports/src/render/__tests__/standalone-html.test.ts +66 -0
  142. package/dist/plugins/fusion-plugin-reports/src/render/escape.ts +12 -0
  143. package/dist/plugins/fusion-plugin-reports/src/render/html-styles.ts +40 -0
  144. package/dist/plugins/fusion-plugin-reports/src/render/html-template.ts +137 -0
  145. package/dist/plugins/fusion-plugin-reports/src/render/index.ts +4 -0
  146. package/dist/plugins/fusion-plugin-reports/src/render/standalone-html.ts +75 -0
  147. package/dist/plugins/fusion-plugin-reports/src/report-schema.ts +31 -0
  148. package/dist/plugins/fusion-plugin-reports/src/routes/__tests__/report-export-routes.test.ts +104 -0
  149. package/dist/plugins/fusion-plugin-reports/src/routes/report-approval-routes.ts +98 -0
  150. package/dist/plugins/fusion-plugin-reports/src/routes/report-export-routes.ts +77 -0
  151. package/dist/plugins/fusion-plugin-reports/src/routes/report-list-routes.ts +72 -0
  152. package/dist/plugins/fusion-plugin-reports/src/runs-store.ts +69 -0
  153. package/dist/plugins/fusion-plugin-reports/src/share-blocks.ts +82 -0
  154. package/dist/plugins/fusion-plugin-reports/src/store/report-store.ts +51 -2
  155. package/dist/plugins/fusion-plugin-reports/src/store/report-types.ts +6 -1
  156. package/dist/plugins/fusion-plugin-roadmap/bundled.js +1672 -0
  157. package/dist/plugins/fusion-plugin-roadmap/manifest.json +1 -1
  158. package/dist/plugins/fusion-plugin-roadmap/package.json +4 -41
  159. package/dist/plugins/fusion-plugin-whatsapp-chat/package.json +1 -1
  160. package/package.json +2 -3
  161. package/skill/fusion/references/engine-tools.md +1 -1
  162. package/skill/fusion/references/extension-tools.md +3 -3
  163. package/skill/fusion/references/fusion-capabilities.md +1 -1
  164. package/dist/client/assets/AgentDetailView-BwJaLqZh.css +0 -1
  165. package/dist/client/assets/AgentDetailView-ZbHEbYRT.js +0 -18
  166. package/dist/client/assets/ChatView-DhPkiEGs.js +0 -1
  167. package/dist/client/assets/InsightsView-vp0RE8Mg.js +0 -11
  168. package/dist/client/assets/NodesView-DMj6HGeC.js +0 -14
  169. package/dist/client/assets/PluginManager-BtYKm8IT.js +0 -1
  170. package/dist/client/assets/ResearchView-BhWqfdV0.js +0 -1
  171. package/dist/client/assets/SettingsModal-BAgB4_AR.js +0 -31
  172. package/dist/client/assets/SettingsModal-BNSrO1M9.css +0 -1
  173. package/dist/client/assets/SettingsModal-DzsLquBu.css +0 -1
  174. package/dist/client/assets/index-Qq2JOOWx.css +0 -1
  175. package/dist/client/assets/index-TFYXEVpn.js +0 -692
  176. package/dist/plugins/fusion-plugin-roadmap/src/__tests__/api-client.test.ts +0 -101
  177. package/dist/plugins/fusion-plugin-roadmap/src/__tests__/index.test.ts +0 -92
  178. package/dist/plugins/fusion-plugin-roadmap/src/__tests__/roadmap-routes.test.ts +0 -48
  179. package/dist/plugins/fusion-plugin-roadmap/src/__tests__/roadmap-suggestions.test.ts +0 -31
  180. package/dist/plugins/fusion-plugin-roadmap/src/dashboard/RoadmapsView.css +0 -1299
  181. package/dist/plugins/fusion-plugin-roadmap/src/dashboard/RoadmapsView.tsx +0 -2559
  182. package/dist/plugins/fusion-plugin-roadmap/src/dashboard/__tests__/RoadmapsView.test.tsx +0 -1144
  183. package/dist/plugins/fusion-plugin-roadmap/src/dashboard/__tests__/useRoadmaps.test.ts +0 -1756
  184. package/dist/plugins/fusion-plugin-roadmap/src/dashboard/api.ts +0 -70
  185. package/dist/plugins/fusion-plugin-roadmap/src/dashboard/test-setup.ts +0 -7
  186. package/dist/plugins/fusion-plugin-roadmap/src/dashboard/types.ts +0 -1
  187. package/dist/plugins/fusion-plugin-roadmap/src/dashboard/useConfirm.ts +0 -8
  188. package/dist/plugins/fusion-plugin-roadmap/src/dashboard/useRoadmaps.ts +0 -1188
  189. package/dist/plugins/fusion-plugin-roadmap/src/dashboard/useViewportMode.ts +0 -20
  190. package/dist/plugins/fusion-plugin-roadmap/src/dashboard-view.tsx +0 -6
  191. package/dist/plugins/fusion-plugin-roadmap/src/index.ts +0 -74
  192. package/dist/plugins/fusion-plugin-roadmap/src/roadmap-routes.ts +0 -1
  193. package/dist/plugins/fusion-plugin-roadmap/src/roadmap-schema.ts +0 -41
  194. package/dist/plugins/fusion-plugin-roadmap/src/roadmap-suggestions.d.ts +0 -15
  195. package/dist/plugins/fusion-plugin-roadmap/src/roadmap-suggestions.ts +0 -15
  196. package/dist/plugins/fusion-plugin-roadmap/src/roadmap-types.d.ts +0 -283
  197. package/dist/plugins/fusion-plugin-roadmap/src/roadmap-types.d.ts.map +0 -1
  198. package/dist/plugins/fusion-plugin-roadmap/src/roadmap-types.js +0 -21
  199. package/dist/plugins/fusion-plugin-roadmap/src/roadmap-types.js.map +0 -1
  200. package/dist/plugins/fusion-plugin-roadmap/src/roadmap-types.ts +0 -310
  201. package/dist/plugins/fusion-plugin-roadmap/src/routes/roadmap-routes.d.ts +0 -5
  202. package/dist/plugins/fusion-plugin-roadmap/src/routes/roadmap-routes.d.ts.map +0 -1
  203. package/dist/plugins/fusion-plugin-roadmap/src/routes/roadmap-routes.js +0 -361
  204. package/dist/plugins/fusion-plugin-roadmap/src/routes/roadmap-routes.js.map +0 -1
  205. package/dist/plugins/fusion-plugin-roadmap/src/routes/roadmap-routes.ts +0 -408
  206. package/dist/plugins/fusion-plugin-roadmap/src/routes/roadmap-suggestions.d.ts +0 -68
  207. package/dist/plugins/fusion-plugin-roadmap/src/routes/roadmap-suggestions.d.ts.map +0 -1
  208. package/dist/plugins/fusion-plugin-roadmap/src/routes/roadmap-suggestions.js +0 -300
  209. package/dist/plugins/fusion-plugin-roadmap/src/routes/roadmap-suggestions.js.map +0 -1
  210. package/dist/plugins/fusion-plugin-roadmap/src/routes/roadmap-suggestions.ts +0 -381
  211. package/dist/plugins/fusion-plugin-roadmap/src/server/index.d.ts +0 -3
  212. package/dist/plugins/fusion-plugin-roadmap/src/server/index.ts +0 -1
  213. package/dist/plugins/fusion-plugin-roadmap/src/store/__tests__/roadmap-handoff.test.ts +0 -445
  214. package/dist/plugins/fusion-plugin-roadmap/src/store/__tests__/roadmap-ordering.test.ts +0 -334
  215. package/dist/plugins/fusion-plugin-roadmap/src/store/__tests__/roadmap-store.test.ts +0 -1318
  216. package/dist/plugins/fusion-plugin-roadmap/src/store/roadmap-handoff.ts +0 -163
  217. package/dist/plugins/fusion-plugin-roadmap/src/store/roadmap-ordering.d.ts +0 -37
  218. package/dist/plugins/fusion-plugin-roadmap/src/store/roadmap-ordering.d.ts.map +0 -1
  219. package/dist/plugins/fusion-plugin-roadmap/src/store/roadmap-ordering.js +0 -188
  220. package/dist/plugins/fusion-plugin-roadmap/src/store/roadmap-ordering.js.map +0 -1
  221. package/dist/plugins/fusion-plugin-roadmap/src/store/roadmap-ordering.ts +0 -311
  222. package/dist/plugins/fusion-plugin-roadmap/src/store/roadmap-store.d.ts +0 -299
  223. package/dist/plugins/fusion-plugin-roadmap/src/store/roadmap-store.d.ts.map +0 -1
  224. package/dist/plugins/fusion-plugin-roadmap/src/store/roadmap-store.js +0 -765
  225. package/dist/plugins/fusion-plugin-roadmap/src/store/roadmap-store.js.map +0 -1
  226. package/dist/plugins/fusion-plugin-roadmap/src/store/roadmap-store.ts +0 -1001
@@ -1,1001 +0,0 @@
1
- /**
2
- * RoadmapStore - Data layer for standalone roadmap persistence.
3
- *
4
- * Manages CRUD operations for roadmaps, milestones, and features.
5
- * Provides deterministic ordering via covering indexes and atomic reorder/move operations.
6
- *
7
- * Ordering invariants:
8
- * - milestone ordering is scoped to a single roadmap and must be contiguous + 0-based
9
- * - feature ordering is scoped to a single milestone and must be contiguous + 0-based
10
- * - all list/read queries use deterministic ordering: ORDER BY orderIndex ASC, createdAt ASC, id ASC
11
- * - cross-milestone feature moves atomically renumber both affected milestone scopes
12
- */
13
-
14
- import { EventEmitter } from "node:events";
15
- import type { Database } from "@fusion/core";
16
- import type {
17
- Roadmap,
18
- RoadmapMilestone,
19
- RoadmapFeature,
20
- RoadmapCreateInput,
21
- RoadmapUpdateInput,
22
- RoadmapMilestoneCreateInput,
23
- RoadmapMilestoneUpdateInput,
24
- RoadmapFeatureCreateInput,
25
- RoadmapFeatureUpdateInput,
26
- RoadmapMilestoneReorderInput,
27
- RoadmapFeatureReorderInput,
28
- RoadmapFeatureMoveInput,
29
- RoadmapMilestoneWithFeatures,
30
- RoadmapWithHierarchy,
31
- RoadmapExportBundle,
32
- RoadmapMissionPlanningHandoff,
33
- RoadmapFeatureTaskPlanningHandoff,
34
- RoadmapFeatureSourceRef,
35
- } from "../roadmap-types.js";
36
- import {
37
- applyRoadmapMilestoneReorder,
38
- applyRoadmapFeatureReorder,
39
- moveRoadmapFeature,
40
- } from "./roadmap-ordering.js";
41
-
42
- // ── Event Types ─────────────────────────────────────────────────────
43
-
44
- export interface RoadmapStoreEvents {
45
- /** Emitted when a roadmap is created */
46
- "roadmap:created": [Roadmap];
47
- /** Emitted when a roadmap is updated */
48
- "roadmap:updated": [Roadmap];
49
- /** Emitted when a roadmap is deleted */
50
- "roadmap:deleted": [string];
51
- /** Emitted when a milestone is created */
52
- "milestone:created": [RoadmapMilestone];
53
- /** Emitted when a milestone is updated */
54
- "milestone:updated": [RoadmapMilestone];
55
- /** Emitted when a milestone is deleted */
56
- "milestone:deleted": [string];
57
- /** Emitted when a milestone is reordered */
58
- "milestone:reordered": [{ roadmapId: string; milestones: RoadmapMilestone[] }];
59
- /** Emitted when a feature is created */
60
- "feature:created": [RoadmapFeature];
61
- /** Emitted when a feature is updated */
62
- "feature:updated": [RoadmapFeature];
63
- /** Emitted when a feature is deleted */
64
- "feature:deleted": [RoadmapFeature];
65
- /** Emitted when features are reordered within a milestone */
66
- "feature:reordered": [{ milestoneId: string; features: RoadmapFeature[] }];
67
- /** Emitted when a feature is moved (including cross-milestone moves) */
68
- "feature:moved": [{ feature: RoadmapFeature; fromMilestoneId: string; toMilestoneId: string }];
69
- }
70
-
71
- // ── Row Interfaces ──────────────────────────────────────────────────
72
-
73
- /** Database row shape for roadmaps. */
74
- interface RoadmapRow {
75
- id: string;
76
- title: string;
77
- description: string | null;
78
- createdAt: string;
79
- updatedAt: string;
80
- }
81
-
82
- /** Database row shape for roadmap_milestones. */
83
- interface RoadmapMilestoneRow {
84
- id: string;
85
- roadmapId: string;
86
- title: string;
87
- description: string | null;
88
- orderIndex: number;
89
- createdAt: string;
90
- updatedAt: string;
91
- }
92
-
93
- /** Database row shape for roadmap_features. */
94
- interface RoadmapFeatureRow {
95
- id: string;
96
- milestoneId: string;
97
- title: string;
98
- description: string | null;
99
- orderIndex: number;
100
- createdAt: string;
101
- updatedAt: string;
102
- }
103
-
104
- // ── RoadmapStore Class ──────────────────────────────────────────────
105
-
106
- export class RoadmapStore extends EventEmitter<RoadmapStoreEvents> {
107
- /**
108
- * Creates a new RoadmapStore instance.
109
- *
110
- * @param db - Shared Database instance (same instance used by TaskStore)
111
- */
112
- constructor(private db: Database) {
113
- super();
114
- this.setMaxListeners(50);
115
- this.ensureSchema();
116
- }
117
-
118
- private ensureSchema(): void {
119
- this.db.exec(`
120
- CREATE TABLE IF NOT EXISTS roadmaps (
121
- id TEXT PRIMARY KEY,
122
- title TEXT NOT NULL,
123
- description TEXT,
124
- createdAt TEXT NOT NULL,
125
- updatedAt TEXT NOT NULL
126
- );
127
-
128
- CREATE TABLE IF NOT EXISTS roadmap_milestones (
129
- id TEXT PRIMARY KEY,
130
- roadmapId TEXT NOT NULL,
131
- title TEXT NOT NULL,
132
- description TEXT,
133
- orderIndex INTEGER NOT NULL,
134
- createdAt TEXT NOT NULL,
135
- updatedAt TEXT NOT NULL,
136
- FOREIGN KEY (roadmapId) REFERENCES roadmaps(id) ON DELETE CASCADE
137
- );
138
-
139
- CREATE TABLE IF NOT EXISTS roadmap_features (
140
- id TEXT PRIMARY KEY,
141
- milestoneId TEXT NOT NULL,
142
- title TEXT NOT NULL,
143
- description TEXT,
144
- orderIndex INTEGER NOT NULL,
145
- createdAt TEXT NOT NULL,
146
- updatedAt TEXT NOT NULL,
147
- FOREIGN KEY (milestoneId) REFERENCES roadmap_milestones(id) ON DELETE CASCADE
148
- );
149
-
150
- CREATE INDEX IF NOT EXISTS idxRoadmapMilestonesRoadmapOrder
151
- ON roadmap_milestones(roadmapId, orderIndex, createdAt, id);
152
-
153
- CREATE INDEX IF NOT EXISTS idxRoadmapFeaturesMilestoneOrder
154
- ON roadmap_features(milestoneId, orderIndex, createdAt, id);
155
- `);
156
- }
157
-
158
- // ── ID Generators ───────────────────────────────────────────────────
159
-
160
- private generateRoadmapId(): string {
161
- const timestamp = Date.now();
162
- const random = Math.random().toString(36).substring(2, 6).toUpperCase();
163
- return `RM-${timestamp.toString(36).toUpperCase()}-${random}`;
164
- }
165
-
166
- private generateMilestoneId(): string {
167
- const timestamp = Date.now();
168
- const random = Math.random().toString(36).substring(2, 6).toUpperCase();
169
- return `RMS-${timestamp.toString(36).toUpperCase()}-${random}`;
170
- }
171
-
172
- private generateFeatureId(): string {
173
- const timestamp = Date.now();
174
- const random = Math.random().toString(36).substring(2, 6).toUpperCase();
175
- return `RF-${timestamp.toString(36).toUpperCase()}-${random}`;
176
- }
177
-
178
- // ── Row-to-Object Converters ───────────────────────────────────────
179
-
180
- private rowToRoadmap(row: RoadmapRow): Roadmap {
181
- return {
182
- id: row.id,
183
- title: row.title,
184
- description: row.description || undefined,
185
- createdAt: row.createdAt,
186
- updatedAt: row.updatedAt,
187
- };
188
- }
189
-
190
- private rowToMilestone(row: RoadmapMilestoneRow): RoadmapMilestone {
191
- return {
192
- id: row.id,
193
- roadmapId: row.roadmapId,
194
- title: row.title,
195
- description: row.description || undefined,
196
- orderIndex: row.orderIndex,
197
- createdAt: row.createdAt,
198
- updatedAt: row.updatedAt,
199
- };
200
- }
201
-
202
- private rowToFeature(row: RoadmapFeatureRow): RoadmapFeature {
203
- return {
204
- id: row.id,
205
- milestoneId: row.milestoneId,
206
- title: row.title,
207
- description: row.description || undefined,
208
- orderIndex: row.orderIndex,
209
- createdAt: row.createdAt,
210
- updatedAt: row.updatedAt,
211
- };
212
- }
213
-
214
- // ── Roadmap CRUD ─────────────────────────────────────────────────
215
-
216
- /**
217
- * Create a new roadmap.
218
- *
219
- * @param input - Roadmap creation input
220
- * @returns The created roadmap
221
- */
222
- createRoadmap(input: RoadmapCreateInput): Roadmap {
223
- const now = new Date().toISOString();
224
- const id = this.generateRoadmapId();
225
-
226
- const roadmap: Roadmap = {
227
- id,
228
- title: input.title,
229
- description: input.description,
230
- createdAt: now,
231
- updatedAt: now,
232
- };
233
-
234
- this.db.prepare(`
235
- INSERT INTO roadmaps (id, title, description, createdAt, updatedAt)
236
- VALUES (?, ?, ?, ?, ?)
237
- `).run(
238
- roadmap.id,
239
- roadmap.title,
240
- roadmap.description ?? null,
241
- roadmap.createdAt,
242
- roadmap.updatedAt,
243
- );
244
-
245
- this.db.bumpLastModified();
246
- this.emit("roadmap:created", roadmap);
247
- return roadmap;
248
- }
249
-
250
- /**
251
- * Get a roadmap by ID.
252
- *
253
- * @param id - Roadmap ID
254
- * @returns The roadmap, or undefined if not found
255
- */
256
- getRoadmap(id: string): Roadmap | undefined {
257
- const row = this.db.prepare("SELECT * FROM roadmaps WHERE id = ?").get(id) as unknown as RoadmapRow | undefined;
258
- if (!row) return undefined;
259
- return this.rowToRoadmap(row);
260
- }
261
-
262
- /**
263
- * List all roadmaps, ordered by creation date (newest first).
264
- *
265
- * @returns Array of roadmaps
266
- */
267
- listRoadmaps(): Roadmap[] {
268
- const rows = this.db.prepare(
269
- "SELECT * FROM roadmaps ORDER BY createdAt DESC"
270
- ).all();
271
- return (rows as unknown as RoadmapRow[]).map((row) => this.rowToRoadmap(row));
272
- }
273
-
274
- /**
275
- * Update a roadmap.
276
- *
277
- * @param id - Roadmap ID
278
- * @param updates - Partial roadmap updates
279
- * @returns The updated roadmap
280
- * @throws Error if roadmap not found
281
- */
282
- updateRoadmap(id: string, updates: RoadmapUpdateInput): Roadmap {
283
- const roadmap = this.getRoadmap(id);
284
- if (!roadmap) {
285
- throw new Error(`Roadmap ${id} not found`);
286
- }
287
-
288
- const updated: Roadmap = {
289
- ...roadmap,
290
- ...updates,
291
- id, // Prevent changing ID
292
- createdAt: roadmap.createdAt, // Prevent changing creation time
293
- updatedAt: new Date().toISOString(),
294
- };
295
-
296
- this.db.prepare(`
297
- UPDATE roadmaps SET
298
- title = ?,
299
- description = ?,
300
- updatedAt = ?
301
- WHERE id = ?
302
- `).run(
303
- updated.title,
304
- updated.description ?? null,
305
- updated.updatedAt,
306
- updated.id,
307
- );
308
-
309
- this.db.bumpLastModified();
310
- this.emit("roadmap:updated", updated);
311
- return updated;
312
- }
313
-
314
- /**
315
- * Delete a roadmap and all its milestones/features (cascading).
316
- *
317
- * @param id - Roadmap ID
318
- * @throws Error if roadmap not found
319
- */
320
- deleteRoadmap(id: string): void {
321
- const roadmap = this.getRoadmap(id);
322
- if (!roadmap) {
323
- throw new Error(`Roadmap ${id} not found`);
324
- }
325
-
326
- // SQLite FK cascade will handle milestones and features
327
- this.db.prepare("DELETE FROM roadmaps WHERE id = ?").run(id);
328
- this.db.bumpLastModified();
329
-
330
- this.emit("roadmap:deleted", id);
331
- }
332
-
333
- // ── Milestone CRUD ────────────────────────────────────────────────
334
-
335
- /**
336
- * Add a milestone to a roadmap.
337
- * Automatically computes the orderIndex (max + 1).
338
- *
339
- * @param roadmapId - Parent roadmap ID
340
- * @param input - Milestone creation input
341
- * @returns The created milestone
342
- * @throws Error if roadmap not found
343
- */
344
- createMilestone(roadmapId: string, input: RoadmapMilestoneCreateInput): RoadmapMilestone {
345
- const roadmap = this.getRoadmap(roadmapId);
346
- if (!roadmap) {
347
- throw new Error(`Roadmap ${roadmapId} not found`);
348
- }
349
-
350
- const now = new Date().toISOString();
351
- const id = this.generateMilestoneId();
352
-
353
- // Compute next orderIndex
354
- const existingMilestones = this.listMilestones(roadmapId);
355
- const orderIndex = existingMilestones.length > 0
356
- ? Math.max(...existingMilestones.map((m) => m.orderIndex)) + 1
357
- : 0;
358
-
359
- const milestone: RoadmapMilestone = {
360
- id,
361
- roadmapId,
362
- title: input.title,
363
- description: input.description,
364
- orderIndex,
365
- createdAt: now,
366
- updatedAt: now,
367
- };
368
-
369
- this.db.prepare(`
370
- INSERT INTO roadmap_milestones (id, roadmapId, title, description, orderIndex, createdAt, updatedAt)
371
- VALUES (?, ?, ?, ?, ?, ?, ?)
372
- `).run(
373
- milestone.id,
374
- milestone.roadmapId,
375
- milestone.title,
376
- milestone.description ?? null,
377
- milestone.orderIndex,
378
- milestone.createdAt,
379
- milestone.updatedAt,
380
- );
381
-
382
- this.db.bumpLastModified();
383
- this.emit("milestone:created", milestone);
384
- return milestone;
385
- }
386
-
387
- /**
388
- * Get a milestone by ID.
389
- *
390
- * @param id - Milestone ID
391
- * @returns The milestone, or undefined if not found
392
- */
393
- getMilestone(id: string): RoadmapMilestone | undefined {
394
- const row = this.db.prepare("SELECT * FROM roadmap_milestones WHERE id = ?").get(id) as unknown as RoadmapMilestoneRow | undefined;
395
- if (!row) return undefined;
396
- return this.rowToMilestone(row);
397
- }
398
-
399
- /**
400
- * List milestones for a roadmap, ordered deterministically.
401
- *
402
- * Uses deterministic ordering: ORDER BY orderIndex ASC, createdAt ASC, id ASC
403
- * to ensure consistent results when stored order data is incomplete or conflicting.
404
- *
405
- * @param roadmapId - Roadmap ID
406
- * @returns Array of milestones in deterministic order
407
- */
408
- listMilestones(roadmapId: string): RoadmapMilestone[] {
409
- const rows = this.db.prepare(
410
- "SELECT * FROM roadmap_milestones WHERE roadmapId = ? ORDER BY orderIndex ASC, createdAt ASC, id ASC"
411
- ).all(roadmapId);
412
- return (rows as unknown as RoadmapMilestoneRow[]).map((row) => this.rowToMilestone(row));
413
- }
414
-
415
- /**
416
- * Update a milestone.
417
- *
418
- * @param id - Milestone ID
419
- * @param updates - Partial milestone updates
420
- * @returns The updated milestone
421
- * @throws Error if milestone not found
422
- */
423
- updateMilestone(id: string, updates: RoadmapMilestoneUpdateInput): RoadmapMilestone {
424
- const milestone = this.getMilestone(id);
425
- if (!milestone) {
426
- throw new Error(`Milestone ${id} not found`);
427
- }
428
-
429
- const updated: RoadmapMilestone = {
430
- ...milestone,
431
- ...updates,
432
- id, // Prevent changing ID
433
- roadmapId: milestone.roadmapId, // Prevent moving to different roadmap
434
- createdAt: milestone.createdAt, // Prevent changing creation time
435
- updatedAt: new Date().toISOString(),
436
- };
437
-
438
- this.db.prepare(`
439
- UPDATE roadmap_milestones SET
440
- title = ?,
441
- description = ?,
442
- updatedAt = ?
443
- WHERE id = ?
444
- `).run(
445
- updated.title,
446
- updated.description ?? null,
447
- updated.updatedAt,
448
- updated.id,
449
- );
450
-
451
- this.db.bumpLastModified();
452
- this.emit("milestone:updated", updated);
453
- return updated;
454
- }
455
-
456
- /**
457
- * Delete a milestone and all its features (cascading).
458
- *
459
- * @param id - Milestone ID
460
- * @throws Error if milestone not found
461
- */
462
- deleteMilestone(id: string): void {
463
- const milestone = this.getMilestone(id);
464
- if (!milestone) {
465
- throw new Error(`Milestone ${id} not found`);
466
- }
467
-
468
- // SQLite FK cascade will handle features
469
- this.db.prepare("DELETE FROM roadmap_milestones WHERE id = ?").run(id);
470
- this.db.bumpLastModified();
471
-
472
- this.emit("milestone:deleted", id);
473
- }
474
-
475
- // ── Feature CRUD ─────────────────────────────────────────────────
476
-
477
- /**
478
- * Add a feature to a milestone.
479
- * Automatically computes the orderIndex (max + 1).
480
- *
481
- * @param milestoneId - Parent milestone ID
482
- * @param input - Feature creation input
483
- * @returns The created feature
484
- * @throws Error if milestone not found
485
- */
486
- createFeature(milestoneId: string, input: RoadmapFeatureCreateInput): RoadmapFeature {
487
- const milestone = this.getMilestone(milestoneId);
488
- if (!milestone) {
489
- throw new Error(`Milestone ${milestoneId} not found`);
490
- }
491
-
492
- const now = new Date().toISOString();
493
- const id = this.generateFeatureId();
494
-
495
- // Compute next orderIndex
496
- const existingFeatures = this.listFeatures(milestoneId);
497
- const orderIndex = existingFeatures.length > 0
498
- ? Math.max(...existingFeatures.map((f) => f.orderIndex)) + 1
499
- : 0;
500
-
501
- const feature: RoadmapFeature = {
502
- id,
503
- milestoneId,
504
- title: input.title,
505
- description: input.description,
506
- orderIndex,
507
- createdAt: now,
508
- updatedAt: now,
509
- };
510
-
511
- this.db.prepare(`
512
- INSERT INTO roadmap_features (id, milestoneId, title, description, orderIndex, createdAt, updatedAt)
513
- VALUES (?, ?, ?, ?, ?, ?, ?)
514
- `).run(
515
- feature.id,
516
- feature.milestoneId,
517
- feature.title,
518
- feature.description ?? null,
519
- feature.orderIndex,
520
- feature.createdAt,
521
- feature.updatedAt,
522
- );
523
-
524
- this.db.bumpLastModified();
525
- this.emit("feature:created", feature);
526
- return feature;
527
- }
528
-
529
- /**
530
- * Get a feature by ID.
531
- *
532
- * @param id - Feature ID
533
- * @returns The feature, or undefined if not found
534
- */
535
- getFeature(id: string): RoadmapFeature | undefined {
536
- const row = this.db.prepare("SELECT * FROM roadmap_features WHERE id = ?").get(id) as unknown as RoadmapFeatureRow | undefined;
537
- if (!row) return undefined;
538
- return this.rowToFeature(row);
539
- }
540
-
541
- /**
542
- * List features for a milestone, ordered deterministically.
543
- *
544
- * Uses deterministic ordering: ORDER BY orderIndex ASC, createdAt ASC, id ASC
545
- * to ensure consistent results when stored order data is incomplete or conflicting.
546
- *
547
- * @param milestoneId - Milestone ID
548
- * @returns Array of features in deterministic order
549
- */
550
- listFeatures(milestoneId: string): RoadmapFeature[] {
551
- const rows = this.db.prepare(
552
- "SELECT * FROM roadmap_features WHERE milestoneId = ? ORDER BY orderIndex ASC, createdAt ASC, id ASC"
553
- ).all(milestoneId);
554
- return (rows as unknown as RoadmapFeatureRow[]).map((row) => this.rowToFeature(row));
555
- }
556
-
557
- /**
558
- * Update a feature.
559
- *
560
- * @param id - Feature ID
561
- * @param updates - Partial feature updates
562
- * @returns The updated feature
563
- * @throws Error if feature not found
564
- */
565
- updateFeature(id: string, updates: RoadmapFeatureUpdateInput): RoadmapFeature {
566
- const feature = this.getFeature(id);
567
- if (!feature) {
568
- throw new Error(`Feature ${id} not found`);
569
- }
570
-
571
- const updated: RoadmapFeature = {
572
- ...feature,
573
- ...updates,
574
- id, // Prevent changing ID
575
- milestoneId: feature.milestoneId, // Prevent moving via update (use moveFeature instead)
576
- createdAt: feature.createdAt, // Prevent changing creation time
577
- updatedAt: new Date().toISOString(),
578
- };
579
-
580
- this.db.prepare(`
581
- UPDATE roadmap_features SET
582
- title = ?,
583
- description = ?,
584
- updatedAt = ?
585
- WHERE id = ?
586
- `).run(
587
- updated.title,
588
- updated.description ?? null,
589
- updated.updatedAt,
590
- updated.id,
591
- );
592
-
593
- this.db.bumpLastModified();
594
- this.emit("feature:updated", updated);
595
- return updated;
596
- }
597
-
598
- /**
599
- * Delete a feature.
600
- *
601
- * @param id - Feature ID
602
- * @throws Error if feature not found
603
- */
604
- deleteFeature(id: string): void {
605
- const feature = this.getFeature(id);
606
- if (!feature) {
607
- throw new Error(`Feature ${id} not found`);
608
- }
609
-
610
- this.db.prepare("DELETE FROM roadmap_features WHERE id = ?").run(id);
611
- this.db.bumpLastModified();
612
-
613
- this.emit("feature:deleted", feature);
614
- }
615
-
616
- // ── Reorder Operations ────────────────────────────────────────────
617
-
618
- /**
619
- * Reorder milestones within a roadmap.
620
- *
621
- * Applies an explicit reorder input and persists the full normalized order.
622
- * The input must contain all milestone IDs exactly once.
623
- *
624
- * @param input - Reorder input with complete milestone ID list
625
- * @returns The reordered milestones in their new order
626
- * @throws Error if milestone set is incomplete, duplicate, or not found
627
- */
628
- reorderMilestones(input: RoadmapMilestoneReorderInput): RoadmapMilestone[] {
629
- // Validate roadmap exists
630
- const roadmap = this.getRoadmap(input.roadmapId);
631
- if (!roadmap) {
632
- throw new Error(`Roadmap ${input.roadmapId} not found`);
633
- }
634
-
635
- // Load current milestones with deterministic ordering
636
- const milestones = this.listMilestones(input.roadmapId);
637
-
638
- // Apply the reorder using the pure ordering helper
639
- const reordered = applyRoadmapMilestoneReorder(milestones, input);
640
-
641
- // Persist in a transaction
642
- this.db.transaction(() => {
643
- for (const milestone of reordered) {
644
- this.db.prepare(`
645
- UPDATE roadmap_milestones SET orderIndex = ?, updatedAt = ? WHERE id = ?
646
- `).run(milestone.orderIndex, new Date().toISOString(), milestone.id);
647
- }
648
- });
649
-
650
- this.db.bumpLastModified();
651
- this.emit("milestone:reordered", { roadmapId: input.roadmapId, milestones: reordered });
652
-
653
- return reordered;
654
- }
655
-
656
- /**
657
- * Reorder features within a milestone.
658
- *
659
- * Applies an explicit reorder input and persists the full normalized order.
660
- * The input must contain all feature IDs for the milestone exactly once.
661
- *
662
- * @param input - Reorder input with complete feature ID list
663
- * @returns The reordered features in their new order
664
- * @throws Error if feature set is incomplete, duplicate, or not found
665
- */
666
- reorderFeatures(input: RoadmapFeatureReorderInput): RoadmapFeature[] {
667
- // Validate milestone exists and belongs to the roadmap
668
- const milestone = this.getMilestone(input.milestoneId);
669
- if (!milestone) {
670
- throw new Error(`Milestone ${input.milestoneId} not found`);
671
- }
672
- if (milestone.roadmapId !== input.roadmapId) {
673
- throw new Error(`Milestone ${input.milestoneId} does not belong to roadmap ${input.roadmapId}`);
674
- }
675
-
676
- // Load current features with deterministic ordering
677
- const features = this.listFeatures(input.milestoneId);
678
-
679
- // Apply the reorder using the pure ordering helper
680
- const reordered = applyRoadmapFeatureReorder(features, input);
681
-
682
- // Persist in a transaction
683
- this.db.transaction(() => {
684
- for (const feature of reordered) {
685
- this.db.prepare(`
686
- UPDATE roadmap_features SET orderIndex = ?, updatedAt = ? WHERE id = ?
687
- `).run(feature.orderIndex, new Date().toISOString(), feature.id);
688
- }
689
- });
690
-
691
- this.db.bumpLastModified();
692
- this.emit("feature:reordered", { milestoneId: input.milestoneId, features: reordered });
693
-
694
- return reordered;
695
- }
696
-
697
- /**
698
- * Move a feature, including cross-milestone moves.
699
- *
700
- * Atomically renumbers both the source and destination milestone scopes.
701
- *
702
- * @param input - Move input with source/destination milestone info
703
- * @returns The moved feature and both affected milestone feature lists
704
- * @throws Error if feature or milestone not found, or scope validation fails
705
- */
706
- moveFeature(input: RoadmapFeatureMoveInput): {
707
- movedFeature: RoadmapFeature;
708
- sourceMilestoneFeatures: RoadmapFeature[];
709
- targetMilestoneFeatures: RoadmapFeature[];
710
- } {
711
- // Validate roadmap exists
712
- const roadmap = this.getRoadmap(input.roadmapId);
713
- if (!roadmap) {
714
- throw new Error(`Roadmap ${input.roadmapId} not found`);
715
- }
716
-
717
- // Validate both milestones exist and belong to the roadmap
718
- const fromMilestone = this.getMilestone(input.fromMilestoneId);
719
- const toMilestone = this.getMilestone(input.toMilestoneId);
720
-
721
- if (!fromMilestone) {
722
- throw new Error(`Source milestone ${input.fromMilestoneId} not found`);
723
- }
724
- if (!toMilestone) {
725
- throw new Error(`Destination milestone ${input.toMilestoneId} not found`);
726
- }
727
- if (fromMilestone.roadmapId !== input.roadmapId) {
728
- throw new Error(`Source milestone ${input.fromMilestoneId} does not belong to roadmap ${input.roadmapId}`);
729
- }
730
- if (toMilestone.roadmapId !== input.roadmapId) {
731
- throw new Error(`Destination milestone ${input.toMilestoneId} does not belong to roadmap ${input.roadmapId}`);
732
- }
733
-
734
- // Load features from both milestones with deterministic ordering
735
- const sourceFeatures = this.listFeatures(input.fromMilestoneId);
736
- const targetFeatures = this.listFeatures(input.toMilestoneId);
737
-
738
- // For same-milestone moves, pass only one list to avoid duplication
739
- // For cross-milestone moves, pass the combined list
740
- const allFeatures = input.fromMilestoneId === input.toMilestoneId
741
- ? sourceFeatures
742
- : [...sourceFeatures, ...targetFeatures];
743
-
744
- // Apply the move using the pure ordering helper
745
- const result = moveRoadmapFeature(allFeatures, input);
746
-
747
- // Persist in a transaction
748
- this.db.transaction(() => {
749
- // Update all affected features
750
- for (const feature of result.affectedFeatures) {
751
- this.db.prepare(`
752
- UPDATE roadmap_features SET milestoneId = ?, orderIndex = ?, updatedAt = ? WHERE id = ?
753
- `).run(feature.milestoneId, feature.orderIndex, new Date().toISOString(), feature.id);
754
- }
755
- });
756
-
757
- this.db.bumpLastModified();
758
- this.emit("feature:moved", {
759
- feature: result.movedFeature,
760
- fromMilestoneId: input.fromMilestoneId,
761
- toMilestoneId: input.toMilestoneId,
762
- });
763
-
764
- return {
765
- movedFeature: result.movedFeature,
766
- sourceMilestoneFeatures: result.sourceMilestoneFeatures,
767
- targetMilestoneFeatures: result.targetMilestoneFeatures,
768
- };
769
- }
770
-
771
- // ── Hierarchy Operations ───────────────────────────────────────────
772
-
773
- /**
774
- * Get a milestone with all of its features in deterministic order.
775
- *
776
- * @param id - Milestone ID
777
- * @returns The milestone with features, or undefined if not found
778
- */
779
- getMilestoneWithFeatures(id: string): RoadmapMilestoneWithFeatures | undefined {
780
- const milestone = this.getMilestone(id);
781
- if (!milestone) return undefined;
782
-
783
- return {
784
- ...milestone,
785
- features: this.listFeatures(id),
786
- };
787
- }
788
-
789
- /**
790
- * Get a roadmap with its full hierarchy (milestones → features).
791
- *
792
- * @param id - Roadmap ID
793
- * @returns The roadmap with hierarchy, or undefined if not found
794
- */
795
- getRoadmapWithHierarchy(id: string): RoadmapWithHierarchy | undefined {
796
- const roadmap = this.getRoadmap(id);
797
- if (!roadmap) return undefined;
798
-
799
- return {
800
- ...roadmap,
801
- milestones: this.listMilestones(id).map((milestone) => ({
802
- ...milestone,
803
- features: this.listFeatures(milestone.id),
804
- })),
805
- };
806
- }
807
-
808
- // ── Export / Handoff Operations ────────────────────────────────────
809
-
810
- /**
811
- * Get a flat export bundle for a roadmap.
812
- *
813
- * Returns all roadmap data in a flat structure suitable for persistence,
814
- * APIs, import/export, and sync jobs. Entities are separated so downstream
815
- * persistence layers can upsert by table/collection.
816
- *
817
- * @param roadmapId - Roadmap ID
818
- * @returns The export bundle with ordered entities
819
- * @throws Error if roadmap not found
820
- */
821
- getRoadmapExport(roadmapId: string): RoadmapExportBundle {
822
- const roadmap = this.getRoadmap(roadmapId);
823
- if (!roadmap) {
824
- throw new Error(`Roadmap ${roadmapId} not found`);
825
- }
826
-
827
- const milestones = this.listMilestones(roadmapId);
828
- const allFeatures: RoadmapFeature[] = [];
829
-
830
- for (const milestone of milestones) {
831
- const features = this.listFeatures(milestone.id);
832
- allFeatures.push(...features);
833
- }
834
-
835
- return {
836
- roadmap,
837
- milestones,
838
- features: allFeatures,
839
- };
840
- }
841
-
842
- /**
843
- * Get a mission planning handoff payload for a roadmap.
844
- *
845
- * Converts the roadmap into a mission planning structure while preserving
846
- * source IDs and deterministic order. Does not couple to MissionStore internals.
847
- *
848
- * @param roadmapId - Roadmap ID
849
- * @returns The mission planning handoff payload
850
- * @throws Error if roadmap not found
851
- */
852
- getRoadmapMissionHandoff(roadmapId: string): RoadmapMissionPlanningHandoff {
853
- const roadmap = this.getRoadmap(roadmapId);
854
- if (!roadmap) {
855
- throw new Error(`Roadmap ${roadmapId} not found`);
856
- }
857
-
858
- const milestones = this.listMilestones(roadmapId);
859
-
860
- return {
861
- sourceRoadmapId: roadmap.id,
862
- title: roadmap.title,
863
- description: roadmap.description,
864
- milestones: milestones.map((milestone) => {
865
- const features = this.listFeatures(milestone.id);
866
-
867
- return {
868
- sourceMilestoneId: milestone.id,
869
- title: milestone.title,
870
- description: milestone.description,
871
- orderIndex: milestone.orderIndex,
872
- features: features.map((feature) => ({
873
- sourceFeatureId: feature.id,
874
- title: feature.title,
875
- description: feature.description,
876
- orderIndex: feature.orderIndex,
877
- })),
878
- };
879
- }),
880
- };
881
- }
882
-
883
- /**
884
- * Get a task planning handoff payload for a single roadmap feature.
885
- *
886
- * Returns a self-contained handoff payload for converting a roadmap feature
887
- * into task planning flows without coupling to MissionStore internals.
888
- *
889
- * @param roadmapId - Parent roadmap ID (for validation)
890
- * @param milestoneId - Parent milestone ID (for validation)
891
- * @param featureId - Feature ID to generate handoff for
892
- * @returns The task planning handoff payload
893
- * @throws Error if any entity is not found or if ownership validation fails
894
- */
895
- getRoadmapFeatureHandoff(
896
- roadmapId: string,
897
- milestoneId: string,
898
- featureId: string,
899
- ): RoadmapFeatureTaskPlanningHandoff {
900
- // Validate roadmap exists
901
- const roadmap = this.getRoadmap(roadmapId);
902
- if (!roadmap) {
903
- throw new Error(`Roadmap ${roadmapId} not found`);
904
- }
905
-
906
- // Validate milestone exists and belongs to roadmap
907
- const milestone = this.getMilestone(milestoneId);
908
- if (!milestone) {
909
- throw new Error(`Milestone ${milestoneId} not found`);
910
- }
911
- if (milestone.roadmapId !== roadmapId) {
912
- throw new Error(`Milestone ${milestoneId} does not belong to roadmap ${roadmapId}`);
913
- }
914
-
915
- // Validate feature exists and belongs to milestone
916
- const feature = this.getFeature(featureId);
917
- if (!feature) {
918
- throw new Error(`Feature ${featureId} not found`);
919
- }
920
- if (feature.milestoneId !== milestoneId) {
921
- throw new Error(`Feature ${featureId} does not belong to milestone ${milestoneId}`);
922
- }
923
-
924
- // Build the source reference with ordering context
925
- const source: RoadmapFeatureSourceRef = {
926
- roadmapId: roadmap.id,
927
- milestoneId: milestone.id,
928
- featureId: feature.id,
929
- roadmapTitle: roadmap.title,
930
- milestoneTitle: milestone.title,
931
- milestoneOrderIndex: milestone.orderIndex,
932
- featureOrderIndex: feature.orderIndex,
933
- };
934
-
935
- return {
936
- source,
937
- title: feature.title,
938
- description: feature.description,
939
- };
940
- }
941
-
942
- /**
943
- * Get a mission planning handoff payload for a roadmap.
944
- *
945
- * Alias for getRoadmapMissionHandoff() for API consistency.
946
- * Converts the roadmap into a mission planning structure while preserving
947
- * source IDs and deterministic order.
948
- *
949
- * @param roadmapId - Roadmap ID
950
- * @returns The mission planning handoff payload
951
- * @throws Error if roadmap not found
952
- */
953
- getMissionPlanningHandoff(roadmapId: string): RoadmapMissionPlanningHandoff {
954
- return this.getRoadmapMissionHandoff(roadmapId);
955
- }
956
-
957
- /**
958
- * List all task planning handoff payloads for a roadmap.
959
- *
960
- * Returns a flat list of all feature handoffs in deterministic order
961
- * (milestone order index, then feature order index).
962
- *
963
- * @param roadmapId - Roadmap ID
964
- * @returns Array of task planning handoff payloads for all features
965
- * @throws Error if roadmap not found
966
- */
967
- listFeatureTaskPlanningHandoffs(roadmapId: string): RoadmapFeatureTaskPlanningHandoff[] {
968
- // Validate roadmap exists
969
- const roadmap = this.getRoadmap(roadmapId);
970
- if (!roadmap) {
971
- throw new Error(`Roadmap ${roadmapId} not found`);
972
- }
973
-
974
- const milestones = this.listMilestones(roadmapId);
975
- const handoffs: RoadmapFeatureTaskPlanningHandoff[] = [];
976
-
977
- for (const milestone of milestones) {
978
- const features = this.listFeatures(milestone.id);
979
-
980
- for (const feature of features) {
981
- const source: RoadmapFeatureSourceRef = {
982
- roadmapId: roadmap.id,
983
- milestoneId: milestone.id,
984
- featureId: feature.id,
985
- roadmapTitle: roadmap.title,
986
- milestoneTitle: milestone.title,
987
- milestoneOrderIndex: milestone.orderIndex,
988
- featureOrderIndex: feature.orderIndex,
989
- };
990
-
991
- handoffs.push({
992
- source,
993
- title: feature.title,
994
- description: feature.description,
995
- });
996
- }
997
- }
998
-
999
- return handoffs;
1000
- }
1001
- }