lumencode 1.3.2 → 1.3.4

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/lib/server.js CHANGED
@@ -1,7 +1,8 @@
1
- import { createServer } from 'http';
2
- import { readFileSync, existsSync } from 'fs';
3
- import { join, extname, resolve, sep } from 'path';
4
- import { fileURLToPath } from 'url';
1
+ import { createServer } from 'http';
2
+ import { readFileSync, existsSync } from 'fs';
3
+ import { join, extname, resolve, sep } from 'path';
4
+ import { fileURLToPath } from 'url';
5
+ import { randomUUID } from 'crypto';
5
6
  import { saveConfig } from './config.js';
6
7
  import { parseRepoPaths } from './path-utils.js';
7
8
  import { generateWorkReport, generateFeishuCard, generateBossReport } from './report.js';
@@ -14,7 +15,9 @@ import { identifyBillingBlocks } from './blocks.js';
14
15
  import { detectAvailableTools, parseAllEnabledTools } from './parsers/index.js';
15
16
  import { isAssistantRecord, getInputTokens, getOutputTokens } from './record-utils.js';
16
17
  import { StepTracker } from './step-tracker.js';
17
- import { disableHooks, enableHooks, getHooksStatus, HOOK_TOOLS, initStepTracking } from './hooks-manager.js';
18
+ import { disableHooks, enableHooks, getHooksStatus, HOOK_TOOLS, initStepTracking } from './hooks-manager.js';
19
+ import { SMART_REPORT_PROMPT_MARKER, buildSmartReportContext, createSmartReport, detectSmartReportAgents } from './smart-report.js';
20
+ import { buildSmartReportKey, buildSourceHash, getSmartReportStoreDir, readSmartReportRecord, saveSmartReportRecord } from './smart-report-store.js';
18
21
 
19
22
  // basename 提取,兼容不同路径格式
20
23
  function getProjectBaseName(p) {
@@ -87,9 +90,11 @@ export function startServer(config, effectiveIncludeProjects, buildReportData, c
87
90
  const PARSED_CACHE_TTL = 30_000; // 30s
88
91
 
89
92
  // ── 查询结果缓存(按查询条件缓存 buildReportData 结果) ──
90
- const _reportCache = new Map();
91
- const REPORT_CACHE_TTL = 30_000; // 30s
92
- const REPORT_CACHE_MAX_SIZE = 50;
93
+ const _reportCache = new Map();
94
+ const REPORT_CACHE_TTL = 30_000; // 30s
95
+ const REPORT_CACHE_MAX_SIZE = 50;
96
+ const smartReportJobs = new Map();
97
+ const SMART_REPORT_JOB_KEEP_MS = 30 * 60_000;
93
98
 
94
99
  function getReportCacheKey(period, date, tool, customStart, customEnd) {
95
100
  return `${period}|${date}|${tool || 'all'}|${customStart || ''}|${customEnd || ''}`;
@@ -115,13 +120,38 @@ export function startServer(config, effectiveIncludeProjects, buildReportData, c
115
120
  _reportCache.clear();
116
121
  }
117
122
 
118
- function writeJson(res, statusCode, data) {
123
+ function writeJson(res, statusCode, data) {
119
124
  res.writeHead(statusCode, {
120
125
  'Content-Type': 'application/json',
121
126
  'Access-Control-Allow-Origin': 'http://localhost:' + PORT,
122
127
  });
123
- res.end(JSON.stringify(data));
124
- }
128
+ res.end(JSON.stringify(data));
129
+ }
130
+
131
+ function publicSmartReportJob(job) {
132
+ if (!job) return null;
133
+ return {
134
+ id: job.id,
135
+ reportKey: job.reportKey,
136
+ status: job.status,
137
+ error: job.error || '',
138
+ startedAt: job.startedAt,
139
+ updatedAt: job.updatedAt,
140
+ completedAt: job.completedAt || '',
141
+ };
142
+ }
143
+
144
+ function rememberSmartReportJob(job) {
145
+ smartReportJobs.set(job.reportKey, job);
146
+ return job;
147
+ }
148
+
149
+ function forgetSmartReportJobLater(reportKey) {
150
+ setTimeout(() => {
151
+ const job = smartReportJobs.get(reportKey);
152
+ if (job && job.status !== 'running') smartReportJobs.delete(reportKey);
153
+ }, SMART_REPORT_JOB_KEEP_MS).unref?.();
154
+ }
125
155
 
126
156
  function parseHookTools(value) {
127
157
  if (!value) return [HOOK_TOOLS.CLAUDE, HOOK_TOOLS.CODEX, HOOK_TOOLS.OPENCODE];
@@ -181,7 +211,7 @@ export function startServer(config, effectiveIncludeProjects, buildReportData, c
181
211
  return result;
182
212
  }
183
213
 
184
- function deriveProjectGitStats(gitStats, project, start, end, attributionOptions) {
214
+ function deriveProjectGitStats(gitStats, project, start, end, attributionOptions) {
185
215
  if (!gitStats?.commitList?.length || !project) return null;
186
216
  const windowEnd = end + 'T23:59:59';
187
217
  const commitList = gitStats.commitList.filter(c => {
@@ -199,10 +229,178 @@ export function startServer(config, effectiveIncludeProjects, buildReportData, c
199
229
  fileHotspots: computeFileHotspots(commitList, 10),
200
230
  aiContribution: computeAIContribution(commitList, null, attributionOptions),
201
231
  attributionSummary: gitStats.attributionSummary,
202
- };
203
- }
204
-
205
- function prepareJsonReportData(baseData, tool) {
232
+ };
233
+ }
234
+
235
+ function parseSmartReportParams(input = {}) {
236
+ return {
237
+ agent: input.agent || '',
238
+ period: input.period || 'daily',
239
+ date: input.date || new Date().toISOString().slice(0, 10),
240
+ start: input.start || '',
241
+ end: input.end || '',
242
+ tool: input.tool || 'all',
243
+ project: input.project || '',
244
+ level: input.level || 'detailed',
245
+ style: input.style || 'default',
246
+ platform: input.platform || 'default',
247
+ };
248
+ }
249
+
250
+ function validateSmartReportParams(params) {
251
+ const validPeriods = ['daily', 'weekly', 'monthly', 'custom'];
252
+ if (!validPeriods.includes(params.period)) {
253
+ return `无效的 period 参数,可选值:${validPeriods.join('/')}`;
254
+ }
255
+ if (params.period === 'custom') {
256
+ if (!params.start || !params.end || !/^\d{4}-\d{2}-\d{2}$/.test(params.start) || !/^\d{4}-\d{2}-\d{2}$/.test(params.end)) {
257
+ return '自定义周期需要 start 和 end 参数 (YYYY-MM-DD)';
258
+ }
259
+ if (params.start > params.end) return '起始日期不能晚于结束日期';
260
+ }
261
+ const validStyles = ['default', 'workhorse'];
262
+ if (!validStyles.includes(params.style)) {
263
+ return `无效的 style 参数,可选值:${validStyles.join('/')}`;
264
+ }
265
+ return '';
266
+ }
267
+
268
+ function buildSmartReportRecordIdentity(params, data) {
269
+ return {
270
+ agent: params.agent,
271
+ period: params.period,
272
+ date: params.period === 'daily' ? data.start : '',
273
+ start: data.start,
274
+ end: data.end,
275
+ tool: params.tool,
276
+ project: params.project,
277
+ level: params.level,
278
+ style: params.style,
279
+ platform: params.platform,
280
+ };
281
+ }
282
+
283
+ function isSmartReportInternalRecord(record) {
284
+ const text = String(record?.metadata?.text || record?.text || '');
285
+ return text.includes(SMART_REPORT_PROMPT_MARKER)
286
+ || text.includes('LumenCode 的智能报告分析器');
287
+ }
288
+
289
+ function filterInternalSmartReportParsed(parsed) {
290
+ if (!parsed?.records?.length) return parsed;
291
+ const internalSessions = new Set(
292
+ parsed.records
293
+ .filter(isSmartReportInternalRecord)
294
+ .map(r => r.sessionId)
295
+ .filter(Boolean)
296
+ );
297
+ if (internalSessions.size === 0) return parsed;
298
+
299
+ const records = parsed.records.filter(r => !internalSessions.has(r.sessionId));
300
+ const toolBreakdown = {};
301
+ for (const [tool, base] of Object.entries(parsed.toolBreakdown || {})) {
302
+ toolBreakdown[tool] = { recordCount: 0, sessionCount: 0, error: base.error };
303
+ }
304
+ const groups = {};
305
+ for (const record of records) {
306
+ const tool = record.tool || 'claude';
307
+ if (!groups[tool]) groups[tool] = { recordCount: 0, sessions: new Set() };
308
+ groups[tool].recordCount++;
309
+ if (record.sessionId) groups[tool].sessions.add(record.sessionId);
310
+ }
311
+ for (const [tool, group] of Object.entries(groups)) {
312
+ toolBreakdown[tool] = {
313
+ recordCount: group.recordCount,
314
+ sessionCount: group.sessions.size,
315
+ };
316
+ }
317
+ return { ...parsed, records, toolBreakdown };
318
+ }
319
+
320
+ function sourceReportHashesMatch(record, source) {
321
+ const saved = record?.sourceReports || {};
322
+ const current = source?.sourceHashes || {};
323
+ if (!saved.detailedHash && !saved.briefHash && !saved.bossHash) return false;
324
+ return ['detailedHash', 'briefHash', 'bossHash'].every(key => (saved[key] || '') === (current[key] || ''));
325
+ }
326
+
327
+ function smartReportNeedsUpdate(record, source) {
328
+ if (!record?.sourceHash) return false;
329
+ if (record.sourceHash === source.sourceHash) return false;
330
+ if ((record.sourceHashVersion || 1) < 2 && sourceReportHashesMatch(record, source)) return false;
331
+ return true;
332
+ }
333
+
334
+ async function buildSmartReportSource(params) {
335
+ const includeProjects = computeIncludeProjects(config);
336
+ const parsed = filterInternalSmartReportParsed(await getOrParse(config, includeProjects));
337
+ const data = await buildReportData(params.period, params.date, config, includeProjects, params.tool, parsed, {
338
+ customStart: params.start,
339
+ customEnd: params.end,
340
+ });
341
+ if (!data) return null;
342
+
343
+ let reportData = data;
344
+ let workUsageStats = data.usageStats;
345
+ let workGitStats = data.gitStats;
346
+ let workPrevStats = data.prevStats;
347
+ let projectName = '';
348
+
349
+ if (params.project) {
350
+ const { records: allRecords } = parsed;
351
+ const toolRecords = params.tool !== 'all' ? allRecords.filter(r => r.tool === params.tool) : allRecords;
352
+ const projRecords = toolRecords.filter(r => getProjectBaseName(r.project) === params.project);
353
+ const { filtered: projFiltered, start: pStart, end: pEnd } = filterRecordsByPeriod(projRecords, params.period, params.date, { customStart: params.start, customEnd: params.end });
354
+ workUsageStats = projFiltered.length > 0 ? computeUsageStats(projFiltered, config.scenarioKeywords, config.costMode) : { requestCount: 0, projects: {} };
355
+ workPrevStats = null;
356
+ workGitStats = deriveProjectGitStats(data.gitStats, params.project, pStart, pEnd, config.aiAttribution);
357
+ projectName = params.project;
358
+ reportData = {
359
+ ...data,
360
+ usageStats: workUsageStats,
361
+ gitStats: workGitStats,
362
+ prevStats: workPrevStats,
363
+ };
364
+ }
365
+
366
+ const detailedMarkdown = generateWorkReport(workUsageStats, workGitStats, params.period, data.start, data.end, workPrevStats, { level: 'detailed', platform: params.platform, tool: params.tool, projectName });
367
+ const briefMarkdown = generateWorkReport(workUsageStats, workGitStats, params.period, data.start, data.end, workPrevStats, { level: 'brief', platform: params.platform, tool: params.tool, projectName });
368
+ const bossMarkdown = generateBossReport(workUsageStats, workGitStats, params.period, data.start, data.end, workPrevStats, params.platform);
369
+ const workMarkdown = params.level === 'brief' ? briefMarkdown : detailedMarkdown;
370
+ const includeBossSource = params.style === 'workhorse';
371
+ const sourceReports = {
372
+ detailedMarkdown,
373
+ briefMarkdown,
374
+ bossMarkdown: includeBossSource ? bossMarkdown : '',
375
+ };
376
+ const sourceHashes = {
377
+ detailedHash: buildSourceHash(detailedMarkdown),
378
+ briefHash: buildSourceHash(briefMarkdown),
379
+ bossHash: includeBossSource ? buildSourceHash(bossMarkdown) : '',
380
+ };
381
+ const sourceHash = buildSourceHash(buildSmartReportContext(reportData, workMarkdown, {
382
+ period: params.period,
383
+ date: params.date,
384
+ tool: params.tool,
385
+ project: params.project,
386
+ level: params.level,
387
+ style: params.style,
388
+ platform: params.platform,
389
+ sourceReports,
390
+ }));
391
+
392
+ return {
393
+ data,
394
+ reportData,
395
+ workMarkdown,
396
+ sourceReports,
397
+ sourceHashes,
398
+ sourceHash,
399
+ identity: buildSmartReportRecordIdentity(params, data),
400
+ };
401
+ }
402
+
403
+ function prepareJsonReportData(baseData, tool) {
206
404
  const data = { ...baseData };
207
405
 
208
406
  if (tool !== 'all' && data.gitStats?.aiContributionByTool) {
@@ -251,8 +449,146 @@ export function startServer(config, effectiveIncludeProjects, buildReportData, c
251
449
  res.setHeader('X-XSS-Protection', '1; mode=block');
252
450
  res.setHeader('Permissions-Policy', 'camera=(), microphone=(), geolocation=()');
253
451
 
254
- // API endpoint
255
- if (url.pathname === '/api/hooks') {
452
+ // API endpoint
453
+ if (url.pathname === '/api/smart-report/tools') {
454
+ try {
455
+ const tools = await detectSmartReportAgents();
456
+ writeJson(res, 200, { tools });
457
+ } catch (err) {
458
+ console.error('API error:', err.message);
459
+ writeJson(res, 500, { error: err.message || '服务端内部错误' });
460
+ }
461
+ return;
462
+ }
463
+
464
+ if (url.pathname === '/api/smart-report') {
465
+ if (req.method === 'GET') {
466
+ const params = parseSmartReportParams({
467
+ agent: url.searchParams.get('agent') || '',
468
+ period: url.searchParams.get('period') || 'daily',
469
+ date: url.searchParams.get('date') || '',
470
+ start: url.searchParams.get('start') || '',
471
+ end: url.searchParams.get('end') || '',
472
+ tool: url.searchParams.get('tool') || 'all',
473
+ project: url.searchParams.get('project') || '',
474
+ level: url.searchParams.get('level') || 'detailed',
475
+ style: url.searchParams.get('style') || 'default',
476
+ platform: url.searchParams.get('platform') || 'default',
477
+ });
478
+ const validationError = validateSmartReportParams(params);
479
+ if (validationError) {
480
+ writeJson(res, 400, { error: validationError });
481
+ return;
482
+ }
483
+
484
+ const source = await buildSmartReportSource(params);
485
+ if (!source) {
486
+ writeJson(res, 200, { record: null, needsUpdate: false });
487
+ return;
488
+ }
489
+
490
+ const storeDir = getSmartReportStoreDir(configPath);
491
+ let record = readSmartReportRecord(storeDir, buildSmartReportKey(source.identity));
492
+ if (!record) {
493
+ record = readSmartReportRecord(storeDir, buildSmartReportKey(params));
494
+ }
495
+ const reportKey = buildSmartReportKey(source.identity);
496
+ const job = smartReportJobs.get(reportKey) || null;
497
+ const needsUpdate = smartReportNeedsUpdate(record, source);
498
+ writeJson(res, 200, {
499
+ record,
500
+ job: publicSmartReportJob(job),
501
+ needsUpdate,
502
+ currentSourceHash: source.sourceHash,
503
+ range: { start: source.data.start, end: source.data.end },
504
+ });
505
+ return;
506
+ }
507
+
508
+ if (req.method !== 'POST') {
509
+ writeJson(res, 405, { error: 'Method not allowed' });
510
+ return;
511
+ }
512
+
513
+ readJsonBody(req, res, async (body) => {
514
+ const params = parseSmartReportParams(body);
515
+ const validationError = validateSmartReportParams(params);
516
+ if (validationError) {
517
+ writeJson(res, 400, { error: validationError });
518
+ return;
519
+ }
520
+
521
+ const source = await buildSmartReportSource(params);
522
+ if (!source) {
523
+ writeJson(res, 404, { error: '未找到可用于智能报告的数据' });
524
+ return;
525
+ }
526
+
527
+ const storeDir = getSmartReportStoreDir(configPath);
528
+ const reportKey = buildSmartReportKey(source.identity);
529
+ const existingJob = smartReportJobs.get(reportKey);
530
+ if (existingJob?.status === 'running') {
531
+ const record = readSmartReportRecord(storeDir, reportKey);
532
+ writeJson(res, 202, {
533
+ record,
534
+ job: publicSmartReportJob(existingJob),
535
+ needsUpdate: smartReportNeedsUpdate(record, source),
536
+ currentSourceHash: source.sourceHash,
537
+ });
538
+ return;
539
+ }
540
+
541
+ const job = rememberSmartReportJob({
542
+ id: randomUUID(),
543
+ reportKey,
544
+ status: 'running',
545
+ error: '',
546
+ startedAt: new Date().toISOString(),
547
+ updatedAt: new Date().toISOString(),
548
+ completedAt: '',
549
+ });
550
+
551
+ Promise.resolve().then(async () => {
552
+ try {
553
+ const markdown = await createSmartReport({
554
+ agent: params.agent,
555
+ reportData: source.reportData,
556
+ workMarkdown: source.workMarkdown,
557
+ options: { period: params.period, date: params.date, tool: params.tool, project: params.project, level: params.level, style: params.style, platform: params.platform, sourceReports: source.sourceReports },
558
+ requireAvailable: true,
559
+ });
560
+ const record = saveSmartReportRecord(storeDir, {
561
+ ...source.identity,
562
+ markdown,
563
+ sourceHash: source.sourceHash,
564
+ sourceHashVersion: 2,
565
+ sourceReports: source.sourceHashes,
566
+ });
567
+ job.status = 'completed';
568
+ job.record = record;
569
+ job.completedAt = new Date().toISOString();
570
+ job.updatedAt = job.completedAt;
571
+ } catch (err) {
572
+ job.status = 'failed';
573
+ job.error = err.message || '智能报告生成失败';
574
+ job.updatedAt = new Date().toISOString();
575
+ } finally {
576
+ forgetSmartReportJobLater(reportKey);
577
+ }
578
+ });
579
+
580
+ const record = readSmartReportRecord(storeDir, reportKey);
581
+ writeJson(res, 202, {
582
+ record,
583
+ job: publicSmartReportJob(job),
584
+ needsUpdate: smartReportNeedsUpdate(record, source),
585
+ currentSourceHash: source.sourceHash,
586
+ });
587
+ });
588
+ return;
589
+ }
590
+
591
+ if (url.pathname === '/api/hooks') {
256
592
  try {
257
593
  if (req.method === 'GET') {
258
594
  writeJson(res, 200, getConfiguredHooksStatus(config));
@@ -402,11 +738,12 @@ export function startServer(config, effectiveIncludeProjects, buildReportData, c
402
738
  return;
403
739
  }
404
740
 
405
- if (format === 'work') {
406
- const platform = url.searchParams.get('platform') || 'default';
407
- const level = url.searchParams.get('level') || 'detailed';
408
- const feishuCard = url.searchParams.get('feishuCard') === 'true';
409
- const project = url.searchParams.get('project') || '';
741
+ if (format === 'work') {
742
+ const platform = url.searchParams.get('platform') || 'default';
743
+ const requestedLevel = url.searchParams.get('level') || 'detailed';
744
+ const level = requestedLevel === 'brief' ? 'brief' : 'detailed';
745
+ const feishuCard = url.searchParams.get('feishuCard') === 'true';
746
+ const project = url.searchParams.get('project') || '';
410
747
 
411
748
  if (feishuCard) {
412
749
  const card = generateFeishuCard(data.usageStats, data.gitStats, period, data.start, data.end, tool);
@@ -446,12 +783,7 @@ export function startServer(config, effectiveIncludeProjects, buildReportData, c
446
783
  projGitStats = deriveProjectGitStats(data.gitStats, project, pStart, pEnd, config.aiAttribution);
447
784
  }
448
785
 
449
- let markdown;
450
- if (level === 'boss') {
451
- markdown = generateBossReport(projUsageStats, projGitStats, period, data.start, data.end, projPrevStats, platform);
452
- } else {
453
- markdown = generateWorkReport(projUsageStats, projGitStats, period, data.start, data.end, projPrevStats, { level, platform, tool, projectName });
454
- }
786
+ const markdown = generateWorkReport(projUsageStats, projGitStats, period, data.start, data.end, projPrevStats, { level, platform, tool, projectName });
455
787
  res.writeHead(200, {
456
788
  'Content-Type': 'text/plain; charset=utf-8',
457
789
  'Access-Control-Allow-Origin': 'http://localhost:' + PORT,
@@ -0,0 +1,98 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
2
+ import { dirname, join } from 'path';
3
+ import { createHash } from 'crypto';
4
+
5
+ const STORE_FILE = 'records.json';
6
+
7
+ function stableStringify(value) {
8
+ if (Array.isArray(value)) return `[${value.map(stableStringify).join(',')}]`;
9
+ if (value && typeof value === 'object') {
10
+ return `{${Object.keys(value).sort().map(key => `${JSON.stringify(key)}:${stableStringify(value[key])}`).join(',')}}`;
11
+ }
12
+ return JSON.stringify(value);
13
+ }
14
+
15
+ function hashText(text) {
16
+ return createHash('sha256').update(String(text)).digest('hex');
17
+ }
18
+
19
+ export function getSmartReportStoreDir(configPath) {
20
+ return join(dirname(configPath || join(process.cwd(), 'config.json')), 'smart-reports');
21
+ }
22
+
23
+ export function buildSmartReportKey(input = {}) {
24
+ return hashText(stableStringify({
25
+ agent: input.agent || '',
26
+ period: input.period || '',
27
+ date: input.date || '',
28
+ start: input.start || '',
29
+ end: input.end || '',
30
+ tool: input.tool || 'all',
31
+ project: input.project || '',
32
+ level: input.level || 'detailed',
33
+ style: input.style && input.style !== 'default' ? input.style : '',
34
+ platform: input.platform || 'default',
35
+ }));
36
+ }
37
+
38
+ export function buildSourceHash(source = {}) {
39
+ return hashText(stableStringify(source));
40
+ }
41
+
42
+ function getStorePath(storeDir) {
43
+ return join(storeDir, STORE_FILE);
44
+ }
45
+
46
+ function readStore(storeDir) {
47
+ const file = getStorePath(storeDir);
48
+ if (!existsSync(file)) return { records: {} };
49
+ try {
50
+ const parsed = JSON.parse(readFileSync(file, 'utf8'));
51
+ if (!parsed || typeof parsed !== 'object' || !parsed.records) return { records: {} };
52
+ return parsed;
53
+ } catch {
54
+ return { records: {} };
55
+ }
56
+ }
57
+
58
+ function writeStore(storeDir, store) {
59
+ if (!existsSync(storeDir)) mkdirSync(storeDir, { recursive: true });
60
+ writeFileSync(getStorePath(storeDir), JSON.stringify(store, null, 2), 'utf8');
61
+ }
62
+
63
+ export function readSmartReportRecord(storeDir, reportKey) {
64
+ const store = readStore(storeDir);
65
+ return store.records[reportKey] || null;
66
+ }
67
+
68
+ export function saveSmartReportRecord(storeDir, input) {
69
+ const store = readStore(storeDir);
70
+ const reportKey = buildSmartReportKey(input);
71
+ const now = new Date().toISOString();
72
+ const existing = store.records[reportKey] || null;
73
+ const record = {
74
+ id: existing?.id || reportKey,
75
+ reportKey,
76
+ agent: input.agent || '',
77
+ period: input.period || '',
78
+ date: input.date || '',
79
+ start: input.start || '',
80
+ end: input.end || '',
81
+ tool: input.tool || 'all',
82
+ project: input.project || '',
83
+ level: input.level || 'detailed',
84
+ style: input.style || 'default',
85
+ platform: input.platform || 'default',
86
+ markdown: input.markdown || '',
87
+ sourceHash: input.sourceHash || '',
88
+ sourceHashVersion: input.sourceHashVersion || 1,
89
+ sourceReports: input.sourceReports || {},
90
+ createdAt: existing?.createdAt || now,
91
+ updatedAt: now,
92
+ generatedCount: (existing?.generatedCount || 0) + 1,
93
+ };
94
+
95
+ store.records[reportKey] = record;
96
+ writeStore(storeDir, store);
97
+ return record;
98
+ }