@runfusion/fusion 0.23.0 → 0.25.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 (172) hide show
  1. package/dist/bin.js +27921 -21003
  2. package/dist/client/assets/AgentDetailView-BwJaLqZh.css +1 -0
  3. package/dist/client/assets/AgentDetailView-ZbHEbYRT.js +18 -0
  4. package/dist/client/assets/AgentsView-B3jYk8Kt.js +29 -0
  5. package/dist/client/assets/{AgentsView-DSGQWObq.css → AgentsView-CV3vm7Qk.css} +1 -1
  6. package/dist/client/assets/ChatView-DhPkiEGs.js +1 -0
  7. package/dist/client/assets/ChatView-DwJAd5G1.css +1 -0
  8. package/dist/client/assets/{DevServerView-C9lzHrcT.js → DevServerView-DyGDEiBP.js} +1 -1
  9. package/dist/client/assets/{DirectoryPicker-aVdFaV37.js → DirectoryPicker-D5UIeIl6.js} +1 -1
  10. package/dist/client/assets/{DocumentsView-DIpg3NSP.js → DocumentsView-DNHu1T8K.js} +1 -1
  11. package/dist/client/assets/{DocumentsView-BrhyOdeE.css → DocumentsView-gv4zG3aT.css} +1 -1
  12. package/dist/client/assets/EvalsView-CUNJ1TLc.css +1 -0
  13. package/dist/client/assets/EvalsView-CpRobtDi.js +1 -0
  14. package/dist/client/assets/{agentSkills-DDHJnrkn.css → ExperimentalAgentOnboardingModal-B-APN_lM.css} +1 -1
  15. package/dist/client/assets/ExperimentalAgentOnboardingModal-DOY_oZi7.js +499 -0
  16. package/dist/client/assets/InsightsView-B0J4mhzV.css +1 -0
  17. package/dist/client/assets/InsightsView-vp0RE8Mg.js +11 -0
  18. package/dist/client/assets/MemoryView-PSc5lGJt.js +2 -0
  19. package/dist/client/assets/MemoryView-zaXewZzi.css +1 -0
  20. package/dist/client/assets/NodesView-DMj6HGeC.js +14 -0
  21. package/dist/client/assets/NodesView-DT4pXowv.css +1 -0
  22. package/dist/client/assets/{PiExtensionsManager-Buopv-jb.js → PiExtensionsManager-DL_QcN56.js} +2 -2
  23. package/dist/client/assets/PluginManager-BtYKm8IT.js +1 -0
  24. package/dist/client/assets/PluginManager-DtRQXia5.css +1 -0
  25. package/dist/client/assets/{ResearchView-_BHXUv2j.js → ResearchView-BhWqfdV0.js} +1 -1
  26. package/dist/client/assets/SettingsModal-BAgB4_AR.js +31 -0
  27. package/dist/client/assets/SettingsModal-CUCyaAyE.js +1 -0
  28. package/dist/client/assets/SettingsModal-DzsLquBu.css +1 -0
  29. package/dist/client/assets/SetupWizardModal-BKscasuh.js +1 -0
  30. package/dist/client/assets/{SkillsView-hDpTBdFT.js → SkillsView-BdELqTy7.js} +1 -1
  31. package/dist/client/assets/TodoView-Cx9cVhq7.css +1 -0
  32. package/dist/client/assets/TodoView-DFNGBDNV.js +6 -0
  33. package/dist/client/assets/{folder-open-usZkXdq2.js → folder-open-k1xmUMyr.js} +1 -1
  34. package/dist/client/assets/index-Qq2JOOWx.css +1 -0
  35. package/dist/client/assets/index-TFYXEVpn.js +692 -0
  36. package/dist/client/assets/projectDetection-G3XuxD2X.js +1 -0
  37. package/dist/client/assets/{star-BAT_ObKE.js → star-ne32r3Y4.js} +1 -1
  38. package/dist/client/assets/{upload-BC2YKNEV.js → upload-MS-2Gx53.js} +1 -1
  39. package/dist/client/assets/{users-Dkd4rtrN.js → users-C519GSjH.js} +1 -1
  40. package/dist/client/index.html +12 -20
  41. package/dist/client/theme-data.css +106 -0
  42. package/dist/client/version.json +1 -1
  43. package/dist/droid-cli/package.json +1 -1
  44. package/dist/extension.js +15395 -9935
  45. package/dist/pi-claude-cli/package.json +1 -1
  46. package/dist/plugins/fusion-plugin-cursor-runtime/bundled.js +216 -0
  47. package/dist/plugins/fusion-plugin-cursor-runtime/manifest.json +6 -0
  48. package/dist/plugins/fusion-plugin-cursor-runtime/package.json +11 -0
  49. package/dist/plugins/fusion-plugin-dependency-graph/bundled.js +30 -0
  50. package/dist/plugins/fusion-plugin-dependency-graph/manifest.json +1 -1
  51. package/dist/plugins/fusion-plugin-dependency-graph/package.json +3 -26
  52. package/dist/plugins/fusion-plugin-droid-runtime/bundled.js +136684 -0
  53. package/dist/plugins/fusion-plugin-droid-runtime/manifest.json +13 -0
  54. package/dist/plugins/fusion-plugin-droid-runtime/mcp-schema-server.cjs +49 -0
  55. package/dist/plugins/fusion-plugin-droid-runtime/package.json +11 -0
  56. package/dist/plugins/fusion-plugin-hermes-runtime/bundled.js +68 -71
  57. package/dist/plugins/fusion-plugin-hermes-runtime/package.json +1 -1
  58. package/dist/plugins/fusion-plugin-openclaw-runtime/bundled.js +137 -53
  59. package/dist/plugins/fusion-plugin-openclaw-runtime/mcp-schema-server.cjs +59 -0
  60. package/dist/plugins/fusion-plugin-openclaw-runtime/package.json +1 -1
  61. package/dist/plugins/fusion-plugin-paperclip-runtime/bundled.js +155 -109
  62. package/dist/plugins/fusion-plugin-paperclip-runtime/package.json +1 -1
  63. package/dist/plugins/fusion-plugin-reports/manifest.json +33 -0
  64. package/dist/plugins/fusion-plugin-reports/package.json +26 -0
  65. package/dist/plugins/fusion-plugin-reports/src/__tests__/manifest.test.ts +51 -0
  66. package/dist/plugins/fusion-plugin-reports/src/__tests__/review-panel.test.ts +166 -0
  67. package/dist/plugins/fusion-plugin-reports/src/__tests__/settings.test.ts +157 -0
  68. package/dist/plugins/fusion-plugin-reports/src/index.ts +87 -0
  69. package/dist/plugins/fusion-plugin-reports/src/report-schema.ts +38 -0
  70. package/dist/plugins/fusion-plugin-reports/src/review-panel.ts +294 -0
  71. package/dist/plugins/fusion-plugin-reports/src/review-types.ts +75 -0
  72. package/dist/plugins/fusion-plugin-reports/src/settings.ts +105 -0
  73. package/dist/plugins/fusion-plugin-reports/src/store/__tests__/report-schema.test.ts +66 -0
  74. package/dist/plugins/fusion-plugin-reports/src/store/__tests__/report-store.test.ts +177 -0
  75. package/dist/plugins/fusion-plugin-reports/src/store/report-store.ts +341 -0
  76. package/dist/plugins/fusion-plugin-reports/src/store/report-types.ts +77 -0
  77. package/dist/plugins/fusion-plugin-roadmap/manifest.json +16 -0
  78. package/dist/plugins/fusion-plugin-roadmap/package.json +48 -0
  79. package/dist/plugins/fusion-plugin-roadmap/src/__tests__/api-client.test.ts +101 -0
  80. package/dist/plugins/fusion-plugin-roadmap/src/__tests__/index.test.ts +92 -0
  81. package/dist/plugins/fusion-plugin-roadmap/src/__tests__/roadmap-routes.test.ts +48 -0
  82. package/dist/plugins/fusion-plugin-roadmap/src/__tests__/roadmap-suggestions.test.ts +31 -0
  83. package/dist/plugins/fusion-plugin-roadmap/src/dashboard/RoadmapsView.css +1299 -0
  84. package/dist/plugins/fusion-plugin-roadmap/src/dashboard/RoadmapsView.tsx +2559 -0
  85. package/dist/plugins/fusion-plugin-roadmap/src/dashboard/__tests__/RoadmapsView.test.tsx +1144 -0
  86. package/dist/plugins/fusion-plugin-roadmap/src/dashboard/__tests__/useRoadmaps.test.ts +1756 -0
  87. package/dist/plugins/fusion-plugin-roadmap/src/dashboard/api.ts +70 -0
  88. package/dist/plugins/fusion-plugin-roadmap/src/dashboard/test-setup.ts +7 -0
  89. package/dist/plugins/fusion-plugin-roadmap/src/dashboard/types.ts +1 -0
  90. package/dist/plugins/fusion-plugin-roadmap/src/dashboard/useConfirm.ts +8 -0
  91. package/dist/plugins/fusion-plugin-roadmap/src/dashboard/useRoadmaps.ts +1188 -0
  92. package/dist/plugins/fusion-plugin-roadmap/src/dashboard/useViewportMode.ts +20 -0
  93. package/dist/plugins/fusion-plugin-roadmap/src/dashboard-view.tsx +6 -0
  94. package/dist/plugins/fusion-plugin-roadmap/src/index.ts +74 -0
  95. package/dist/plugins/fusion-plugin-roadmap/src/roadmap-routes.ts +1 -0
  96. package/dist/plugins/fusion-plugin-roadmap/src/roadmap-schema.ts +41 -0
  97. package/dist/plugins/fusion-plugin-roadmap/src/roadmap-suggestions.d.ts +15 -0
  98. package/dist/plugins/fusion-plugin-roadmap/src/roadmap-suggestions.ts +15 -0
  99. package/dist/plugins/fusion-plugin-roadmap/src/roadmap-types.d.ts +283 -0
  100. package/dist/plugins/fusion-plugin-roadmap/src/roadmap-types.d.ts.map +1 -0
  101. package/dist/plugins/fusion-plugin-roadmap/src/roadmap-types.js +21 -0
  102. package/dist/plugins/fusion-plugin-roadmap/src/roadmap-types.js.map +1 -0
  103. package/dist/plugins/fusion-plugin-roadmap/src/roadmap-types.ts +310 -0
  104. package/dist/plugins/fusion-plugin-roadmap/src/routes/roadmap-routes.d.ts +5 -0
  105. package/dist/plugins/fusion-plugin-roadmap/src/routes/roadmap-routes.d.ts.map +1 -0
  106. package/dist/plugins/fusion-plugin-roadmap/src/routes/roadmap-routes.js +361 -0
  107. package/dist/plugins/fusion-plugin-roadmap/src/routes/roadmap-routes.js.map +1 -0
  108. package/dist/plugins/fusion-plugin-roadmap/src/routes/roadmap-routes.ts +408 -0
  109. package/dist/plugins/fusion-plugin-roadmap/src/routes/roadmap-suggestions.d.ts +68 -0
  110. package/dist/plugins/fusion-plugin-roadmap/src/routes/roadmap-suggestions.d.ts.map +1 -0
  111. package/dist/plugins/fusion-plugin-roadmap/src/routes/roadmap-suggestions.js +300 -0
  112. package/dist/plugins/fusion-plugin-roadmap/src/routes/roadmap-suggestions.js.map +1 -0
  113. package/dist/plugins/fusion-plugin-roadmap/src/routes/roadmap-suggestions.ts +381 -0
  114. package/dist/plugins/fusion-plugin-roadmap/src/server/index.d.ts +3 -0
  115. package/dist/plugins/fusion-plugin-roadmap/src/server/index.ts +1 -0
  116. package/dist/plugins/fusion-plugin-roadmap/src/store/__tests__/roadmap-handoff.test.ts +445 -0
  117. package/dist/plugins/fusion-plugin-roadmap/src/store/__tests__/roadmap-ordering.test.ts +334 -0
  118. package/dist/plugins/fusion-plugin-roadmap/src/store/__tests__/roadmap-store.test.ts +1318 -0
  119. package/dist/plugins/fusion-plugin-roadmap/src/store/roadmap-handoff.ts +163 -0
  120. package/dist/plugins/fusion-plugin-roadmap/src/store/roadmap-ordering.d.ts +37 -0
  121. package/dist/plugins/fusion-plugin-roadmap/src/store/roadmap-ordering.d.ts.map +1 -0
  122. package/dist/plugins/fusion-plugin-roadmap/src/store/roadmap-ordering.js +188 -0
  123. package/dist/plugins/fusion-plugin-roadmap/src/store/roadmap-ordering.js.map +1 -0
  124. package/dist/plugins/fusion-plugin-roadmap/src/store/roadmap-ordering.ts +311 -0
  125. package/dist/plugins/fusion-plugin-roadmap/src/store/roadmap-store.d.ts +299 -0
  126. package/dist/plugins/fusion-plugin-roadmap/src/store/roadmap-store.d.ts.map +1 -0
  127. package/dist/plugins/fusion-plugin-roadmap/src/store/roadmap-store.js +765 -0
  128. package/dist/plugins/fusion-plugin-roadmap/src/store/roadmap-store.js.map +1 -0
  129. package/dist/plugins/fusion-plugin-roadmap/src/store/roadmap-store.ts +1001 -0
  130. package/dist/plugins/fusion-plugin-whatsapp-chat/manifest.json +8 -0
  131. package/dist/plugins/fusion-plugin-whatsapp-chat/package.json +34 -0
  132. package/dist/plugins/fusion-plugin-whatsapp-chat/src/__tests__/auth-state.test.ts +99 -0
  133. package/dist/plugins/fusion-plugin-whatsapp-chat/src/__tests__/connection.test.ts +145 -0
  134. package/dist/plugins/fusion-plugin-whatsapp-chat/src/__tests__/index.test.ts +216 -0
  135. package/dist/plugins/fusion-plugin-whatsapp-chat/src/__tests__/reply.test.ts +52 -0
  136. package/dist/plugins/fusion-plugin-whatsapp-chat/src/auth-state.ts +89 -0
  137. package/dist/plugins/fusion-plugin-whatsapp-chat/src/connection.ts +253 -0
  138. package/dist/plugins/fusion-plugin-whatsapp-chat/src/index.ts +262 -0
  139. package/dist/plugins/fusion-plugin-whatsapp-chat/src/qrcode.d.ts +1 -0
  140. package/dist/plugins/fusion-plugin-whatsapp-chat/src/reply.ts +37 -0
  141. package/package.json +2 -2
  142. package/skill/fusion/SKILL.md +2 -2
  143. package/skill/fusion/references/engine-tools.md +3 -0
  144. package/skill/fusion/references/extension-tools.md +39 -0
  145. package/skill/fusion/references/fusion-capabilities.md +3 -0
  146. package/dist/client/assets/AgentDetailView-C1XceMgi.js +0 -18
  147. package/dist/client/assets/AgentDetailView-CeO_1MK7.css +0 -1
  148. package/dist/client/assets/AgentsView-Deh125ss.js +0 -527
  149. package/dist/client/assets/ChatView-7D_RQDqT.js +0 -1
  150. package/dist/client/assets/InsightsView-AWo5o_81.css +0 -1
  151. package/dist/client/assets/InsightsView-jKjEFAx_.js +0 -11
  152. package/dist/client/assets/MemoryView-DiajLXby.css +0 -1
  153. package/dist/client/assets/MemoryView-nXlTqebk.js +0 -2
  154. package/dist/client/assets/NodesView-Di2SvOhg.js +0 -14
  155. package/dist/client/assets/NodesView-fXqDk9ur.css +0 -1
  156. package/dist/client/assets/PluginManager-B9-NbQ8f.js +0 -1
  157. package/dist/client/assets/PluginManager-C1DbPaar.css +0 -1
  158. package/dist/client/assets/RoadmapsView-DHWjUoc8.js +0 -6
  159. package/dist/client/assets/RoadmapsView-DdGlfuu-.css +0 -1
  160. package/dist/client/assets/SettingsModal-C89Ikhfm.js +0 -1
  161. package/dist/client/assets/SettingsModal-DHitIpsa.css +0 -1
  162. package/dist/client/assets/SettingsModal-DR_yirvK.js +0 -31
  163. package/dist/client/assets/SetupWizardModal-BtDMY9pa.js +0 -1
  164. package/dist/client/assets/agentSkills-B-w5wFHh.js +0 -1
  165. package/dist/client/assets/index-Bc6ZdGMz.css +0 -1
  166. package/dist/client/assets/index-D__RMku8.js +0 -694
  167. package/dist/plugins/fusion-plugin-dependency-graph/src/DependencyGraphView.css +0 -141
  168. package/dist/plugins/fusion-plugin-dependency-graph/src/DependencyGraphView.tsx +0 -428
  169. package/dist/plugins/fusion-plugin-dependency-graph/src/__tests__/DependencyGraphView.test.tsx +0 -261
  170. package/dist/plugins/fusion-plugin-dependency-graph/src/__tests__/storage.test.ts +0 -41
  171. package/dist/plugins/fusion-plugin-dependency-graph/src/index.ts +0 -25
  172. package/dist/plugins/fusion-plugin-dependency-graph/src/storage.ts +0 -22
@@ -0,0 +1,177 @@
1
+ import { Database } from "@fusion/core";
2
+ import { mkdtempSync } from "node:fs";
3
+ import { rm } from "node:fs/promises";
4
+ import { tmpdir } from "node:os";
5
+ import { join } from "node:path";
6
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
7
+ import { ensureReportSchema } from "../../report-schema.js";
8
+ import type { CombinedReview } from "../../review-types.js";
9
+ import { ReportStore, ReportStoreError } from "../report-store.js";
10
+
11
+ function makeTmpDir(): string {
12
+ return mkdtempSync(join(tmpdir(), "report-store-test-"));
13
+ }
14
+
15
+ function makeReview(): CombinedReview {
16
+ return {
17
+ overallVerdict: "revise",
18
+ consensusSummary: "Needs updates",
19
+ mergedHighlights: ["Good structure"],
20
+ mergedLowlights: ["Missing metrics"],
21
+ mergedSuggestions: ["Add numbers"],
22
+ individual: [],
23
+ failures: [],
24
+ };
25
+ }
26
+
27
+ describe("ReportStore", () => {
28
+ let tmp: string;
29
+ let db: Database;
30
+ let store: ReportStore;
31
+
32
+ beforeEach(() => {
33
+ tmp = makeTmpDir();
34
+ db = new Database(join(tmp, ".fusion"), { inMemory: true });
35
+ db.init();
36
+ ensureReportSchema(db);
37
+ store = new ReportStore(db);
38
+ });
39
+
40
+ afterEach(async () => {
41
+ db.close();
42
+ await rm(tmp, { recursive: true, force: true });
43
+ });
44
+
45
+ it("createReport persists generating report", () => {
46
+ const listener = vi.fn();
47
+ store.on("report:created", listener);
48
+
49
+ const report = store.createReport({
50
+ cadence: "daily",
51
+ periodStart: "2026-05-01T00:00:00.000Z",
52
+ periodEnd: "2026-05-01T23:59:59.999Z",
53
+ title: "Daily",
54
+ metadata: { sourceCount: 12 },
55
+ draftMarkdown: "# Draft",
56
+ });
57
+
58
+ expect(report.id).toMatch(/^rep_/);
59
+ expect(report.status).toBe("generating");
60
+ expect(report.generationStartedAt).toBeTruthy();
61
+ expect(listener).toHaveBeenCalledTimes(1);
62
+ expect(store.getReport(report.id)?.metadata).toEqual({ sourceCount: 12 });
63
+ });
64
+
65
+ it("getReport hydrates metadata and combinedReview", () => {
66
+ const report = store.createReport({ cadence: "weekly", periodStart: "2026-05-01", periodEnd: "2026-05-07", title: "Weekly" });
67
+ store.setStatus(report.id, "review_pending");
68
+ store.setStatus(report.id, "review_in_progress");
69
+ store.attachReview(report.id, makeReview());
70
+
71
+ const hydrated = store.getReport(report.id);
72
+ expect(hydrated?.combinedReview?.overallVerdict).toBe("revise");
73
+ expect(hydrated?.metadata).toEqual({});
74
+ });
75
+
76
+ it("listReports filters and paginates", () => {
77
+ const a = store.createReport({ cadence: "daily", periodStart: "2026-05-01", periodEnd: "2026-05-01", title: "A" });
78
+ const b = store.createReport({ cadence: "weekly", periodStart: "2026-05-02", periodEnd: "2026-05-08", title: "B" });
79
+ const c = store.createReport({ cadence: "daily", periodStart: "2026-05-03", periodEnd: "2026-05-03", title: "C" });
80
+ store.setStatus(c.id, "failed", { failureReason: "x" });
81
+
82
+ expect(store.listReports({ cadence: "daily" }).length).toBe(2);
83
+ expect(store.listReports({ statusIn: ["failed"] }).map((r) => r.id)).toEqual([c.id]);
84
+ expect(store.listReports({ periodStartFrom: "2026-05-02", periodStartTo: "2026-05-03", orderBy: "periodStart", orderDir: "asc" }).map((r) => r.id)).toEqual([b.id, c.id]);
85
+
86
+ const paged = store.listReports({ orderBy: "periodStart", orderDir: "asc", limit: 1, offset: 1 });
87
+ expect(paged).toHaveLength(1);
88
+ });
89
+
90
+ it("setStatus enforces lifecycle and timestamps", () => {
91
+ const report = store.createReport({ cadence: "daily", periodStart: "2026-05-01", periodEnd: "2026-05-01", title: "A" });
92
+
93
+ const pending = store.setStatus(report.id, "review_pending");
94
+ expect(pending.generationCompletedAt).toBeTruthy();
95
+
96
+ const inProgress = store.setStatus(report.id, "review_in_progress");
97
+ expect(inProgress.reviewStartedAt).toBeTruthy();
98
+
99
+ const complete = store.setStatus(report.id, "review_complete");
100
+ expect(complete.reviewCompletedAt).toBeTruthy();
101
+
102
+ const approved = store.setStatus(report.id, "approved", { approvedBy: "agent-1" });
103
+ expect(approved.approvedAt).toBeTruthy();
104
+ expect(approved.approvedBy).toBe("agent-1");
105
+
106
+ const published = store.setStatus(report.id, "published");
107
+ expect(published.publishedAt).toBeTruthy();
108
+
109
+ expect(() => store.setStatus(report.id, "generating")).toThrow(ReportStoreError);
110
+ });
111
+
112
+ it("setStatus failed works from non-terminal state and saves failureReason", () => {
113
+ const report = store.createReport({ cadence: "daily", periodStart: "2026-05-01", periodEnd: "2026-05-01", title: "A" });
114
+ const failed = store.setStatus(report.id, "failed", { failureReason: "timeout" });
115
+ expect(failed.failureReason).toBe("timeout");
116
+ });
117
+
118
+ it("attachRenderedHtml and deleteReport persist and emit events", () => {
119
+ const report = store.createReport({ cadence: "manual", periodStart: "2026-05-01", periodEnd: "2026-05-01", title: "A" });
120
+ const deleted = vi.fn();
121
+ store.on("report:deleted", deleted);
122
+
123
+ const updated = store.attachRenderedHtml(report.id, ".fusion/plugins/reports/report.html");
124
+ expect(updated.renderedHtmlPath).toContain("report.html");
125
+
126
+ store.deleteReport(report.id);
127
+ expect(store.getReport(report.id)).toBeNull();
128
+ expect(deleted).toHaveBeenCalledWith(report.id);
129
+ });
130
+
131
+ it("emits events once for update/status/review", () => {
132
+ const report = store.createReport({ cadence: "daily", periodStart: "2026-05-01", periodEnd: "2026-05-01", title: "A" });
133
+ const updated = vi.fn();
134
+ const status = vi.fn();
135
+ const review = vi.fn();
136
+ store.on("report:updated", updated);
137
+ store.on("report:status-changed", status);
138
+ store.on("report:review-attached", review);
139
+
140
+ store.updateReport(report.id, { title: "B" });
141
+ store.setStatus(report.id, "review_pending");
142
+ store.setStatus(report.id, "review_in_progress");
143
+ store.attachReview(report.id, makeReview());
144
+
145
+ expect(updated).toHaveBeenCalledTimes(1);
146
+ expect(status).toHaveBeenCalledTimes(3);
147
+ expect(review).toHaveBeenCalledTimes(1);
148
+ });
149
+
150
+ it("rolls back failed transaction and commits successful transaction", () => {
151
+ const report = store.createReport({ cadence: "daily", periodStart: "2026-05-01", periodEnd: "2026-05-01", title: "A" });
152
+
153
+ const originalPrepare = db.prepare.bind(db);
154
+ const spy = vi.spyOn(db, "prepare").mockImplementation((sql: string) => {
155
+ const stmt = originalPrepare(sql);
156
+ if (sql.includes("UPDATE reports") && !sql.includes("WHERE id = @id")) {
157
+ return stmt;
158
+ }
159
+ if (sql.includes("UPDATE reports")) {
160
+ return {
161
+ ...stmt,
162
+ run: (...args: unknown[]) => {
163
+ throw new Error("forced failure");
164
+ },
165
+ } as typeof stmt;
166
+ }
167
+ return stmt;
168
+ });
169
+
170
+ expect(() => store.updateReport(report.id, { title: "Broken" })).toThrow("forced failure");
171
+ expect(store.getReport(report.id)?.title).toBe("A");
172
+
173
+ spy.mockRestore();
174
+ const ok = store.updateReport(report.id, { title: "Good" });
175
+ expect(ok.title).toBe("Good");
176
+ });
177
+ });
@@ -0,0 +1,341 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { EventEmitter } from "node:events";
3
+ import type { Database } from "@fusion/core";
4
+ import type { CombinedReview } from "../review-types.js";
5
+ import {
6
+ type Report,
7
+ type ReportCreateInput,
8
+ type ReportListFilter,
9
+ type ReportStatus,
10
+ type ReportUpdateInput,
11
+ isValidReportStatusTransition,
12
+ } from "./report-types.js";
13
+
14
+ interface ReportRow {
15
+ id: string;
16
+ cadence: Report["cadence"];
17
+ periodStart: string;
18
+ periodEnd: string;
19
+ title: string;
20
+ status: ReportStatus;
21
+ generationStartedAt: string;
22
+ generationCompletedAt: string | null;
23
+ reviewStartedAt: string | null;
24
+ reviewCompletedAt: string | null;
25
+ approvedAt: string | null;
26
+ approvedBy: string | null;
27
+ publishedAt: string | null;
28
+ archivedAt: string | null;
29
+ failureReason: string | null;
30
+ draftMarkdown: string | null;
31
+ renderedHtmlPath: string | null;
32
+ metadataJson: string;
33
+ combinedReviewJson: string | null;
34
+ createdAt: string;
35
+ updatedAt: string;
36
+ }
37
+
38
+ export interface ReportStoreEvents {
39
+ "report:created": [Report];
40
+ "report:updated": [Report];
41
+ "report:status-changed": [Report];
42
+ "report:review-attached": [Report];
43
+ "report:deleted": [string];
44
+ }
45
+
46
+ export class ReportStoreError extends Error {
47
+ constructor(message: string) {
48
+ super(message);
49
+ this.name = "ReportStoreError";
50
+ }
51
+ }
52
+
53
+ export class ReportStore extends EventEmitter<ReportStoreEvents> {
54
+ constructor(private readonly db: Database) {
55
+ super();
56
+ this.setMaxListeners(50);
57
+ }
58
+
59
+ createReport(input: ReportCreateInput): Report {
60
+ const now = new Date().toISOString();
61
+ const report: Report = {
62
+ id: `rep_${randomUUID().replaceAll("-", "")}`,
63
+ cadence: input.cadence,
64
+ periodStart: input.periodStart,
65
+ periodEnd: input.periodEnd,
66
+ title: input.title,
67
+ status: "generating",
68
+ generationStartedAt: now,
69
+ generationCompletedAt: null,
70
+ reviewStartedAt: null,
71
+ reviewCompletedAt: null,
72
+ approvedAt: null,
73
+ approvedBy: null,
74
+ publishedAt: null,
75
+ archivedAt: null,
76
+ failureReason: null,
77
+ draftMarkdown: input.draftMarkdown ?? null,
78
+ renderedHtmlPath: null,
79
+ metadata: input.metadata ?? {},
80
+ combinedReview: null,
81
+ createdAt: now,
82
+ updatedAt: now,
83
+ };
84
+
85
+ this.db.transaction(() => {
86
+ this.db.prepare(`
87
+ INSERT INTO reports (
88
+ id, cadence, periodStart, periodEnd, title, status,
89
+ generationStartedAt, generationCompletedAt, reviewStartedAt, reviewCompletedAt,
90
+ approvedAt, approvedBy, publishedAt, archivedAt, failureReason,
91
+ draftMarkdown, renderedHtmlPath, metadataJson, combinedReviewJson, createdAt, updatedAt
92
+ ) VALUES (
93
+ @id, @cadence, @periodStart, @periodEnd, @title, @status,
94
+ @generationStartedAt, @generationCompletedAt, @reviewStartedAt, @reviewCompletedAt,
95
+ @approvedAt, @approvedBy, @publishedAt, @archivedAt, @failureReason,
96
+ @draftMarkdown, @renderedHtmlPath, @metadataJson, @combinedReviewJson, @createdAt, @updatedAt
97
+ )
98
+ `).run(this.toDbParams(report, true));
99
+ });
100
+
101
+ this.db.bumpLastModified();
102
+ this.emit("report:created", report);
103
+ return report;
104
+ }
105
+
106
+ getReport(id: string): Report | null {
107
+ const row = this.db.prepare("SELECT * FROM reports WHERE id = ?").get(id) as ReportRow | undefined;
108
+ return row ? this.rowToReport(row) : null;
109
+ }
110
+
111
+ listReports(filter: ReportListFilter = {}): Report[] {
112
+ const params: unknown[] = [];
113
+ const where: string[] = [];
114
+
115
+ if (filter.cadence) {
116
+ where.push("cadence = ?");
117
+ params.push(filter.cadence);
118
+ }
119
+ if (filter.statusIn && filter.statusIn.length > 0) {
120
+ where.push(`status IN (${filter.statusIn.map(() => "?").join(",")})`);
121
+ params.push(...filter.statusIn);
122
+ } else if (filter.status) {
123
+ where.push("status = ?");
124
+ params.push(filter.status);
125
+ }
126
+ if (filter.periodStartFrom) {
127
+ where.push("periodStart >= ?");
128
+ params.push(filter.periodStartFrom);
129
+ }
130
+ if (filter.periodStartTo) {
131
+ where.push("periodStart <= ?");
132
+ params.push(filter.periodStartTo);
133
+ }
134
+
135
+ const orderBy = filter.orderBy === "periodStart" ? "periodStart" : "createdAt";
136
+ const orderDir = filter.orderDir === "asc" ? "ASC" : "DESC";
137
+ const limit = Math.min(Math.max(filter.limit ?? 50, 1), 500);
138
+ const offset = Math.max(filter.offset ?? 0, 0);
139
+
140
+ const sql = `
141
+ SELECT * FROM reports
142
+ ${where.length > 0 ? `WHERE ${where.join(" AND ")}` : ""}
143
+ ORDER BY ${orderBy} ${orderDir}, id ${orderDir}
144
+ LIMIT ? OFFSET ?
145
+ `;
146
+ params.push(limit, offset);
147
+
148
+ const rows = this.db.prepare(sql).all(...params) as ReportRow[];
149
+ return rows.map((row) => this.rowToReport(row));
150
+ }
151
+
152
+ updateReport(id: string, patch: ReportUpdateInput): Report {
153
+ const current = this.requireReport(id);
154
+ const next: Report = {
155
+ ...current,
156
+ title: patch.title ?? current.title,
157
+ draftMarkdown: patch.draftMarkdown ?? current.draftMarkdown,
158
+ renderedHtmlPath: patch.renderedHtmlPath ?? current.renderedHtmlPath,
159
+ metadata: patch.metadata ?? current.metadata,
160
+ failureReason: patch.failureReason ?? current.failureReason,
161
+ updatedAt: new Date().toISOString(),
162
+ };
163
+
164
+ this.db.transaction(() => this.persistExisting(next));
165
+ this.db.bumpLastModified();
166
+ this.emit("report:updated", next);
167
+ return next;
168
+ }
169
+
170
+ setStatus(id: string, next: ReportStatus, opts: { failureReason?: string; approvedBy?: string } = {}): Report {
171
+ const current = this.requireReport(id);
172
+ if (current.status === next) return current;
173
+ if (!isValidReportStatusTransition(current.status, next)) {
174
+ throw new ReportStoreError(`Invalid status transition: ${current.status} -> ${next}`);
175
+ }
176
+
177
+ const now = new Date().toISOString();
178
+ const updated: Report = {
179
+ ...current,
180
+ status: next,
181
+ updatedAt: now,
182
+ failureReason: next === "failed" ? (opts.failureReason ?? current.failureReason) : current.failureReason,
183
+ };
184
+
185
+ if (next === "review_pending") updated.generationCompletedAt = now;
186
+ if (next === "review_in_progress") updated.reviewStartedAt = now;
187
+ if (next === "review_complete") updated.reviewCompletedAt = now;
188
+ if (next === "approved") {
189
+ updated.approvedAt = now;
190
+ updated.approvedBy = opts.approvedBy ?? current.approvedBy;
191
+ }
192
+ if (next === "published") updated.publishedAt = now;
193
+ if (next === "archived") updated.archivedAt = now;
194
+
195
+ this.db.transaction(() => this.persistExisting(updated));
196
+ this.db.bumpLastModified();
197
+ this.emit("report:status-changed", updated);
198
+ return updated;
199
+ }
200
+
201
+ attachReview(id: string, combined: CombinedReview): Report {
202
+ const current = this.requireReport(id);
203
+ if (current.status !== "review_in_progress") {
204
+ throw new ReportStoreError(`attachReview requires review_in_progress status; got ${current.status}`);
205
+ }
206
+
207
+ const now = new Date().toISOString();
208
+ const updated: Report = {
209
+ ...current,
210
+ combinedReview: combined,
211
+ status: "review_complete",
212
+ reviewCompletedAt: now,
213
+ updatedAt: now,
214
+ };
215
+
216
+ this.db.transaction(() => this.persistExisting(updated));
217
+ this.db.bumpLastModified();
218
+ this.emit("report:review-attached", updated);
219
+ this.emit("report:status-changed", updated);
220
+ return updated;
221
+ }
222
+
223
+ attachRenderedHtml(id: string, htmlPath: string): Report {
224
+ return this.updateReport(id, { renderedHtmlPath: htmlPath });
225
+ }
226
+
227
+ deleteReport(id: string): void {
228
+ this.requireReport(id);
229
+ this.db.transaction(() => {
230
+ this.db.prepare("DELETE FROM reports WHERE id = ?").run(id);
231
+ });
232
+ this.db.bumpLastModified();
233
+ this.emit("report:deleted", id);
234
+ }
235
+
236
+ private requireReport(id: string): Report {
237
+ const report = this.getReport(id);
238
+ if (!report) throw new ReportStoreError(`Report ${id} not found`);
239
+ return report;
240
+ }
241
+
242
+ private rowToReport(row: ReportRow): Report {
243
+ return {
244
+ id: row.id,
245
+ cadence: row.cadence,
246
+ periodStart: row.periodStart,
247
+ periodEnd: row.periodEnd,
248
+ title: row.title,
249
+ status: row.status,
250
+ generationStartedAt: row.generationStartedAt,
251
+ generationCompletedAt: row.generationCompletedAt,
252
+ reviewStartedAt: row.reviewStartedAt,
253
+ reviewCompletedAt: row.reviewCompletedAt,
254
+ approvedAt: row.approvedAt,
255
+ approvedBy: row.approvedBy,
256
+ publishedAt: row.publishedAt,
257
+ archivedAt: row.archivedAt,
258
+ failureReason: row.failureReason,
259
+ draftMarkdown: row.draftMarkdown,
260
+ renderedHtmlPath: row.renderedHtmlPath,
261
+ metadata: this.parseMetadata(row.metadataJson),
262
+ combinedReview: this.parseCombinedReview(row.combinedReviewJson),
263
+ createdAt: row.createdAt,
264
+ updatedAt: row.updatedAt,
265
+ };
266
+ }
267
+
268
+ private persistExisting(report: Report): void {
269
+ const result = this.db.prepare(`
270
+ UPDATE reports
271
+ SET cadence = @cadence,
272
+ periodStart = @periodStart,
273
+ periodEnd = @periodEnd,
274
+ title = @title,
275
+ status = @status,
276
+ generationStartedAt = @generationStartedAt,
277
+ generationCompletedAt = @generationCompletedAt,
278
+ reviewStartedAt = @reviewStartedAt,
279
+ reviewCompletedAt = @reviewCompletedAt,
280
+ approvedAt = @approvedAt,
281
+ approvedBy = @approvedBy,
282
+ publishedAt = @publishedAt,
283
+ archivedAt = @archivedAt,
284
+ failureReason = @failureReason,
285
+ draftMarkdown = @draftMarkdown,
286
+ renderedHtmlPath = @renderedHtmlPath,
287
+ metadataJson = @metadataJson,
288
+ combinedReviewJson = @combinedReviewJson,
289
+ updatedAt = @updatedAt
290
+ WHERE id = @id
291
+ `).run(this.toDbParams(report, false));
292
+
293
+ if (result.changes === 0) {
294
+ throw new ReportStoreError(`Report ${report.id} not found`);
295
+ }
296
+ }
297
+
298
+ private toDbParams(report: Report, includeCreatedAt: boolean): Record<string, unknown> {
299
+ return {
300
+ id: report.id,
301
+ cadence: report.cadence,
302
+ periodStart: report.periodStart,
303
+ periodEnd: report.periodEnd,
304
+ title: report.title,
305
+ status: report.status,
306
+ generationStartedAt: report.generationStartedAt,
307
+ generationCompletedAt: report.generationCompletedAt,
308
+ reviewStartedAt: report.reviewStartedAt,
309
+ reviewCompletedAt: report.reviewCompletedAt,
310
+ approvedAt: report.approvedAt,
311
+ approvedBy: report.approvedBy,
312
+ publishedAt: report.publishedAt,
313
+ archivedAt: report.archivedAt,
314
+ failureReason: report.failureReason,
315
+ draftMarkdown: report.draftMarkdown,
316
+ renderedHtmlPath: report.renderedHtmlPath,
317
+ metadataJson: JSON.stringify(report.metadata ?? {}),
318
+ combinedReviewJson: report.combinedReview ? JSON.stringify(report.combinedReview) : null,
319
+ ...(includeCreatedAt ? { createdAt: report.createdAt } : {}),
320
+ updatedAt: report.updatedAt,
321
+ };
322
+ }
323
+
324
+ private parseMetadata(json: string): Record<string, unknown> {
325
+ try {
326
+ const parsed = JSON.parse(json);
327
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed as Record<string, unknown> : {};
328
+ } catch {
329
+ return {};
330
+ }
331
+ }
332
+
333
+ private parseCombinedReview(json: string | null): CombinedReview | null {
334
+ if (!json) return null;
335
+ try {
336
+ return JSON.parse(json) as CombinedReview;
337
+ } catch {
338
+ return null;
339
+ }
340
+ }
341
+ }
@@ -0,0 +1,77 @@
1
+ import type { CombinedReview } from "../review-types.js";
2
+
3
+ export type ReportCadence = "daily" | "weekly" | "monthly" | "quarterly" | "manual";
4
+
5
+ export type ReportStatus =
6
+ | "generating"
7
+ | "review_pending"
8
+ | "review_in_progress"
9
+ | "review_complete"
10
+ | "approved"
11
+ | "published"
12
+ | "archived"
13
+ | "failed";
14
+
15
+ export interface Report {
16
+ id: string;
17
+ cadence: ReportCadence;
18
+ periodStart: string;
19
+ periodEnd: string;
20
+ title: string;
21
+ status: ReportStatus;
22
+ generationStartedAt: string;
23
+ generationCompletedAt: string | null;
24
+ reviewStartedAt: string | null;
25
+ reviewCompletedAt: string | null;
26
+ approvedAt: string | null;
27
+ approvedBy: string | null;
28
+ publishedAt: string | null;
29
+ archivedAt: string | null;
30
+ failureReason: string | null;
31
+ draftMarkdown: string | null;
32
+ renderedHtmlPath: string | null;
33
+ metadata: Record<string, unknown>;
34
+ combinedReview: CombinedReview | null;
35
+ createdAt: string;
36
+ updatedAt: string;
37
+ }
38
+
39
+ export interface ReportCreateInput {
40
+ cadence: ReportCadence;
41
+ periodStart: string;
42
+ periodEnd: string;
43
+ title: string;
44
+ metadata?: Record<string, unknown>;
45
+ draftMarkdown?: string;
46
+ }
47
+
48
+ export type ReportUpdateInput = Partial<Pick<Report, "title" | "draftMarkdown" | "renderedHtmlPath" | "metadata" | "failureReason">>;
49
+
50
+ export interface ReportListFilter {
51
+ cadence?: ReportCadence;
52
+ status?: ReportStatus;
53
+ statusIn?: ReportStatus[];
54
+ periodStartFrom?: string;
55
+ periodStartTo?: string;
56
+ limit?: number;
57
+ offset?: number;
58
+ orderBy?: "createdAt" | "periodStart";
59
+ orderDir?: "asc" | "desc";
60
+ }
61
+
62
+ const TERMINAL_STATUSES = new Set<ReportStatus>(["published", "archived", "failed"]);
63
+ const LINEAR_TRANSITIONS: Record<Exclude<ReportStatus, "published" | "archived" | "failed">, ReportStatus> = {
64
+ generating: "review_pending",
65
+ review_pending: "review_in_progress",
66
+ review_in_progress: "review_complete",
67
+ review_complete: "approved",
68
+ approved: "published",
69
+ };
70
+
71
+ export function isValidReportStatusTransition(from: ReportStatus, to: ReportStatus): boolean {
72
+ if (from === to) return true;
73
+ if (TERMINAL_STATUSES.has(from)) return false;
74
+ if (to === "failed" || to === "archived") return true;
75
+ if (!(from in LINEAR_TRANSITIONS)) return false;
76
+ return LINEAR_TRANSITIONS[from as keyof typeof LINEAR_TRANSITIONS] === to;
77
+ }
@@ -0,0 +1,16 @@
1
+ {
2
+ "id": "roadmap-planner",
3
+ "name": "Roadmaps",
4
+ "version": "0.1.0",
5
+ "description": "Standalone roadmap planning plugin",
6
+ "dashboardViews": [
7
+ {
8
+ "viewId": "roadmaps",
9
+ "label": "Roadmaps",
10
+ "componentPath": "./dashboard-view",
11
+ "icon": "Map",
12
+ "placement": "primary",
13
+ "order": 30
14
+ }
15
+ ]
16
+ }
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "@fusion-plugin-examples/roadmap",
3
+ "version": "0.1.2",
4
+ "type": "module",
5
+ "description": "Roadmap plugin package for Fusion",
6
+ "private": true,
7
+ "exports": {
8
+ ".": {
9
+ "types": "./src/index.ts",
10
+ "import": "./src/index.ts"
11
+ },
12
+ "./server": {
13
+ "types": "./src/server/index.d.ts",
14
+ "import": "./src/server/index.ts"
15
+ },
16
+ "./dashboard-view": {
17
+ "types": "./src/dashboard-view.tsx",
18
+ "import": "./src/dashboard-view.tsx"
19
+ },
20
+ "./roadmap-suggestions": {
21
+ "types": "./src/roadmap-suggestions.d.ts",
22
+ "import": "./src/roadmap-suggestions.ts"
23
+ }
24
+ },
25
+ "scripts": {
26
+ "build": "tsc",
27
+ "test": "vitest run --silent=passed-only --reporter=dot"
28
+ },
29
+ "dependencies": {
30
+ "@fusion/core": "workspace:*",
31
+ "@fusion/dashboard": "workspace:*",
32
+ "@fusion/plugin-sdk": "workspace:*",
33
+ "express": "^5.1.0",
34
+ "lucide-react": "^0.542.0",
35
+ "react": "^19.0.0",
36
+ "react-dom": "^19.2.4"
37
+ },
38
+ "devDependencies": {
39
+ "@testing-library/jest-dom": "^6.6.3",
40
+ "@testing-library/react": "^16.3.2",
41
+ "@testing-library/user-event": "^14.6.1",
42
+ "@types/express": "^5.0.5",
43
+ "@types/node": "^25.5.2",
44
+ "@types/react": "^19.0.0",
45
+ "typescript": "^5.7.0",
46
+ "vitest": "^3.2.4"
47
+ }
48
+ }