chainlesschain 0.37.10 → 0.37.12

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 (39) hide show
  1. package/README.md +166 -10
  2. package/package.json +1 -1
  3. package/src/commands/a2a.js +374 -0
  4. package/src/commands/bi.js +240 -0
  5. package/src/commands/cowork.js +317 -0
  6. package/src/commands/economy.js +375 -0
  7. package/src/commands/evolution.js +398 -0
  8. package/src/commands/hmemory.js +273 -0
  9. package/src/commands/hook.js +260 -0
  10. package/src/commands/init.js +184 -0
  11. package/src/commands/lowcode.js +320 -0
  12. package/src/commands/plugin.js +55 -2
  13. package/src/commands/sandbox.js +366 -0
  14. package/src/commands/skill.js +254 -201
  15. package/src/commands/workflow.js +359 -0
  16. package/src/commands/zkp.js +277 -0
  17. package/src/index.js +44 -0
  18. package/src/lib/a2a-protocol.js +371 -0
  19. package/src/lib/agent-coordinator.js +273 -0
  20. package/src/lib/agent-economy.js +369 -0
  21. package/src/lib/app-builder.js +377 -0
  22. package/src/lib/bi-engine.js +299 -0
  23. package/src/lib/cowork/ab-comparator-cli.js +180 -0
  24. package/src/lib/cowork/code-knowledge-graph-cli.js +232 -0
  25. package/src/lib/cowork/debate-review-cli.js +144 -0
  26. package/src/lib/cowork/decision-kb-cli.js +153 -0
  27. package/src/lib/cowork/project-style-analyzer-cli.js +168 -0
  28. package/src/lib/cowork-adapter.js +106 -0
  29. package/src/lib/evolution-system.js +508 -0
  30. package/src/lib/hierarchical-memory.js +471 -0
  31. package/src/lib/hook-manager.js +387 -0
  32. package/src/lib/plugin-manager.js +118 -0
  33. package/src/lib/project-detector.js +53 -0
  34. package/src/lib/sandbox-v2.js +503 -0
  35. package/src/lib/service-container.js +183 -0
  36. package/src/lib/skill-loader.js +274 -0
  37. package/src/lib/workflow-engine.js +503 -0
  38. package/src/lib/zkp-engine.js +241 -0
  39. package/src/repl/agent-repl.js +117 -112
@@ -0,0 +1,377 @@
1
+ /**
2
+ * Low-Code App Builder — create, design, preview, publish, and manage
3
+ * low-code applications with built-in components and data sources.
4
+ */
5
+
6
+ import crypto from "crypto";
7
+
8
+ /** @type {Map<string, object>} In-memory app cache */
9
+ const _apps = new Map();
10
+
11
+ /** @type {Map<string, object>} In-memory data sources */
12
+ const _dataSources = new Map();
13
+
14
+ /** @type {Map<string, object[]>} In-memory version history */
15
+ const _versions = new Map();
16
+
17
+ /** @type {object[]|null} Cached built-in component list */
18
+ let _components = null;
19
+
20
+ /**
21
+ * Return the 15 built-in components, initializing on first call.
22
+ *
23
+ * @returns {object[]}
24
+ */
25
+ export function listComponents() {
26
+ if (!_components) {
27
+ _components = [
28
+ {
29
+ name: "Form",
30
+ category: "input",
31
+ props: ["fields", "onSubmit", "validation"],
32
+ },
33
+ {
34
+ name: "DataTable",
35
+ category: "display",
36
+ props: ["columns", "data", "pagination", "sortable"],
37
+ },
38
+ {
39
+ name: "BarChart",
40
+ category: "chart",
41
+ props: ["data", "xAxis", "yAxis", "colors"],
42
+ },
43
+ {
44
+ name: "LineChart",
45
+ category: "chart",
46
+ props: ["data", "xAxis", "yAxis", "smooth"],
47
+ },
48
+ {
49
+ name: "PieChart",
50
+ category: "chart",
51
+ props: ["data", "labels", "colors"],
52
+ },
53
+ {
54
+ name: "Dashboard",
55
+ category: "layout",
56
+ props: ["widgets", "layout", "refreshInterval"],
57
+ },
58
+ {
59
+ name: "Button",
60
+ category: "input",
61
+ props: ["label", "onClick", "variant", "disabled"],
62
+ },
63
+ {
64
+ name: "TextInput",
65
+ category: "input",
66
+ props: ["placeholder", "value", "onChange", "type"],
67
+ },
68
+ {
69
+ name: "Select",
70
+ category: "input",
71
+ props: ["options", "value", "onChange", "multiple"],
72
+ },
73
+ {
74
+ name: "Modal",
75
+ category: "overlay",
76
+ props: ["visible", "title", "onClose", "width"],
77
+ },
78
+ {
79
+ name: "Card",
80
+ category: "layout",
81
+ props: ["title", "content", "footer"],
82
+ },
83
+ {
84
+ name: "List",
85
+ category: "display",
86
+ props: ["items", "renderItem", "pagination"],
87
+ },
88
+ {
89
+ name: "Image",
90
+ category: "display",
91
+ props: ["src", "alt", "width", "height"],
92
+ },
93
+ {
94
+ name: "Tabs",
95
+ category: "layout",
96
+ props: ["tabs", "activeKey", "onChange"],
97
+ },
98
+ {
99
+ name: "Calendar",
100
+ category: "display",
101
+ props: ["events", "view", "onSelect"],
102
+ },
103
+ ];
104
+ }
105
+ return _components;
106
+ }
107
+
108
+ /**
109
+ * Ensure low-code tables exist in the database.
110
+ *
111
+ * @param {object} db
112
+ */
113
+ export function ensureLowcodeTables(db) {
114
+ db.exec(`
115
+ CREATE TABLE IF NOT EXISTS lowcode_apps (
116
+ id TEXT PRIMARY KEY,
117
+ name TEXT NOT NULL,
118
+ description TEXT,
119
+ design TEXT,
120
+ status TEXT DEFAULT 'draft',
121
+ version INTEGER DEFAULT 1,
122
+ platform TEXT DEFAULT 'web',
123
+ created_at TEXT DEFAULT (datetime('now')),
124
+ updated_at TEXT DEFAULT (datetime('now'))
125
+ )
126
+ `);
127
+ db.exec(`
128
+ CREATE TABLE IF NOT EXISTS lowcode_datasources (
129
+ id TEXT PRIMARY KEY,
130
+ app_id TEXT NOT NULL,
131
+ name TEXT NOT NULL,
132
+ type TEXT NOT NULL,
133
+ config TEXT,
134
+ status TEXT DEFAULT 'active',
135
+ created_at TEXT DEFAULT (datetime('now'))
136
+ )
137
+ `);
138
+ db.exec(`
139
+ CREATE TABLE IF NOT EXISTS lowcode_versions (
140
+ id TEXT PRIMARY KEY,
141
+ app_id TEXT NOT NULL,
142
+ version INTEGER NOT NULL,
143
+ snapshot TEXT,
144
+ created_at TEXT DEFAULT (datetime('now'))
145
+ )
146
+ `);
147
+ }
148
+
149
+ /**
150
+ * Create a new low-code application.
151
+ *
152
+ * @param {object} db
153
+ * @param {{ name: string, description?: string, platform?: string, design?: object }} definition
154
+ * @returns {{ id: string, name: string, status: string }}
155
+ */
156
+ export function createApp(db, definition) {
157
+ const id = crypto.randomUUID().slice(0, 12);
158
+ const name = definition.name || "Untitled App";
159
+ const description = definition.description || "";
160
+ const platform = definition.platform || "web";
161
+ const design = definition.design || { components: [], layout: {} };
162
+
163
+ const stmt = db.prepare(
164
+ `INSERT INTO lowcode_apps (id, name, description, design, status, version, platform)
165
+ VALUES (?, ?, ?, ?, ?, ?, ?)`,
166
+ );
167
+ stmt.run(id, name, description, JSON.stringify(design), "draft", 1, platform);
168
+
169
+ const app = {
170
+ id,
171
+ name,
172
+ description,
173
+ platform,
174
+ design,
175
+ status: "draft",
176
+ version: 1,
177
+ };
178
+ _apps.set(id, app);
179
+
180
+ // Create initial version snapshot
181
+ const versionId = crypto.randomUUID().slice(0, 12);
182
+ const vStmt = db.prepare(
183
+ `INSERT INTO lowcode_versions (id, app_id, version, snapshot)
184
+ VALUES (?, ?, ?, ?)`,
185
+ );
186
+ vStmt.run(versionId, id, 1, JSON.stringify(design));
187
+
188
+ if (!_versions.has(id)) _versions.set(id, []);
189
+ _versions
190
+ .get(id)
191
+ .push({ id: versionId, app_id: id, version: 1, snapshot: design });
192
+
193
+ return { id, name, status: "draft" };
194
+ }
195
+
196
+ /**
197
+ * Save a new design for an app, bump the version, and create a snapshot.
198
+ *
199
+ * @param {object} db
200
+ * @param {string} appId
201
+ * @param {object} design
202
+ * @returns {{ appId: string, version: number }}
203
+ */
204
+ export function saveDesign(db, appId, design) {
205
+ // Get current version
206
+ const row = db
207
+ .prepare(`SELECT version FROM lowcode_apps WHERE id = ?`)
208
+ .get(appId);
209
+ const currentVersion = row ? row.version || 1 : 1;
210
+ const newVersion = currentVersion + 1;
211
+
212
+ db.prepare(
213
+ `UPDATE lowcode_apps SET design = ?, version = ?, updated_at = datetime('now') WHERE id = ?`,
214
+ ).run(JSON.stringify(design), newVersion, appId);
215
+
216
+ // Create version snapshot
217
+ const versionId = crypto.randomUUID().slice(0, 12);
218
+ db.prepare(
219
+ `INSERT INTO lowcode_versions (id, app_id, version, snapshot) VALUES (?, ?, ?, ?)`,
220
+ ).run(versionId, appId, newVersion, JSON.stringify(design));
221
+
222
+ if (!_versions.has(appId)) _versions.set(appId, []);
223
+ _versions
224
+ .get(appId)
225
+ .push({
226
+ id: versionId,
227
+ app_id: appId,
228
+ version: newVersion,
229
+ snapshot: design,
230
+ });
231
+
232
+ if (_apps.has(appId)) {
233
+ _apps.get(appId).design = design;
234
+ _apps.get(appId).version = newVersion;
235
+ }
236
+
237
+ return { appId, version: newVersion };
238
+ }
239
+
240
+ /**
241
+ * Get preview info for an application.
242
+ *
243
+ * @param {string} appId
244
+ * @returns {{ appId: string, design: object, previewUrl: string, platform: string }}
245
+ */
246
+ export function previewApp(appId) {
247
+ const app = _apps.get(appId);
248
+ const design = app ? app.design : { components: [], layout: {} };
249
+ const platform = app ? app.platform : "web";
250
+
251
+ return {
252
+ appId,
253
+ design,
254
+ previewUrl: `http://localhost:5173/lowcode/preview/${appId}`,
255
+ platform,
256
+ };
257
+ }
258
+
259
+ /**
260
+ * Publish an application (set status to 'published').
261
+ *
262
+ * @param {object} db
263
+ * @param {string} appId
264
+ * @returns {{ appId: string, status: string }}
265
+ */
266
+ export function publishApp(db, appId) {
267
+ db.prepare(
268
+ `UPDATE lowcode_apps SET status = ?, updated_at = datetime('now') WHERE id = ?`,
269
+ ).run("published", appId);
270
+
271
+ if (_apps.has(appId)) {
272
+ _apps.get(appId).status = "published";
273
+ }
274
+
275
+ return { appId, status: "published" };
276
+ }
277
+
278
+ /**
279
+ * Add a data source to an application.
280
+ *
281
+ * @param {object} db
282
+ * @param {string} appId
283
+ * @param {string} name
284
+ * @param {string} type - e.g. "rest", "graphql", "database", "csv"
285
+ * @param {object} config
286
+ * @returns {{ id: string, appId: string, name: string, type: string }}
287
+ */
288
+ export function addDataSource(db, appId, name, type, config) {
289
+ const id = crypto.randomUUID().slice(0, 12);
290
+ const stmt = db.prepare(
291
+ `INSERT INTO lowcode_datasources (id, app_id, name, type, config, status)
292
+ VALUES (?, ?, ?, ?, ?, ?)`,
293
+ );
294
+ stmt.run(id, appId, name, type, JSON.stringify(config || {}), "active");
295
+
296
+ _dataSources.set(id, { id, appId, name, type, config, status: "active" });
297
+
298
+ return { id, appId, name, type };
299
+ }
300
+
301
+ /**
302
+ * Get version history for an application.
303
+ *
304
+ * @param {string} appId
305
+ * @returns {object[]}
306
+ */
307
+ export function getVersions(appId) {
308
+ return _versions.get(appId) || [];
309
+ }
310
+
311
+ /**
312
+ * Rollback an application to a previous version.
313
+ *
314
+ * @param {object} db
315
+ * @param {string} appId
316
+ * @param {number} version
317
+ * @returns {{ appId: string, version: number, restored: boolean }}
318
+ */
319
+ export function rollbackApp(db, appId, version) {
320
+ const versions = _versions.get(appId) || [];
321
+ const target = versions.find((v) => v.version === version);
322
+
323
+ if (!target) {
324
+ return { appId, version, restored: false };
325
+ }
326
+
327
+ const design = target.snapshot;
328
+ db.prepare(
329
+ `UPDATE lowcode_apps SET design = ?, version = ?, updated_at = datetime('now') WHERE id = ?`,
330
+ ).run(JSON.stringify(design), version, appId);
331
+
332
+ if (_apps.has(appId)) {
333
+ _apps.get(appId).design = design;
334
+ _apps.get(appId).version = version;
335
+ }
336
+
337
+ return { appId, version, restored: true };
338
+ }
339
+
340
+ /**
341
+ * Export an app definition with data sources.
342
+ *
343
+ * @param {string} appId
344
+ * @returns {{ appId: string, app: object|null, dataSources: object[], versions: object[] }}
345
+ */
346
+ export function exportApp(appId) {
347
+ const app = _apps.get(appId) || null;
348
+ const dataSources = [];
349
+ for (const [, ds] of _dataSources) {
350
+ if (ds.appId === appId) dataSources.push(ds);
351
+ }
352
+ const versions = _versions.get(appId) || [];
353
+
354
+ return { appId, app, dataSources, versions };
355
+ }
356
+
357
+ /**
358
+ * List all applications from the database.
359
+ *
360
+ * @param {object} db
361
+ * @returns {object[]}
362
+ */
363
+ export function listApps(db) {
364
+ const rows = db
365
+ .prepare(`SELECT * FROM lowcode_apps ORDER BY updated_at DESC`)
366
+ .all();
367
+ return rows.map((r) => ({
368
+ id: r.id,
369
+ name: r.name,
370
+ description: r.description,
371
+ status: r.status,
372
+ version: r.version,
373
+ platform: r.platform,
374
+ created_at: r.created_at,
375
+ updated_at: r.updated_at,
376
+ }));
377
+ }
@@ -0,0 +1,299 @@
1
+ /**
2
+ * BI Engine — Business intelligence with NL→SQL queries, dashboards,
3
+ * reports, anomaly detection, trend prediction, and scheduling.
4
+ */
5
+
6
+ import crypto from "crypto";
7
+
8
+ /* ── In-memory stores ──────────────────────────────────────── */
9
+ const _dashboards = new Map();
10
+ const _reports = new Map();
11
+ const _scheduledReports = new Map();
12
+
13
+ const _templates = [
14
+ {
15
+ id: "tpl-kpi",
16
+ name: "KPI Dashboard",
17
+ description: "Key performance indicators overview",
18
+ widgets: ["metric-card", "sparkline", "gauge"],
19
+ },
20
+ {
21
+ id: "tpl-sales",
22
+ name: "Sales Report",
23
+ description: "Sales pipeline and revenue analysis",
24
+ widgets: ["bar-chart", "funnel", "table"],
25
+ },
26
+ {
27
+ id: "tpl-ops",
28
+ name: "Operations Dashboard",
29
+ description: "System health and operational metrics",
30
+ widgets: ["heatmap", "timeline", "alert-list"],
31
+ },
32
+ {
33
+ id: "tpl-hr",
34
+ name: "HR Analytics",
35
+ description: "Workforce analytics and headcount",
36
+ widgets: ["pie-chart", "trend-line", "scorecard"],
37
+ },
38
+ {
39
+ id: "tpl-finance",
40
+ name: "Financial Overview",
41
+ description: "Revenue, expenses, and cash flow",
42
+ widgets: ["waterfall", "stacked-bar", "summary-table"],
43
+ },
44
+ ];
45
+
46
+ /* ── Schema ────────────────────────────────────────────────── */
47
+
48
+ export function ensureBITables(db) {
49
+ db.exec(`
50
+ CREATE TABLE IF NOT EXISTS bi_dashboards (
51
+ id TEXT PRIMARY KEY,
52
+ name TEXT NOT NULL,
53
+ widgets TEXT,
54
+ layout TEXT,
55
+ created_at TEXT DEFAULT (datetime('now')),
56
+ updated_at TEXT DEFAULT (datetime('now'))
57
+ )
58
+ `);
59
+ db.exec(`
60
+ CREATE TABLE IF NOT EXISTS bi_reports (
61
+ id TEXT PRIMARY KEY,
62
+ name TEXT NOT NULL,
63
+ query TEXT,
64
+ result TEXT,
65
+ format TEXT DEFAULT 'pdf',
66
+ created_at TEXT DEFAULT (datetime('now'))
67
+ )
68
+ `);
69
+ db.exec(`
70
+ CREATE TABLE IF NOT EXISTS bi_scheduled (
71
+ id TEXT PRIMARY KEY,
72
+ report_id TEXT NOT NULL,
73
+ cron TEXT NOT NULL,
74
+ recipients TEXT,
75
+ last_run TEXT,
76
+ status TEXT DEFAULT 'active'
77
+ )
78
+ `);
79
+ }
80
+
81
+ /* ── NL → SQL Query ────────────────────────────────────────── */
82
+
83
+ export function nlQuery(query) {
84
+ if (!query || typeof query !== "string") {
85
+ throw new Error("Query must be a non-empty string");
86
+ }
87
+
88
+ // Mock NL→SQL translation: generate a SELECT LIKE query
89
+ const sanitized = query.replace(/['"]/g, "").trim();
90
+ const words = sanitized.split(/\s+/).slice(0, 3).join("_").toLowerCase();
91
+ const generatedSQL = `SELECT * FROM data WHERE content LIKE '%${words}%'`;
92
+
93
+ const id = crypto.randomUUID();
94
+ const results = [];
95
+ const visualization = {
96
+ type: "table",
97
+ title: query,
98
+ columns: ["id", "content", "value"],
99
+ };
100
+
101
+ return {
102
+ id,
103
+ query,
104
+ generatedSQL,
105
+ results,
106
+ rowCount: results.length,
107
+ visualization,
108
+ };
109
+ }
110
+
111
+ /* ── Reports ───────────────────────────────────────────────── */
112
+
113
+ export function generateReport(db, name, options) {
114
+ const id = crypto.randomUUID();
115
+ const now = new Date().toISOString();
116
+ const format = (options && options.format) || "pdf";
117
+
118
+ const sectionNames = (options && options.sections) || [
119
+ "summary",
120
+ "details",
121
+ "conclusion",
122
+ ];
123
+ const sections = sectionNames.map((s) => ({
124
+ name: s,
125
+ content: `Auto-generated ${s} section`,
126
+ generatedAt: now,
127
+ }));
128
+
129
+ const report = {
130
+ id,
131
+ name,
132
+ format,
133
+ sections,
134
+ generatedAt: now,
135
+ };
136
+
137
+ _reports.set(id, report);
138
+
139
+ db.prepare(
140
+ `INSERT INTO bi_reports (id, name, query, result, format, created_at)
141
+ VALUES (?, ?, ?, ?, ?, ?)`,
142
+ ).run(id, name, "", JSON.stringify(report), format, now);
143
+
144
+ return report;
145
+ }
146
+
147
+ /* ── Dashboards ────────────────────────────────────────────── */
148
+
149
+ export function createDashboard(db, name, widgets, layout) {
150
+ const id = crypto.randomUUID();
151
+ const now = new Date().toISOString();
152
+
153
+ const dashboard = {
154
+ id,
155
+ name,
156
+ widgets: widgets || [],
157
+ layout: layout || { type: "grid", columns: 2 },
158
+ createdAt: now,
159
+ };
160
+
161
+ _dashboards.set(id, dashboard);
162
+
163
+ db.prepare(
164
+ `INSERT INTO bi_dashboards (id, name, widgets, layout, created_at, updated_at)
165
+ VALUES (?, ?, ?, ?, ?, ?)`,
166
+ ).run(
167
+ id,
168
+ name,
169
+ JSON.stringify(dashboard.widgets),
170
+ JSON.stringify(dashboard.layout),
171
+ now,
172
+ now,
173
+ );
174
+
175
+ return dashboard;
176
+ }
177
+
178
+ /* ── Anomaly Detection ─────────────────────────────────────── */
179
+
180
+ export function detectAnomaly(data, options) {
181
+ if (!Array.isArray(data) || data.length === 0) {
182
+ throw new Error("Data must be a non-empty array of numbers");
183
+ }
184
+
185
+ const threshold = (options && options.threshold) || 2;
186
+ const n = data.length;
187
+ const mean = data.reduce((s, v) => s + v, 0) / n;
188
+ const variance = data.reduce((s, v) => s + (v - mean) ** 2, 0) / n;
189
+ const std = Math.sqrt(variance);
190
+
191
+ const anomalies = [];
192
+ if (std > 0) {
193
+ for (let i = 0; i < data.length; i++) {
194
+ const zScore = Math.abs((data[i] - mean) / std);
195
+ if (zScore > threshold) {
196
+ anomalies.push({ index: i, value: data[i], zScore });
197
+ }
198
+ }
199
+ }
200
+
201
+ return { anomalies, mean, std, threshold };
202
+ }
203
+
204
+ /* ── Trend Prediction ──────────────────────────────────────── */
205
+
206
+ export function predictTrend(data, periods) {
207
+ if (!Array.isArray(data) || data.length < 2) {
208
+ throw new Error("Data must be an array with at least 2 points");
209
+ }
210
+
211
+ const n = data.length;
212
+ const periodsToPredict = periods || 3;
213
+
214
+ // Simple linear regression: y = slope * x + intercept
215
+ let sumX = 0;
216
+ let sumY = 0;
217
+ let sumXY = 0;
218
+ let sumX2 = 0;
219
+
220
+ for (let i = 0; i < n; i++) {
221
+ sumX += i;
222
+ sumY += data[i];
223
+ sumXY += i * data[i];
224
+ sumX2 += i * i;
225
+ }
226
+
227
+ const slope = (n * sumXY - sumX * sumY) / (n * sumX2 - sumX * sumX);
228
+ const intercept = (sumY - slope * sumX) / n;
229
+
230
+ const predictions = [];
231
+ for (let i = 0; i < periodsToPredict; i++) {
232
+ const x = n + i;
233
+ predictions.push(Math.round((slope * x + intercept) * 100) / 100);
234
+ }
235
+
236
+ let trend;
237
+ if (Math.abs(slope) < 0.001) {
238
+ trend = "flat";
239
+ } else if (slope > 0) {
240
+ trend = "up";
241
+ } else {
242
+ trend = "down";
243
+ }
244
+
245
+ return { predictions, trend, slope: Math.round(slope * 1000) / 1000 };
246
+ }
247
+
248
+ /* ── Templates ─────────────────────────────────────────────── */
249
+
250
+ export function listTemplates() {
251
+ return [..._templates];
252
+ }
253
+
254
+ /* ── Scheduling ────────────────────────────────────────────── */
255
+
256
+ export function scheduleReport(db, reportId, cron, recipients) {
257
+ const id = crypto.randomUUID();
258
+ const schedule = {
259
+ id,
260
+ reportId,
261
+ cron,
262
+ recipients: recipients || [],
263
+ lastRun: null,
264
+ status: "active",
265
+ };
266
+
267
+ _scheduledReports.set(id, schedule);
268
+
269
+ db.prepare(
270
+ `INSERT INTO bi_scheduled (id, report_id, cron, recipients, last_run, status)
271
+ VALUES (?, ?, ?, ?, ?, ?)`,
272
+ ).run(id, reportId, cron, JSON.stringify(schedule.recipients), "", "active");
273
+
274
+ return schedule;
275
+ }
276
+
277
+ /* ── Export ─────────────────────────────────────────────────── */
278
+
279
+ export function exportReport(reportId, format) {
280
+ const report = _reports.get(reportId);
281
+ if (!report) throw new Error(`Report not found: ${reportId}`);
282
+
283
+ const exportFormat = format || report.format || "pdf";
284
+ return {
285
+ reportId,
286
+ format: exportFormat,
287
+ filename: `${report.name.replace(/\s+/g, "_")}.${exportFormat}`,
288
+ size: 0,
289
+ exportedAt: new Date().toISOString(),
290
+ };
291
+ }
292
+
293
+ /* ── Reset (for testing) ───────────────────────────────────── */
294
+
295
+ export function _resetState() {
296
+ _dashboards.clear();
297
+ _reports.clear();
298
+ _scheduledReports.clear();
299
+ }