@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.
- package/dist/bin.js +27921 -21003
- package/dist/client/assets/AgentDetailView-BwJaLqZh.css +1 -0
- package/dist/client/assets/AgentDetailView-ZbHEbYRT.js +18 -0
- package/dist/client/assets/AgentsView-B3jYk8Kt.js +29 -0
- package/dist/client/assets/{AgentsView-DSGQWObq.css → AgentsView-CV3vm7Qk.css} +1 -1
- package/dist/client/assets/ChatView-DhPkiEGs.js +1 -0
- package/dist/client/assets/ChatView-DwJAd5G1.css +1 -0
- package/dist/client/assets/{DevServerView-C9lzHrcT.js → DevServerView-DyGDEiBP.js} +1 -1
- package/dist/client/assets/{DirectoryPicker-aVdFaV37.js → DirectoryPicker-D5UIeIl6.js} +1 -1
- package/dist/client/assets/{DocumentsView-DIpg3NSP.js → DocumentsView-DNHu1T8K.js} +1 -1
- package/dist/client/assets/{DocumentsView-BrhyOdeE.css → DocumentsView-gv4zG3aT.css} +1 -1
- package/dist/client/assets/EvalsView-CUNJ1TLc.css +1 -0
- package/dist/client/assets/EvalsView-CpRobtDi.js +1 -0
- package/dist/client/assets/{agentSkills-DDHJnrkn.css → ExperimentalAgentOnboardingModal-B-APN_lM.css} +1 -1
- package/dist/client/assets/ExperimentalAgentOnboardingModal-DOY_oZi7.js +499 -0
- package/dist/client/assets/InsightsView-B0J4mhzV.css +1 -0
- package/dist/client/assets/InsightsView-vp0RE8Mg.js +11 -0
- package/dist/client/assets/MemoryView-PSc5lGJt.js +2 -0
- package/dist/client/assets/MemoryView-zaXewZzi.css +1 -0
- package/dist/client/assets/NodesView-DMj6HGeC.js +14 -0
- package/dist/client/assets/NodesView-DT4pXowv.css +1 -0
- package/dist/client/assets/{PiExtensionsManager-Buopv-jb.js → PiExtensionsManager-DL_QcN56.js} +2 -2
- package/dist/client/assets/PluginManager-BtYKm8IT.js +1 -0
- package/dist/client/assets/PluginManager-DtRQXia5.css +1 -0
- package/dist/client/assets/{ResearchView-_BHXUv2j.js → ResearchView-BhWqfdV0.js} +1 -1
- package/dist/client/assets/SettingsModal-BAgB4_AR.js +31 -0
- package/dist/client/assets/SettingsModal-CUCyaAyE.js +1 -0
- package/dist/client/assets/SettingsModal-DzsLquBu.css +1 -0
- package/dist/client/assets/SetupWizardModal-BKscasuh.js +1 -0
- package/dist/client/assets/{SkillsView-hDpTBdFT.js → SkillsView-BdELqTy7.js} +1 -1
- package/dist/client/assets/TodoView-Cx9cVhq7.css +1 -0
- package/dist/client/assets/TodoView-DFNGBDNV.js +6 -0
- package/dist/client/assets/{folder-open-usZkXdq2.js → folder-open-k1xmUMyr.js} +1 -1
- package/dist/client/assets/index-Qq2JOOWx.css +1 -0
- package/dist/client/assets/index-TFYXEVpn.js +692 -0
- package/dist/client/assets/projectDetection-G3XuxD2X.js +1 -0
- package/dist/client/assets/{star-BAT_ObKE.js → star-ne32r3Y4.js} +1 -1
- package/dist/client/assets/{upload-BC2YKNEV.js → upload-MS-2Gx53.js} +1 -1
- package/dist/client/assets/{users-Dkd4rtrN.js → users-C519GSjH.js} +1 -1
- package/dist/client/index.html +12 -20
- package/dist/client/theme-data.css +106 -0
- package/dist/client/version.json +1 -1
- package/dist/droid-cli/package.json +1 -1
- package/dist/extension.js +15395 -9935
- package/dist/pi-claude-cli/package.json +1 -1
- package/dist/plugins/fusion-plugin-cursor-runtime/bundled.js +216 -0
- package/dist/plugins/fusion-plugin-cursor-runtime/manifest.json +6 -0
- package/dist/plugins/fusion-plugin-cursor-runtime/package.json +11 -0
- package/dist/plugins/fusion-plugin-dependency-graph/bundled.js +30 -0
- package/dist/plugins/fusion-plugin-dependency-graph/manifest.json +1 -1
- package/dist/plugins/fusion-plugin-dependency-graph/package.json +3 -26
- package/dist/plugins/fusion-plugin-droid-runtime/bundled.js +136684 -0
- package/dist/plugins/fusion-plugin-droid-runtime/manifest.json +13 -0
- package/dist/plugins/fusion-plugin-droid-runtime/mcp-schema-server.cjs +49 -0
- package/dist/plugins/fusion-plugin-droid-runtime/package.json +11 -0
- package/dist/plugins/fusion-plugin-hermes-runtime/bundled.js +68 -71
- package/dist/plugins/fusion-plugin-hermes-runtime/package.json +1 -1
- package/dist/plugins/fusion-plugin-openclaw-runtime/bundled.js +137 -53
- package/dist/plugins/fusion-plugin-openclaw-runtime/mcp-schema-server.cjs +59 -0
- package/dist/plugins/fusion-plugin-openclaw-runtime/package.json +1 -1
- package/dist/plugins/fusion-plugin-paperclip-runtime/bundled.js +155 -109
- package/dist/plugins/fusion-plugin-paperclip-runtime/package.json +1 -1
- package/dist/plugins/fusion-plugin-reports/manifest.json +33 -0
- package/dist/plugins/fusion-plugin-reports/package.json +26 -0
- package/dist/plugins/fusion-plugin-reports/src/__tests__/manifest.test.ts +51 -0
- package/dist/plugins/fusion-plugin-reports/src/__tests__/review-panel.test.ts +166 -0
- package/dist/plugins/fusion-plugin-reports/src/__tests__/settings.test.ts +157 -0
- package/dist/plugins/fusion-plugin-reports/src/index.ts +87 -0
- package/dist/plugins/fusion-plugin-reports/src/report-schema.ts +38 -0
- package/dist/plugins/fusion-plugin-reports/src/review-panel.ts +294 -0
- package/dist/plugins/fusion-plugin-reports/src/review-types.ts +75 -0
- package/dist/plugins/fusion-plugin-reports/src/settings.ts +105 -0
- package/dist/plugins/fusion-plugin-reports/src/store/__tests__/report-schema.test.ts +66 -0
- package/dist/plugins/fusion-plugin-reports/src/store/__tests__/report-store.test.ts +177 -0
- package/dist/plugins/fusion-plugin-reports/src/store/report-store.ts +341 -0
- package/dist/plugins/fusion-plugin-reports/src/store/report-types.ts +77 -0
- package/dist/plugins/fusion-plugin-roadmap/manifest.json +16 -0
- package/dist/plugins/fusion-plugin-roadmap/package.json +48 -0
- package/dist/plugins/fusion-plugin-roadmap/src/__tests__/api-client.test.ts +101 -0
- package/dist/plugins/fusion-plugin-roadmap/src/__tests__/index.test.ts +92 -0
- package/dist/plugins/fusion-plugin-roadmap/src/__tests__/roadmap-routes.test.ts +48 -0
- package/dist/plugins/fusion-plugin-roadmap/src/__tests__/roadmap-suggestions.test.ts +31 -0
- package/dist/plugins/fusion-plugin-roadmap/src/dashboard/RoadmapsView.css +1299 -0
- package/dist/plugins/fusion-plugin-roadmap/src/dashboard/RoadmapsView.tsx +2559 -0
- package/dist/plugins/fusion-plugin-roadmap/src/dashboard/__tests__/RoadmapsView.test.tsx +1144 -0
- package/dist/plugins/fusion-plugin-roadmap/src/dashboard/__tests__/useRoadmaps.test.ts +1756 -0
- package/dist/plugins/fusion-plugin-roadmap/src/dashboard/api.ts +70 -0
- package/dist/plugins/fusion-plugin-roadmap/src/dashboard/test-setup.ts +7 -0
- package/dist/plugins/fusion-plugin-roadmap/src/dashboard/types.ts +1 -0
- package/dist/plugins/fusion-plugin-roadmap/src/dashboard/useConfirm.ts +8 -0
- package/dist/plugins/fusion-plugin-roadmap/src/dashboard/useRoadmaps.ts +1188 -0
- package/dist/plugins/fusion-plugin-roadmap/src/dashboard/useViewportMode.ts +20 -0
- package/dist/plugins/fusion-plugin-roadmap/src/dashboard-view.tsx +6 -0
- package/dist/plugins/fusion-plugin-roadmap/src/index.ts +74 -0
- package/dist/plugins/fusion-plugin-roadmap/src/roadmap-routes.ts +1 -0
- package/dist/plugins/fusion-plugin-roadmap/src/roadmap-schema.ts +41 -0
- package/dist/plugins/fusion-plugin-roadmap/src/roadmap-suggestions.d.ts +15 -0
- package/dist/plugins/fusion-plugin-roadmap/src/roadmap-suggestions.ts +15 -0
- package/dist/plugins/fusion-plugin-roadmap/src/roadmap-types.d.ts +283 -0
- package/dist/plugins/fusion-plugin-roadmap/src/roadmap-types.d.ts.map +1 -0
- package/dist/plugins/fusion-plugin-roadmap/src/roadmap-types.js +21 -0
- package/dist/plugins/fusion-plugin-roadmap/src/roadmap-types.js.map +1 -0
- package/dist/plugins/fusion-plugin-roadmap/src/roadmap-types.ts +310 -0
- package/dist/plugins/fusion-plugin-roadmap/src/routes/roadmap-routes.d.ts +5 -0
- package/dist/plugins/fusion-plugin-roadmap/src/routes/roadmap-routes.d.ts.map +1 -0
- package/dist/plugins/fusion-plugin-roadmap/src/routes/roadmap-routes.js +361 -0
- package/dist/plugins/fusion-plugin-roadmap/src/routes/roadmap-routes.js.map +1 -0
- package/dist/plugins/fusion-plugin-roadmap/src/routes/roadmap-routes.ts +408 -0
- package/dist/plugins/fusion-plugin-roadmap/src/routes/roadmap-suggestions.d.ts +68 -0
- package/dist/plugins/fusion-plugin-roadmap/src/routes/roadmap-suggestions.d.ts.map +1 -0
- package/dist/plugins/fusion-plugin-roadmap/src/routes/roadmap-suggestions.js +300 -0
- package/dist/plugins/fusion-plugin-roadmap/src/routes/roadmap-suggestions.js.map +1 -0
- package/dist/plugins/fusion-plugin-roadmap/src/routes/roadmap-suggestions.ts +381 -0
- package/dist/plugins/fusion-plugin-roadmap/src/server/index.d.ts +3 -0
- package/dist/plugins/fusion-plugin-roadmap/src/server/index.ts +1 -0
- package/dist/plugins/fusion-plugin-roadmap/src/store/__tests__/roadmap-handoff.test.ts +445 -0
- package/dist/plugins/fusion-plugin-roadmap/src/store/__tests__/roadmap-ordering.test.ts +334 -0
- package/dist/plugins/fusion-plugin-roadmap/src/store/__tests__/roadmap-store.test.ts +1318 -0
- package/dist/plugins/fusion-plugin-roadmap/src/store/roadmap-handoff.ts +163 -0
- package/dist/plugins/fusion-plugin-roadmap/src/store/roadmap-ordering.d.ts +37 -0
- package/dist/plugins/fusion-plugin-roadmap/src/store/roadmap-ordering.d.ts.map +1 -0
- package/dist/plugins/fusion-plugin-roadmap/src/store/roadmap-ordering.js +188 -0
- package/dist/plugins/fusion-plugin-roadmap/src/store/roadmap-ordering.js.map +1 -0
- package/dist/plugins/fusion-plugin-roadmap/src/store/roadmap-ordering.ts +311 -0
- package/dist/plugins/fusion-plugin-roadmap/src/store/roadmap-store.d.ts +299 -0
- package/dist/plugins/fusion-plugin-roadmap/src/store/roadmap-store.d.ts.map +1 -0
- package/dist/plugins/fusion-plugin-roadmap/src/store/roadmap-store.js +765 -0
- package/dist/plugins/fusion-plugin-roadmap/src/store/roadmap-store.js.map +1 -0
- package/dist/plugins/fusion-plugin-roadmap/src/store/roadmap-store.ts +1001 -0
- package/dist/plugins/fusion-plugin-whatsapp-chat/manifest.json +8 -0
- package/dist/plugins/fusion-plugin-whatsapp-chat/package.json +34 -0
- package/dist/plugins/fusion-plugin-whatsapp-chat/src/__tests__/auth-state.test.ts +99 -0
- package/dist/plugins/fusion-plugin-whatsapp-chat/src/__tests__/connection.test.ts +145 -0
- package/dist/plugins/fusion-plugin-whatsapp-chat/src/__tests__/index.test.ts +216 -0
- package/dist/plugins/fusion-plugin-whatsapp-chat/src/__tests__/reply.test.ts +52 -0
- package/dist/plugins/fusion-plugin-whatsapp-chat/src/auth-state.ts +89 -0
- package/dist/plugins/fusion-plugin-whatsapp-chat/src/connection.ts +253 -0
- package/dist/plugins/fusion-plugin-whatsapp-chat/src/index.ts +262 -0
- package/dist/plugins/fusion-plugin-whatsapp-chat/src/qrcode.d.ts +1 -0
- package/dist/plugins/fusion-plugin-whatsapp-chat/src/reply.ts +37 -0
- package/package.json +2 -2
- package/skill/fusion/SKILL.md +2 -2
- package/skill/fusion/references/engine-tools.md +3 -0
- package/skill/fusion/references/extension-tools.md +39 -0
- package/skill/fusion/references/fusion-capabilities.md +3 -0
- package/dist/client/assets/AgentDetailView-C1XceMgi.js +0 -18
- package/dist/client/assets/AgentDetailView-CeO_1MK7.css +0 -1
- package/dist/client/assets/AgentsView-Deh125ss.js +0 -527
- package/dist/client/assets/ChatView-7D_RQDqT.js +0 -1
- package/dist/client/assets/InsightsView-AWo5o_81.css +0 -1
- package/dist/client/assets/InsightsView-jKjEFAx_.js +0 -11
- package/dist/client/assets/MemoryView-DiajLXby.css +0 -1
- package/dist/client/assets/MemoryView-nXlTqebk.js +0 -2
- package/dist/client/assets/NodesView-Di2SvOhg.js +0 -14
- package/dist/client/assets/NodesView-fXqDk9ur.css +0 -1
- package/dist/client/assets/PluginManager-B9-NbQ8f.js +0 -1
- package/dist/client/assets/PluginManager-C1DbPaar.css +0 -1
- package/dist/client/assets/RoadmapsView-DHWjUoc8.js +0 -6
- package/dist/client/assets/RoadmapsView-DdGlfuu-.css +0 -1
- package/dist/client/assets/SettingsModal-C89Ikhfm.js +0 -1
- package/dist/client/assets/SettingsModal-DHitIpsa.css +0 -1
- package/dist/client/assets/SettingsModal-DR_yirvK.js +0 -31
- package/dist/client/assets/SetupWizardModal-BtDMY9pa.js +0 -1
- package/dist/client/assets/agentSkills-B-w5wFHh.js +0 -1
- package/dist/client/assets/index-Bc6ZdGMz.css +0 -1
- package/dist/client/assets/index-D__RMku8.js +0 -694
- package/dist/plugins/fusion-plugin-dependency-graph/src/DependencyGraphView.css +0 -141
- package/dist/plugins/fusion-plugin-dependency-graph/src/DependencyGraphView.tsx +0 -428
- package/dist/plugins/fusion-plugin-dependency-graph/src/__tests__/DependencyGraphView.test.tsx +0 -261
- package/dist/plugins/fusion-plugin-dependency-graph/src/__tests__/storage.test.ts +0 -41
- package/dist/plugins/fusion-plugin-dependency-graph/src/index.ts +0 -25
- package/dist/plugins/fusion-plugin-dependency-graph/src/storage.ts +0 -22
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
import type { CreateAiSessionFactory, PluginContext } from "@fusion/core";
|
|
2
|
+
import { DEFAULT_REVIEW_PROMPT } from "./settings.js";
|
|
3
|
+
import type {
|
|
4
|
+
CombinedReview,
|
|
5
|
+
IndividualReview,
|
|
6
|
+
ReviewFailure,
|
|
7
|
+
ReviewPanelMember,
|
|
8
|
+
ReviewVerdict,
|
|
9
|
+
RunReviewPanelInput,
|
|
10
|
+
} from "./review-types.js";
|
|
11
|
+
import { ReviewPanelError, ReviewParseError, ReviewTimeoutError } from "./review-types.js";
|
|
12
|
+
|
|
13
|
+
let injectedCreateAiSession: CreateAiSessionFactory | undefined;
|
|
14
|
+
|
|
15
|
+
const MAX_PARSE_RETRIES = 1;
|
|
16
|
+
const MAX_MERGED_ITEMS = 25;
|
|
17
|
+
export const REVIEW_TIMEOUT_MS = 120_000;
|
|
18
|
+
|
|
19
|
+
interface AgentMessage {
|
|
20
|
+
role: string;
|
|
21
|
+
content?: string | Array<{ type: string; text: string }>;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function pickFactory(ctx: PluginContext): CreateAiSessionFactory | undefined {
|
|
25
|
+
return injectedCreateAiSession ?? ctx.createAiSession;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function getReviewPromptTemplate(reviewerId: string, settings: Record<string, unknown>): string {
|
|
29
|
+
const templates = settings.reviewPromptTemplates;
|
|
30
|
+
if (templates && typeof templates === "object" && !Array.isArray(templates)) {
|
|
31
|
+
const candidate = (templates as Record<string, unknown>)[reviewerId];
|
|
32
|
+
if (typeof candidate === "string" && candidate.trim()) return candidate.trim();
|
|
33
|
+
}
|
|
34
|
+
const reviewPrompt = settings.reviewPrompt;
|
|
35
|
+
if (typeof reviewPrompt === "string" && reviewPrompt.trim()) return reviewPrompt.trim();
|
|
36
|
+
return DEFAULT_REVIEW_PROMPT;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function buildSystemPrompt(member: ReviewPanelMember, settings: Record<string, unknown>): string {
|
|
40
|
+
const templateId = member.promptTemplateId ?? member.id;
|
|
41
|
+
const template = getReviewPromptTemplate(templateId, settings);
|
|
42
|
+
return `${template}\n\nReviewer perspective: ${member.perspective}`;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function extractJsonCandidate(text: string): string | null {
|
|
46
|
+
if (!text || !text.trim()) return null;
|
|
47
|
+
const codeBlockMatch = text.match(/```(?:json)?\s*([\s\S]*?)\s*```/i);
|
|
48
|
+
const source = codeBlockMatch?.[1]?.trim() ?? text.trim();
|
|
49
|
+
|
|
50
|
+
const startIndex = source.indexOf("{");
|
|
51
|
+
if (startIndex < 0) return null;
|
|
52
|
+
|
|
53
|
+
let depth = 0;
|
|
54
|
+
let inString = false;
|
|
55
|
+
let escaped = false;
|
|
56
|
+
for (let index = startIndex; index < source.length; index++) {
|
|
57
|
+
const char = source[index];
|
|
58
|
+
if (inString) {
|
|
59
|
+
if (escaped) escaped = false;
|
|
60
|
+
else if (char === "\\") escaped = true;
|
|
61
|
+
else if (char === '"') inString = false;
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (char === '"') {
|
|
66
|
+
inString = true;
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (char === "{") depth++;
|
|
71
|
+
if (char === "}") {
|
|
72
|
+
depth--;
|
|
73
|
+
if (depth === 0) return source.slice(startIndex, index + 1).trim();
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return source.slice(startIndex).trim();
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function asStringArray(value: unknown): string[] {
|
|
81
|
+
if (!Array.isArray(value)) return [];
|
|
82
|
+
return value.filter((item): item is string => typeof item === "string").map((item) => item.trim()).filter(Boolean);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function parseReviewPayload(text: string): Omit<IndividualReview, "memberId" | "memberName" | "perspective" | "durationMs" | "rawText"> {
|
|
86
|
+
const candidate = extractJsonCandidate(text);
|
|
87
|
+
if (!candidate) throw new ReviewParseError("No JSON object found in reviewer response");
|
|
88
|
+
|
|
89
|
+
let parsed: unknown;
|
|
90
|
+
try {
|
|
91
|
+
parsed = JSON.parse(candidate);
|
|
92
|
+
} catch {
|
|
93
|
+
throw new ReviewParseError("Reviewer response is not valid JSON");
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
97
|
+
throw new ReviewParseError("Reviewer response must be a JSON object");
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const row = parsed as Record<string, unknown>;
|
|
101
|
+
const verdict = row.verdict;
|
|
102
|
+
const summary = row.summary;
|
|
103
|
+
if (verdict !== "approve" && verdict !== "revise" && verdict !== "reject") {
|
|
104
|
+
throw new ReviewParseError("Reviewer verdict must be approve, revise, or reject");
|
|
105
|
+
}
|
|
106
|
+
if (typeof summary !== "string" || !summary.trim()) {
|
|
107
|
+
throw new ReviewParseError("Reviewer summary must be a non-empty string");
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return {
|
|
111
|
+
verdict,
|
|
112
|
+
summary: summary.trim(),
|
|
113
|
+
highlights: asStringArray(row.highlights),
|
|
114
|
+
lowlights: asStringArray(row.lowlights),
|
|
115
|
+
suggestions: asStringArray(row.suggestions),
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function getAssistantText(messages: AgentMessage[]): string {
|
|
120
|
+
const lastMessage = messages.filter((message) => message.role === "assistant").pop();
|
|
121
|
+
if (!lastMessage?.content) return "";
|
|
122
|
+
if (typeof lastMessage.content === "string") return lastMessage.content;
|
|
123
|
+
return lastMessage.content
|
|
124
|
+
.filter((chunk): chunk is { type: "text"; text: string } => chunk.type === "text")
|
|
125
|
+
.map((chunk) => chunk.text)
|
|
126
|
+
.join("");
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function toFailure(memberId: string, error: unknown): ReviewFailure {
|
|
130
|
+
if (error instanceof ReviewTimeoutError) {
|
|
131
|
+
return { memberId, reason: "timeout", message: error.message };
|
|
132
|
+
}
|
|
133
|
+
if (error instanceof ReviewParseError) {
|
|
134
|
+
return { memberId, reason: "parse_error", message: error.message };
|
|
135
|
+
}
|
|
136
|
+
if (error instanceof ReviewPanelError && error.reason === "session_unavailable") {
|
|
137
|
+
return { memberId, reason: "session_unavailable", message: error.message };
|
|
138
|
+
}
|
|
139
|
+
return {
|
|
140
|
+
memberId,
|
|
141
|
+
reason: "exception",
|
|
142
|
+
message: error instanceof Error ? error.message : String(error),
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function timeoutAfter(ms: number, memberName: string): Promise<never> {
|
|
147
|
+
return new Promise((_, reject) => {
|
|
148
|
+
globalThis.setTimeout(() => {
|
|
149
|
+
reject(new ReviewTimeoutError(`Review timed out for ${memberName} after ${ms}ms`));
|
|
150
|
+
}, ms);
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function dedupeMerged(items: string[]): string[] {
|
|
155
|
+
const seen = new Set<string>();
|
|
156
|
+
const merged: string[] = [];
|
|
157
|
+
for (const item of items) {
|
|
158
|
+
const trimmed = item.trim();
|
|
159
|
+
if (!trimmed) continue;
|
|
160
|
+
const key = trimmed.toLowerCase();
|
|
161
|
+
if (seen.has(key)) continue;
|
|
162
|
+
seen.add(key);
|
|
163
|
+
merged.push(trimmed);
|
|
164
|
+
if (merged.length >= MAX_MERGED_ITEMS) break;
|
|
165
|
+
}
|
|
166
|
+
return merged;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function getVerdictRank(verdict: ReviewVerdict): number {
|
|
170
|
+
if (verdict === "reject") return 2;
|
|
171
|
+
if (verdict === "revise") return 1;
|
|
172
|
+
return 0;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export function combineReviews(individual: IndividualReview[], failures: ReviewFailure[]): CombinedReview {
|
|
176
|
+
if (individual.length === 0) {
|
|
177
|
+
return {
|
|
178
|
+
overallVerdict: "reject",
|
|
179
|
+
consensusSummary: "Review panel could not produce feedback because all reviewers failed.",
|
|
180
|
+
mergedHighlights: [],
|
|
181
|
+
mergedLowlights: [],
|
|
182
|
+
mergedSuggestions: [],
|
|
183
|
+
individual,
|
|
184
|
+
failures,
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const worstVerdict = individual.reduce<ReviewVerdict>((current, review) =>
|
|
189
|
+
getVerdictRank(review.verdict) > getVerdictRank(current) ? review.verdict : current,
|
|
190
|
+
"approve");
|
|
191
|
+
|
|
192
|
+
const consensusSummary = individual
|
|
193
|
+
.map((review) => `${review.perspective}: ${review.summary}`)
|
|
194
|
+
.join(" | ");
|
|
195
|
+
|
|
196
|
+
return {
|
|
197
|
+
overallVerdict: worstVerdict,
|
|
198
|
+
consensusSummary,
|
|
199
|
+
mergedHighlights: dedupeMerged(individual.flatMap((review) => review.highlights)),
|
|
200
|
+
mergedLowlights: dedupeMerged(individual.flatMap((review) => review.lowlights)),
|
|
201
|
+
mergedSuggestions: dedupeMerged(individual.flatMap((review) => review.suggestions)),
|
|
202
|
+
individual,
|
|
203
|
+
failures,
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
async function runSingleReview(member: ReviewPanelMember, input: RunReviewPanelInput, createAiSession: CreateAiSessionFactory, settings: Record<string, unknown>): Promise<IndividualReview> {
|
|
208
|
+
const startedAt = Date.now();
|
|
209
|
+
const userPrompt = [
|
|
210
|
+
"Review the following generated report draft.",
|
|
211
|
+
"Return only strict JSON with keys: verdict, summary, highlights, lowlights, suggestions.",
|
|
212
|
+
"Do not include markdown fences or extra commentary.",
|
|
213
|
+
"",
|
|
214
|
+
`reportId: ${input.reportMetadata.reportId}`,
|
|
215
|
+
`cadence: ${input.reportMetadata.cadence}`,
|
|
216
|
+
`periodStart: ${input.reportMetadata.periodStart}`,
|
|
217
|
+
`periodEnd: ${input.reportMetadata.periodEnd}`,
|
|
218
|
+
"",
|
|
219
|
+
"reportDraft:",
|
|
220
|
+
input.reportDraft,
|
|
221
|
+
].join("\n");
|
|
222
|
+
|
|
223
|
+
const response = await Promise.race([
|
|
224
|
+
(async () => {
|
|
225
|
+
const agent = await createAiSession({
|
|
226
|
+
cwd: input.cwd,
|
|
227
|
+
systemPrompt: buildSystemPrompt(member, settings),
|
|
228
|
+
tools: "readonly",
|
|
229
|
+
...(member.provider && member.modelId ? { defaultProvider: member.provider, defaultModelId: member.modelId } : {}),
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
try {
|
|
233
|
+
await agent.session.prompt(userPrompt);
|
|
234
|
+
let text = getAssistantText(agent.session.state.messages as AgentMessage[]);
|
|
235
|
+
|
|
236
|
+
let parsed: Omit<IndividualReview, "memberId" | "memberName" | "perspective" | "durationMs" | "rawText"> | undefined;
|
|
237
|
+
let lastParseError: Error | undefined;
|
|
238
|
+
for (let attempt = 0; attempt <= MAX_PARSE_RETRIES; attempt++) {
|
|
239
|
+
try {
|
|
240
|
+
parsed = parseReviewPayload(text);
|
|
241
|
+
break;
|
|
242
|
+
} catch (error) {
|
|
243
|
+
lastParseError = error instanceof Error ? error : new Error(String(error));
|
|
244
|
+
if (attempt === MAX_PARSE_RETRIES) break;
|
|
245
|
+
await agent.session.prompt("Your previous response was not valid JSON. Respond with only a valid JSON object.");
|
|
246
|
+
text = getAssistantText(agent.session.state.messages as AgentMessage[]);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
if (!parsed) {
|
|
251
|
+
throw new ReviewParseError(`Failed to parse reviewer response: ${lastParseError?.message ?? "unknown error"}`);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
return {
|
|
255
|
+
memberId: member.id,
|
|
256
|
+
memberName: member.name,
|
|
257
|
+
perspective: member.perspective,
|
|
258
|
+
rawText: text,
|
|
259
|
+
durationMs: Date.now() - startedAt,
|
|
260
|
+
...parsed,
|
|
261
|
+
};
|
|
262
|
+
} finally {
|
|
263
|
+
(agent.session as { dispose?: () => void }).dispose?.();
|
|
264
|
+
}
|
|
265
|
+
})(),
|
|
266
|
+
timeoutAfter(REVIEW_TIMEOUT_MS, member.name),
|
|
267
|
+
]);
|
|
268
|
+
|
|
269
|
+
return response;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
export async function runReviewPanel(input: RunReviewPanelInput, ctx: PluginContext): Promise<CombinedReview> {
|
|
273
|
+
const createAiSession = pickFactory(ctx);
|
|
274
|
+
if (!createAiSession) {
|
|
275
|
+
throw new ReviewPanelError("session_unavailable", "AI session factory is unavailable");
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const settled = await Promise.allSettled(input.panel.map((member) => runSingleReview(member, input, createAiSession, ctx.settings)));
|
|
279
|
+
|
|
280
|
+
const individual: IndividualReview[] = [];
|
|
281
|
+
const failures: ReviewFailure[] = [];
|
|
282
|
+
for (let index = 0; index < settled.length; index++) {
|
|
283
|
+
const result = settled[index];
|
|
284
|
+
const member = input.panel[index];
|
|
285
|
+
if (result.status === "fulfilled") individual.push(result.value);
|
|
286
|
+
else failures.push(toFailure(member.id, result.reason));
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
return combineReviews(individual, failures);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
export function __setCreateAiSessionFactory(factory: CreateAiSessionFactory | undefined): void {
|
|
293
|
+
injectedCreateAiSession = factory;
|
|
294
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
export type ReviewVerdict = "approve" | "revise" | "reject";
|
|
2
|
+
|
|
3
|
+
export interface ReviewPanelMember {
|
|
4
|
+
id: string;
|
|
5
|
+
name: string;
|
|
6
|
+
perspective: string;
|
|
7
|
+
promptTemplateId?: string;
|
|
8
|
+
provider?: string;
|
|
9
|
+
modelId?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface IndividualReview {
|
|
13
|
+
memberId: string;
|
|
14
|
+
memberName: string;
|
|
15
|
+
perspective: string;
|
|
16
|
+
verdict: ReviewVerdict;
|
|
17
|
+
summary: string;
|
|
18
|
+
highlights: string[];
|
|
19
|
+
lowlights: string[];
|
|
20
|
+
suggestions: string[];
|
|
21
|
+
rawText: string;
|
|
22
|
+
durationMs: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface ReviewFailure {
|
|
26
|
+
memberId: string;
|
|
27
|
+
reason: "timeout" | "parse_error" | "session_unavailable" | "exception";
|
|
28
|
+
message: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface CombinedReview {
|
|
32
|
+
overallVerdict: ReviewVerdict;
|
|
33
|
+
consensusSummary: string;
|
|
34
|
+
mergedHighlights: string[];
|
|
35
|
+
mergedLowlights: string[];
|
|
36
|
+
mergedSuggestions: string[];
|
|
37
|
+
individual: IndividualReview[];
|
|
38
|
+
failures: ReviewFailure[];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface RunReviewPanelInput {
|
|
42
|
+
reportDraft: string;
|
|
43
|
+
reportMetadata: {
|
|
44
|
+
reportId: string;
|
|
45
|
+
cadence: "daily" | "weekly";
|
|
46
|
+
periodStart: string;
|
|
47
|
+
periodEnd: string;
|
|
48
|
+
};
|
|
49
|
+
panel: ReviewPanelMember[];
|
|
50
|
+
cwd: string;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export class ReviewPanelError extends Error {
|
|
54
|
+
constructor(
|
|
55
|
+
public readonly reason: "session_unavailable" | "exception",
|
|
56
|
+
message: string,
|
|
57
|
+
) {
|
|
58
|
+
super(message);
|
|
59
|
+
this.name = "ReviewPanelError";
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export class ReviewParseError extends Error {
|
|
64
|
+
constructor(message: string) {
|
|
65
|
+
super(message);
|
|
66
|
+
this.name = "ReviewParseError";
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export class ReviewTimeoutError extends Error {
|
|
71
|
+
constructor(message: string) {
|
|
72
|
+
super(message);
|
|
73
|
+
this.name = "ReviewTimeoutError";
|
|
74
|
+
}
|
|
75
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import type { PluginSettingSchema } from "@fusion/plugin-sdk";
|
|
2
|
+
|
|
3
|
+
export const DEFAULT_DAILY_ENABLED = true;
|
|
4
|
+
export const DEFAULT_DAILY_CRON = "0 8 * * *";
|
|
5
|
+
export const DEFAULT_WEEKLY_ENABLED = true;
|
|
6
|
+
export const DEFAULT_WEEKLY_CRON = "0 8 * * 1";
|
|
7
|
+
export const DEFAULT_TIMEZONE = "UTC";
|
|
8
|
+
export const DEFAULT_GENERATION_PROMPT_DAILY = "Summarize today's system activity with clear wins, highlights, lowlights, and proposals.";
|
|
9
|
+
export const DEFAULT_GENERATION_PROMPT_WEEKLY = "Summarize this week's system activity trends, major outcomes, and actionable proposals.";
|
|
10
|
+
export const DEFAULT_REVIEW_PROMPT = "Review this report for factual accuracy, clarity, and actionability. Provide concrete revision suggestions.";
|
|
11
|
+
export const DEFAULT_REVIEW_MIN_APPROVALS = 1;
|
|
12
|
+
export const DEFAULT_REVIEW_BLOCKING_MODE = "advisory";
|
|
13
|
+
export const DEFAULT_SECTION_ORDER = ["wins", "highlights", "lowlights", "proposals", "deep-dives", "per-agent"];
|
|
14
|
+
export const DEFAULT_ENABLED_SECTIONS = [...DEFAULT_SECTION_ORDER];
|
|
15
|
+
export const DEFAULT_INCLUDE_SYSTEM_SUMMARY = true;
|
|
16
|
+
export const DEFAULT_INCLUDE_PER_AGENT_SECTIONS = true;
|
|
17
|
+
export const DEFAULT_BRAND_TITLE = "Fusion Activity Report";
|
|
18
|
+
export const DEFAULT_THEME_MODE = "auto";
|
|
19
|
+
export const DEFAULT_ACCENT_COLOR = "#5B8DEF";
|
|
20
|
+
export const DEFAULT_APPROVAL_REQUIRED = false;
|
|
21
|
+
export const DEFAULT_AUTO_PUBLISH_ON_APPROVAL = true;
|
|
22
|
+
export const DEFAULT_PUBLISH_TARGETS: string[] = [];
|
|
23
|
+
|
|
24
|
+
export const settingsSchema: Record<string, PluginSettingSchema> = {
|
|
25
|
+
dailyEnabled: { type: "boolean", label: "Enable Daily Reports", description: "Generate daily reports on schedule.", group: "Schedules", defaultValue: DEFAULT_DAILY_ENABLED },
|
|
26
|
+
dailyCron: { type: "string", label: "Daily Schedule (cron)", description: "Cron expression for daily report generation.", group: "Schedules", required: true, defaultValue: DEFAULT_DAILY_CRON },
|
|
27
|
+
weeklyEnabled: { type: "boolean", label: "Enable Weekly Reports", description: "Generate weekly reports on schedule.", group: "Schedules", defaultValue: DEFAULT_WEEKLY_ENABLED },
|
|
28
|
+
weeklyCron: { type: "string", label: "Weekly Schedule (cron)", description: "Cron expression for weekly report generation.", group: "Schedules", required: true, defaultValue: DEFAULT_WEEKLY_CRON },
|
|
29
|
+
timezone: { type: "string", label: "Timezone", description: "IANA timezone used for schedule evaluation.", group: "Schedules", required: true, defaultValue: DEFAULT_TIMEZONE },
|
|
30
|
+
|
|
31
|
+
generationPromptDaily: { type: "string", multiline: true, label: "Daily Generation Prompt", description: "Prompt template used to generate daily reports.", group: "Prompts", required: true, defaultValue: DEFAULT_GENERATION_PROMPT_DAILY },
|
|
32
|
+
generationPromptWeekly: { type: "string", multiline: true, label: "Weekly Generation Prompt", description: "Prompt template used to generate weekly reports.", group: "Prompts", required: true, defaultValue: DEFAULT_GENERATION_PROMPT_WEEKLY },
|
|
33
|
+
reviewPrompt: { type: "string", multiline: true, label: "Review Prompt", description: "Prompt template used by reviewer agents.", group: "Prompts", required: true, defaultValue: DEFAULT_REVIEW_PROMPT },
|
|
34
|
+
|
|
35
|
+
reviewPanelAgentIds: { type: "array", itemType: "string", label: "Reviewer Agent IDs", description: "Agent IDs allowed to review generated reports.", group: "Review Panel", defaultValue: [] },
|
|
36
|
+
reviewMinApprovals: { type: "number", label: "Minimum Approvals", description: "How many approvals are required from the review panel.", group: "Review Panel", defaultValue: DEFAULT_REVIEW_MIN_APPROVALS },
|
|
37
|
+
reviewBlockingMode: { type: "enum", enumValues: ["block", "advisory"], label: "Review Blocking Mode", description: "Choose whether failed review blocks publication.", group: "Review Panel", defaultValue: DEFAULT_REVIEW_BLOCKING_MODE },
|
|
38
|
+
|
|
39
|
+
sectionOrder: { type: "array", itemType: "string", label: "Section Order", description: "Ordered section IDs for report rendering.", group: "Sections", defaultValue: DEFAULT_SECTION_ORDER },
|
|
40
|
+
enabledSections: { type: "array", itemType: "string", label: "Enabled Sections", description: "Section IDs enabled for output.", group: "Sections", defaultValue: DEFAULT_ENABLED_SECTIONS },
|
|
41
|
+
includeSystemSummary: { type: "boolean", label: "Include System Summary", description: "Include top-level system summary in report output.", group: "Sections", defaultValue: DEFAULT_INCLUDE_SYSTEM_SUMMARY },
|
|
42
|
+
includePerAgentSections: { type: "boolean", label: "Include Per-Agent Sections", description: "Include per-agent detail sections.", group: "Sections", defaultValue: DEFAULT_INCLUDE_PER_AGENT_SECTIONS },
|
|
43
|
+
|
|
44
|
+
brandTitle: { type: "string", label: "Report Title", description: "Primary report title for branding.", group: "Branding", required: true, defaultValue: DEFAULT_BRAND_TITLE },
|
|
45
|
+
brandLogoUrl: { type: "string", label: "Logo URL", description: "Optional URL to a brand logo image.", group: "Branding" },
|
|
46
|
+
themeMode: { type: "enum", enumValues: ["light", "dark", "auto"], label: "Theme Mode", description: "Theme style for generated reports.", group: "Branding", defaultValue: DEFAULT_THEME_MODE },
|
|
47
|
+
accentColor: { type: "string", label: "Accent Color", description: "Accent color used in report styling.", group: "Branding", defaultValue: DEFAULT_ACCENT_COLOR },
|
|
48
|
+
|
|
49
|
+
approvalRequired: { type: "boolean", label: "Require Approval", description: "Require explicit approval before publishing.", group: "Approval Flow", defaultValue: DEFAULT_APPROVAL_REQUIRED },
|
|
50
|
+
autoPublishOnApproval: { type: "boolean", label: "Auto Publish on Approval", description: "Publish automatically after approval threshold is met.", group: "Approval Flow", defaultValue: DEFAULT_AUTO_PUBLISH_ON_APPROVAL },
|
|
51
|
+
approverAgentIds: { type: "array", itemType: "string", label: "Approver Agent IDs", description: "Agent IDs permitted to approve final publish.", group: "Approval Flow", defaultValue: [] },
|
|
52
|
+
publishTargets: { type: "array", itemType: "string", label: "Publish Targets", description: "Destinations to publish reports to (for example dashboard, html-export).", group: "Approval Flow", defaultValue: DEFAULT_PUBLISH_TARGETS },
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
function asString(settings: Record<string, unknown>, key: string): string | undefined {
|
|
56
|
+
const value = settings[key];
|
|
57
|
+
return typeof value === "string" && value.trim().length > 0 ? value.trim() : undefined;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function asBoolean(settings: Record<string, unknown>, key: string, fallback: boolean): boolean {
|
|
61
|
+
const value = settings[key];
|
|
62
|
+
return typeof value === "boolean" ? value : fallback;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function asNumber(settings: Record<string, unknown>, key: string, fallback: number): number {
|
|
66
|
+
const value = settings[key];
|
|
67
|
+
return typeof value === "number" && Number.isFinite(value) ? value : fallback;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function asStringArray(settings: Record<string, unknown>, key: string, fallback: string[]): string[] {
|
|
71
|
+
const value = settings[key];
|
|
72
|
+
if (!Array.isArray(value)) return [...fallback];
|
|
73
|
+
const normalized = value.filter((item): item is string => typeof item === "string").map((item) => item.trim()).filter(Boolean);
|
|
74
|
+
return normalized.length > 0 ? normalized : [...fallback];
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function getDailyEnabled(settings: Record<string, unknown>): boolean { return asBoolean(settings, "dailyEnabled", DEFAULT_DAILY_ENABLED); }
|
|
78
|
+
export function getDailyCron(settings: Record<string, unknown>): string { return asString(settings, "dailyCron") ?? DEFAULT_DAILY_CRON; }
|
|
79
|
+
export function getWeeklyEnabled(settings: Record<string, unknown>): boolean { return asBoolean(settings, "weeklyEnabled", DEFAULT_WEEKLY_ENABLED); }
|
|
80
|
+
export function getWeeklyCron(settings: Record<string, unknown>): string { return asString(settings, "weeklyCron") ?? DEFAULT_WEEKLY_CRON; }
|
|
81
|
+
export function getTimezone(settings: Record<string, unknown>): string { return asString(settings, "timezone") ?? DEFAULT_TIMEZONE; }
|
|
82
|
+
export function getGenerationPromptDaily(settings: Record<string, unknown>): string { return asString(settings, "generationPromptDaily") ?? DEFAULT_GENERATION_PROMPT_DAILY; }
|
|
83
|
+
export function getGenerationPromptWeekly(settings: Record<string, unknown>): string { return asString(settings, "generationPromptWeekly") ?? DEFAULT_GENERATION_PROMPT_WEEKLY; }
|
|
84
|
+
export function getReviewPrompt(settings: Record<string, unknown>): string { return asString(settings, "reviewPrompt") ?? DEFAULT_REVIEW_PROMPT; }
|
|
85
|
+
export function getReviewPanelAgentIds(settings: Record<string, unknown>): string[] { return asStringArray(settings, "reviewPanelAgentIds", []); }
|
|
86
|
+
export function getReviewMinApprovals(settings: Record<string, unknown>): number { return Math.max(1, Math.floor(asNumber(settings, "reviewMinApprovals", DEFAULT_REVIEW_MIN_APPROVALS))); }
|
|
87
|
+
export function getReviewBlockingMode(settings: Record<string, unknown>): "block" | "advisory" {
|
|
88
|
+
const value = asString(settings, "reviewBlockingMode");
|
|
89
|
+
return value === "block" ? "block" : "advisory";
|
|
90
|
+
}
|
|
91
|
+
export function getSectionOrder(settings: Record<string, unknown>): string[] { return asStringArray(settings, "sectionOrder", DEFAULT_SECTION_ORDER); }
|
|
92
|
+
export function getEnabledSections(settings: Record<string, unknown>): string[] { return asStringArray(settings, "enabledSections", DEFAULT_ENABLED_SECTIONS); }
|
|
93
|
+
export function getIncludeSystemSummary(settings: Record<string, unknown>): boolean { return asBoolean(settings, "includeSystemSummary", DEFAULT_INCLUDE_SYSTEM_SUMMARY); }
|
|
94
|
+
export function getIncludePerAgentSections(settings: Record<string, unknown>): boolean { return asBoolean(settings, "includePerAgentSections", DEFAULT_INCLUDE_PER_AGENT_SECTIONS); }
|
|
95
|
+
export function getBrandTitle(settings: Record<string, unknown>): string { return asString(settings, "brandTitle") ?? DEFAULT_BRAND_TITLE; }
|
|
96
|
+
export function getBrandLogoUrl(settings: Record<string, unknown>): string | undefined { return asString(settings, "brandLogoUrl"); }
|
|
97
|
+
export function getThemeMode(settings: Record<string, unknown>): "light" | "dark" | "auto" {
|
|
98
|
+
const value = asString(settings, "themeMode");
|
|
99
|
+
return value === "light" || value === "dark" ? value : "auto";
|
|
100
|
+
}
|
|
101
|
+
export function getAccentColor(settings: Record<string, unknown>): string { return asString(settings, "accentColor") ?? DEFAULT_ACCENT_COLOR; }
|
|
102
|
+
export function getApprovalRequired(settings: Record<string, unknown>): boolean { return asBoolean(settings, "approvalRequired", DEFAULT_APPROVAL_REQUIRED); }
|
|
103
|
+
export function getAutoPublishOnApproval(settings: Record<string, unknown>): boolean { return asBoolean(settings, "autoPublishOnApproval", DEFAULT_AUTO_PUBLISH_ON_APPROVAL); }
|
|
104
|
+
export function getApproverAgentIds(settings: Record<string, unknown>): string[] { return asStringArray(settings, "approverAgentIds", []); }
|
|
105
|
+
export function getPublishTargets(settings: Record<string, unknown>): string[] { return asStringArray(settings, "publishTargets", DEFAULT_PUBLISH_TARGETS); }
|
|
@@ -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
|
+
});
|