@runfusion/fusion 0.24.0 → 0.26.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 (167) hide show
  1. package/README.md +6 -0
  2. package/dist/bin.js +18646 -15669
  3. package/dist/client/assets/AgentDetailView-Cv-vgOj3.js +18 -0
  4. package/dist/client/assets/{AgentsView-BkB9FiMT.js → AgentsView-D6Zi5zfP.js} +4 -4
  5. package/dist/client/assets/ChatView-CAHjY9uO.js +1 -0
  6. package/dist/client/assets/{DevServerView-BkvtjZBa.js → DevServerView--_WBvIDQ.js} +1 -1
  7. package/dist/client/assets/{DirectoryPicker-BK-KbnhP.js → DirectoryPicker-xedtR-Rd.js} +1 -1
  8. package/dist/client/assets/{DocumentsView-BEg1CQAk.js → DocumentsView-Bg2oaZks.js} +1 -1
  9. package/dist/client/assets/{EvalsView-Berf9bQm.js → EvalsView-B3uOCXfr.js} +1 -1
  10. package/dist/client/assets/{ExperimentalAgentOnboardingModal-jcInE50G.js → ExperimentalAgentOnboardingModal-Bx6yXVS5.js} +1 -1
  11. package/dist/client/assets/{InsightsView-BX5bSF1J.js → InsightsView-Q1zvtF4F.js} +1 -1
  12. package/dist/client/assets/MemoryView-xcN_eouf.js +2 -0
  13. package/dist/client/assets/MemoryView-zaXewZzi.css +1 -0
  14. package/dist/client/assets/{NodesView-DLUOBLf6.js → NodesView-RxXg58_Q.js} +1 -1
  15. package/dist/client/assets/{PiExtensionsManager-COlJf0Kx.js → PiExtensionsManager-Cc8aAZXg.js} +2 -2
  16. package/dist/client/assets/PluginManager-BEkyBajl.js +1 -0
  17. package/dist/client/assets/{ResearchView-BzCcDAS4.css → ResearchView-BEI4ZSGs.css} +1 -1
  18. package/dist/client/assets/ResearchView-CERNf7sJ.js +1 -0
  19. package/dist/client/assets/{SettingsModal-yRqM4DV8.js → SettingsModal-B1r0yASu.js} +1 -1
  20. package/dist/client/assets/SettingsModal-BLsac7CJ.js +31 -0
  21. package/dist/client/assets/SettingsModal-Cis-4Lot.css +1 -0
  22. package/dist/client/assets/{SetupWizardModal-uUZk3TKT.js → SetupWizardModal-D1q548_L.js} +1 -1
  23. package/dist/client/assets/{SkillsView-CP8JX0P_.js → SkillsView-ClLM6u6p.js} +1 -1
  24. package/dist/client/assets/StashRecoveryView-B_8WIQEo.css +1 -0
  25. package/dist/client/assets/StashRecoveryView-ze0pEZ5U.js +1 -0
  26. package/dist/client/assets/{TodoView-DCRIkDZ-.js → TodoView-CTmIfy2M.js} +1 -1
  27. package/dist/client/assets/{dashboard-view-lR7YYmSC.js → dashboard-view-4xAN3yO5.js} +2 -2
  28. package/dist/client/assets/{folder-open-DHjELt8-.js → folder-open-BZuKESeq.js} +1 -1
  29. package/dist/client/assets/index-Bdw6llW6.js +692 -0
  30. package/dist/client/assets/index-CZGlyJuS.css +1 -0
  31. package/dist/client/assets/{star-DYesq1AV.js → star-D75YKEq-.js} +1 -1
  32. package/dist/client/assets/{upload-DTWF3Db5.js → upload-BYYTgWFj.js} +1 -1
  33. package/dist/client/assets/{users--syrel4l.js → users-RS90Aii3.js} +1 -1
  34. package/dist/client/index.html +2 -2
  35. package/dist/client/version.json +1 -1
  36. package/dist/droid-cli/package.json +1 -1
  37. package/dist/extension.js +5640 -3618
  38. package/dist/pi-claude-cli/package.json +1 -1
  39. package/dist/plugins/fusion-plugin-cli-printing-press/manifest.json +6 -0
  40. package/dist/plugins/fusion-plugin-cli-printing-press/package.json +26 -0
  41. package/dist/plugins/fusion-plugin-cli-printing-press/src/__tests__/manifest.test.ts +20 -0
  42. package/dist/plugins/fusion-plugin-cli-printing-press/src/index.ts +14 -0
  43. package/dist/plugins/fusion-plugin-cursor-runtime/bundled.js +9 -11
  44. package/dist/plugins/fusion-plugin-cursor-runtime/package.json +1 -1
  45. package/dist/plugins/fusion-plugin-dependency-graph/bundled.js +30 -0
  46. package/dist/plugins/fusion-plugin-dependency-graph/package.json +3 -28
  47. package/dist/plugins/fusion-plugin-droid-runtime/bundled.js +899 -895
  48. package/dist/plugins/fusion-plugin-droid-runtime/package.json +1 -1
  49. package/dist/plugins/fusion-plugin-hermes-runtime/bundled.js +68 -71
  50. package/dist/plugins/fusion-plugin-hermes-runtime/package.json +1 -1
  51. package/dist/plugins/fusion-plugin-openclaw-runtime/bundled.js +47 -50
  52. package/dist/plugins/fusion-plugin-openclaw-runtime/package.json +1 -1
  53. package/dist/plugins/fusion-plugin-paperclip-runtime/bundled.js +155 -109
  54. package/dist/plugins/fusion-plugin-paperclip-runtime/package.json +1 -1
  55. package/dist/plugins/fusion-plugin-reports/package.json +1 -1
  56. package/dist/plugins/fusion-plugin-reports/src/index.ts +49 -3
  57. package/dist/plugins/fusion-plugin-reports/src/report-schema.ts +38 -0
  58. package/dist/plugins/fusion-plugin-reports/src/store/__tests__/report-schema.test.ts +66 -0
  59. package/dist/plugins/fusion-plugin-reports/src/store/__tests__/report-store.test.ts +177 -0
  60. package/dist/plugins/fusion-plugin-reports/src/store/report-store.ts +341 -0
  61. package/dist/plugins/fusion-plugin-reports/src/store/report-types.ts +77 -0
  62. package/dist/plugins/fusion-plugin-roadmap/{src/dashboard/RoadmapsView.css → bundled.css} +13 -219
  63. package/dist/plugins/fusion-plugin-roadmap/bundled.js +29535 -0
  64. package/dist/plugins/fusion-plugin-roadmap/package.json +4 -41
  65. package/dist/plugins/fusion-plugin-whatsapp-chat/package.json +1 -1
  66. package/package.json +2 -3
  67. package/dist/client/assets/AgentDetailView-gy_5SUj2.js +0 -18
  68. package/dist/client/assets/ChatView-B_-B8fqu.js +0 -1
  69. package/dist/client/assets/MemoryView-CKElJY_3.js +0 -2
  70. package/dist/client/assets/MemoryView-DiajLXby.css +0 -1
  71. package/dist/client/assets/PluginManager-CfW55BF4.js +0 -1
  72. package/dist/client/assets/ResearchView-B256Lr8I.js +0 -1
  73. package/dist/client/assets/SettingsModal-BeA_nQtW.js +0 -31
  74. package/dist/client/assets/SettingsModal-DzsLquBu.css +0 -1
  75. package/dist/client/assets/index-CQyVRLOb.js +0 -692
  76. package/dist/client/assets/index-CxA2Nn0_.css +0 -1
  77. package/dist/plugins/fusion-plugin-dependency-graph/src/DependencyGraph.css +0 -58
  78. package/dist/plugins/fusion-plugin-dependency-graph/src/DependencyGraph.tsx +0 -301
  79. package/dist/plugins/fusion-plugin-dependency-graph/src/GraphHighlight.css +0 -27
  80. package/dist/plugins/fusion-plugin-dependency-graph/src/GraphTaskNode.css +0 -157
  81. package/dist/plugins/fusion-plugin-dependency-graph/src/GraphTaskNode.tsx +0 -126
  82. package/dist/plugins/fusion-plugin-dependency-graph/src/GraphToolbar.css +0 -35
  83. package/dist/plugins/fusion-plugin-dependency-graph/src/GraphToolbar.tsx +0 -36
  84. package/dist/plugins/fusion-plugin-dependency-graph/src/__tests__/DependencyGraph.highlighting.test.tsx +0 -112
  85. package/dist/plugins/fusion-plugin-dependency-graph/src/__tests__/DependencyGraph.persistence.test.tsx +0 -115
  86. package/dist/plugins/fusion-plugin-dependency-graph/src/__tests__/DependencyGraph.test.tsx +0 -128
  87. package/dist/plugins/fusion-plugin-dependency-graph/src/__tests__/GraphTaskNode.drag.test.tsx +0 -82
  88. package/dist/plugins/fusion-plugin-dependency-graph/src/__tests__/GraphTaskNode.test.tsx +0 -307
  89. package/dist/plugins/fusion-plugin-dependency-graph/src/__tests__/GraphToolbar.test.tsx +0 -60
  90. package/dist/plugins/fusion-plugin-dependency-graph/src/__tests__/edges.test.tsx +0 -75
  91. package/dist/plugins/fusion-plugin-dependency-graph/src/__tests__/filtering.test.tsx +0 -62
  92. package/dist/plugins/fusion-plugin-dependency-graph/src/__tests__/filters.test.ts +0 -78
  93. package/dist/plugins/fusion-plugin-dependency-graph/src/__tests__/graphPositionStorage.test.ts +0 -95
  94. package/dist/plugins/fusion-plugin-dependency-graph/src/__tests__/host-integration.test.ts +0 -74
  95. package/dist/plugins/fusion-plugin-dependency-graph/src/__tests__/index.test.ts +0 -58
  96. package/dist/plugins/fusion-plugin-dependency-graph/src/__tests__/interactions.test.tsx +0 -121
  97. package/dist/plugins/fusion-plugin-dependency-graph/src/__tests__/layout.test.ts +0 -70
  98. package/dist/plugins/fusion-plugin-dependency-graph/src/__tests__/persistence.test.tsx +0 -89
  99. package/dist/plugins/fusion-plugin-dependency-graph/src/__tests__/useGraphData.test.ts +0 -86
  100. package/dist/plugins/fusion-plugin-dependency-graph/src/__tests__/useGraphInteraction.test.ts +0 -167
  101. package/dist/plugins/fusion-plugin-dependency-graph/src/__tests__/useGraphPositions.test.ts +0 -66
  102. package/dist/plugins/fusion-plugin-dependency-graph/src/__tests__/useNodeDrag.test.ts +0 -81
  103. package/dist/plugins/fusion-plugin-dependency-graph/src/dashboard-interop.d.ts +0 -35
  104. package/dist/plugins/fusion-plugin-dependency-graph/src/dashboard-view.tsx +0 -19
  105. package/dist/plugins/fusion-plugin-dependency-graph/src/edges.tsx +0 -70
  106. package/dist/plugins/fusion-plugin-dependency-graph/src/filters.ts +0 -8
  107. package/dist/plugins/fusion-plugin-dependency-graph/src/hooks/__tests__/useDependencyChain.test.ts +0 -53
  108. package/dist/plugins/fusion-plugin-dependency-graph/src/hooks/useDependencyChain.ts +0 -60
  109. package/dist/plugins/fusion-plugin-dependency-graph/src/hooks/useGraphPositions.ts +0 -45
  110. package/dist/plugins/fusion-plugin-dependency-graph/src/hooks/useNodeDrag.ts +0 -114
  111. package/dist/plugins/fusion-plugin-dependency-graph/src/index.ts +0 -24
  112. package/dist/plugins/fusion-plugin-dependency-graph/src/layout.ts +0 -91
  113. package/dist/plugins/fusion-plugin-dependency-graph/src/styles/drag.css +0 -15
  114. package/dist/plugins/fusion-plugin-dependency-graph/src/types.ts +0 -21
  115. package/dist/plugins/fusion-plugin-dependency-graph/src/useGraphData.ts +0 -17
  116. package/dist/plugins/fusion-plugin-dependency-graph/src/useGraphInteraction.ts +0 -292
  117. package/dist/plugins/fusion-plugin-dependency-graph/src/utils/graphPositionStorage.ts +0 -65
  118. package/dist/plugins/fusion-plugin-roadmap/src/__tests__/api-client.test.ts +0 -101
  119. package/dist/plugins/fusion-plugin-roadmap/src/__tests__/index.test.ts +0 -92
  120. package/dist/plugins/fusion-plugin-roadmap/src/__tests__/roadmap-routes.test.ts +0 -48
  121. package/dist/plugins/fusion-plugin-roadmap/src/__tests__/roadmap-suggestions.test.ts +0 -31
  122. package/dist/plugins/fusion-plugin-roadmap/src/dashboard/RoadmapsView.tsx +0 -2559
  123. package/dist/plugins/fusion-plugin-roadmap/src/dashboard/__tests__/RoadmapsView.test.tsx +0 -1144
  124. package/dist/plugins/fusion-plugin-roadmap/src/dashboard/__tests__/useRoadmaps.test.ts +0 -1756
  125. package/dist/plugins/fusion-plugin-roadmap/src/dashboard/api.ts +0 -70
  126. package/dist/plugins/fusion-plugin-roadmap/src/dashboard/test-setup.ts +0 -7
  127. package/dist/plugins/fusion-plugin-roadmap/src/dashboard/types.ts +0 -1
  128. package/dist/plugins/fusion-plugin-roadmap/src/dashboard/useConfirm.ts +0 -8
  129. package/dist/plugins/fusion-plugin-roadmap/src/dashboard/useRoadmaps.ts +0 -1188
  130. package/dist/plugins/fusion-plugin-roadmap/src/dashboard/useViewportMode.ts +0 -20
  131. package/dist/plugins/fusion-plugin-roadmap/src/dashboard-view.tsx +0 -6
  132. package/dist/plugins/fusion-plugin-roadmap/src/index.ts +0 -74
  133. package/dist/plugins/fusion-plugin-roadmap/src/roadmap-routes.ts +0 -1
  134. package/dist/plugins/fusion-plugin-roadmap/src/roadmap-schema.ts +0 -41
  135. package/dist/plugins/fusion-plugin-roadmap/src/roadmap-suggestions.d.ts +0 -15
  136. package/dist/plugins/fusion-plugin-roadmap/src/roadmap-suggestions.ts +0 -15
  137. package/dist/plugins/fusion-plugin-roadmap/src/roadmap-types.d.ts +0 -283
  138. package/dist/plugins/fusion-plugin-roadmap/src/roadmap-types.d.ts.map +0 -1
  139. package/dist/plugins/fusion-plugin-roadmap/src/roadmap-types.js +0 -21
  140. package/dist/plugins/fusion-plugin-roadmap/src/roadmap-types.js.map +0 -1
  141. package/dist/plugins/fusion-plugin-roadmap/src/roadmap-types.ts +0 -310
  142. package/dist/plugins/fusion-plugin-roadmap/src/routes/roadmap-routes.d.ts +0 -5
  143. package/dist/plugins/fusion-plugin-roadmap/src/routes/roadmap-routes.d.ts.map +0 -1
  144. package/dist/plugins/fusion-plugin-roadmap/src/routes/roadmap-routes.js +0 -361
  145. package/dist/plugins/fusion-plugin-roadmap/src/routes/roadmap-routes.js.map +0 -1
  146. package/dist/plugins/fusion-plugin-roadmap/src/routes/roadmap-routes.ts +0 -408
  147. package/dist/plugins/fusion-plugin-roadmap/src/routes/roadmap-suggestions.d.ts +0 -68
  148. package/dist/plugins/fusion-plugin-roadmap/src/routes/roadmap-suggestions.d.ts.map +0 -1
  149. package/dist/plugins/fusion-plugin-roadmap/src/routes/roadmap-suggestions.js +0 -300
  150. package/dist/plugins/fusion-plugin-roadmap/src/routes/roadmap-suggestions.js.map +0 -1
  151. package/dist/plugins/fusion-plugin-roadmap/src/routes/roadmap-suggestions.ts +0 -381
  152. package/dist/plugins/fusion-plugin-roadmap/src/server/index.d.ts +0 -3
  153. package/dist/plugins/fusion-plugin-roadmap/src/server/index.ts +0 -1
  154. package/dist/plugins/fusion-plugin-roadmap/src/store/__tests__/roadmap-handoff.test.ts +0 -445
  155. package/dist/plugins/fusion-plugin-roadmap/src/store/__tests__/roadmap-ordering.test.ts +0 -334
  156. package/dist/plugins/fusion-plugin-roadmap/src/store/__tests__/roadmap-store.test.ts +0 -1318
  157. package/dist/plugins/fusion-plugin-roadmap/src/store/roadmap-handoff.ts +0 -163
  158. package/dist/plugins/fusion-plugin-roadmap/src/store/roadmap-ordering.d.ts +0 -37
  159. package/dist/plugins/fusion-plugin-roadmap/src/store/roadmap-ordering.d.ts.map +0 -1
  160. package/dist/plugins/fusion-plugin-roadmap/src/store/roadmap-ordering.js +0 -188
  161. package/dist/plugins/fusion-plugin-roadmap/src/store/roadmap-ordering.js.map +0 -1
  162. package/dist/plugins/fusion-plugin-roadmap/src/store/roadmap-ordering.ts +0 -311
  163. package/dist/plugins/fusion-plugin-roadmap/src/store/roadmap-store.d.ts +0 -299
  164. package/dist/plugins/fusion-plugin-roadmap/src/store/roadmap-store.d.ts.map +0 -1
  165. package/dist/plugins/fusion-plugin-roadmap/src/store/roadmap-store.js +0 -765
  166. package/dist/plugins/fusion-plugin-roadmap/src/store/roadmap-store.js.map +0 -1
  167. package/dist/plugins/fusion-plugin-roadmap/src/store/roadmap-store.ts +0 -1001
@@ -0,0 +1,38 @@
1
+ import type { Database } from "@fusion/core";
2
+
3
+ export function ensureReportSchema(db: Database): void {
4
+ db.exec(`
5
+ CREATE TABLE IF NOT EXISTS reports (
6
+ id TEXT PRIMARY KEY,
7
+ cadence TEXT NOT NULL CHECK (cadence IN ('daily','weekly','monthly','quarterly','manual')),
8
+ periodStart TEXT NOT NULL,
9
+ periodEnd TEXT NOT NULL,
10
+ title TEXT NOT NULL,
11
+ status TEXT NOT NULL CHECK (status IN ('generating','review_pending','review_in_progress','review_complete','approved','published','archived','failed')),
12
+ generationStartedAt TEXT NOT NULL,
13
+ generationCompletedAt TEXT,
14
+ reviewStartedAt TEXT,
15
+ reviewCompletedAt TEXT,
16
+ approvedAt TEXT,
17
+ approvedBy TEXT,
18
+ publishedAt TEXT,
19
+ archivedAt TEXT,
20
+ failureReason TEXT,
21
+ draftMarkdown TEXT,
22
+ renderedHtmlPath TEXT,
23
+ metadataJson TEXT NOT NULL DEFAULT '{}',
24
+ combinedReviewJson TEXT,
25
+ createdAt TEXT NOT NULL,
26
+ updatedAt TEXT NOT NULL
27
+ );
28
+
29
+ CREATE INDEX IF NOT EXISTS idxReportsCadenceCreated
30
+ ON reports(cadence, createdAt DESC, id);
31
+
32
+ CREATE INDEX IF NOT EXISTS idxReportsStatusUpdated
33
+ ON reports(status, updatedAt DESC, id);
34
+
35
+ CREATE INDEX IF NOT EXISTS idxReportsPeriod
36
+ ON reports(periodStart, periodEnd, id);
37
+ `);
38
+ }
@@ -0,0 +1,66 @@
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 } from "vitest";
7
+ import { ensureReportSchema } from "../../report-schema.js";
8
+
9
+ function makeTmpDir(): string {
10
+ return mkdtempSync(join(tmpdir(), "report-schema-test-"));
11
+ }
12
+
13
+ describe("ensureReportSchema", () => {
14
+ let tmp: string;
15
+ let db: Database;
16
+
17
+ beforeEach(() => {
18
+ tmp = makeTmpDir();
19
+ db = new Database(join(tmp, ".fusion"), { inMemory: true });
20
+ db.init();
21
+ });
22
+
23
+ afterEach(async () => {
24
+ db.close();
25
+ await rm(tmp, { recursive: true, force: true });
26
+ });
27
+
28
+ it("creates reports table and indexes idempotently", () => {
29
+ ensureReportSchema(db);
30
+ ensureReportSchema(db);
31
+
32
+ const table = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='reports'").get() as { name: string } | undefined;
33
+ expect(table?.name).toBe("reports");
34
+
35
+ const indexes = db.prepare("SELECT name FROM sqlite_master WHERE type='index' AND tbl_name='reports' ORDER BY name").all() as Array<{ name: string }>;
36
+ expect(indexes.map((row) => row.name)).toEqual(expect.arrayContaining([
37
+ "idxReportsCadenceCreated",
38
+ "idxReportsStatusUpdated",
39
+ "idxReportsPeriod",
40
+ ]));
41
+ });
42
+
43
+ it("enforces cadence and status CHECK constraints", () => {
44
+ ensureReportSchema(db);
45
+
46
+ const base = {
47
+ id: "rep_1",
48
+ cadence: "daily",
49
+ periodStart: "2026-05-08T00:00:00.000Z",
50
+ periodEnd: "2026-05-08T23:59:59.999Z",
51
+ title: "Daily Report",
52
+ status: "generating",
53
+ generationStartedAt: "2026-05-09T00:00:00.000Z",
54
+ createdAt: "2026-05-09T00:00:00.000Z",
55
+ updatedAt: "2026-05-09T00:00:00.000Z",
56
+ };
57
+
58
+ const stmt = db.prepare(`
59
+ INSERT INTO reports (id, cadence, periodStart, periodEnd, title, status, generationStartedAt, createdAt, updatedAt)
60
+ VALUES (@id, @cadence, @periodStart, @periodEnd, @title, @status, @generationStartedAt, @createdAt, @updatedAt)
61
+ `);
62
+
63
+ expect(() => stmt.run({ ...base, id: "rep_bad_cadence", cadence: "hourly" })).toThrow();
64
+ expect(() => stmt.run({ ...base, id: "rep_bad_status", status: "queued" })).toThrow();
65
+ });
66
+ });
@@ -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
+ }