devassist-agent 1.0.4 → 1.0.5

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/bin/devassist.js CHANGED
@@ -172,6 +172,7 @@ function printHelp() {
172
172
  console.log(` ${c.green('watch')} 实时监控文件变更并运行检查`);
173
173
  console.log(` ${c.green('diff')} ${c.gray('--staged|--base <分支>')} 仅检查变更文件(CI 模式)`);
174
174
  console.log(` ${c.green('report')} ${c.gray('[--open]')} 生成 HTML 检查报告`);
175
+ console.log(` ${c.gray(' --watch-history [n]')} 查看最近 n 条监控复盘报告(默认 20)`);
175
176
  console.log(` ${c.green('deploy')} ${c.gray('--check')} 部署前验证(端口 + 路由 + MIME + 清单)`);
176
177
  console.log(` ${c.green('debt')} ${c.gray('--scan')} 查看技术债务看板(按优先级排序)`);
177
178
  console.log(` ${c.green('convention')} ${c.gray('add|list|remove')} 管理项目约定`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "devassist-agent",
3
- "version": "1.0.4",
3
+ "version": "1.0.5",
4
4
  "description": "AI Agent 开发质量守护工具 — 30+规则检测代码缺陷/安全漏洞/性能反模式,多引擎AI根因分析,零外部依赖",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -12,6 +12,7 @@
12
12
  * devassist report # Generate report
13
13
  * devassist report --ai # Include AI analysis
14
14
  * devassist report --open # Open in browser after generating
15
+ * devassist report --watch-history [n] # Show recent watch reports (default 20)
15
16
  */
16
17
 
17
18
  const fs = require('fs');
@@ -38,6 +39,11 @@ module.exports = async function report(projectRoot, args, ui) {
38
39
  const useAI = args.includes('--ai');
39
40
  const openBrowser = args.includes('--open');
40
41
 
42
+ // Watch history mode
43
+ if (args.includes('--watch-history')) {
44
+ return showWatchHistory(projectRoot, args, ui);
45
+ }
46
+
41
47
  printHeader('生成报告');
42
48
 
43
49
  // Collect files using shared utility
@@ -308,3 +314,101 @@ function findingHTML(f) {
308
314
  function escapeHTML(str) {
309
315
  return String(str).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
310
316
  }
317
+
318
+ /**
319
+ * Show watch history for retrospective.
320
+ * Usage: devassist report --watch-history [n]
321
+ */
322
+ function showWatchHistory(projectRoot, args, ui) {
323
+ const { printHeader, c } = ui;
324
+ printHeader('监控复盘报告');
325
+
326
+ const historyDir = path.join(projectRoot, '.devassist', 'history', 'watch');
327
+ const logPath = path.join(historyDir, 'watch-log.jsonl');
328
+
329
+ if (!fs.existsSync(logPath)) {
330
+ console.log(` ${c.yellow('暂无监控历史记录。')}`);
331
+ console.log(` ${c.gray('运行 devassist start 后,文件变更的检查/分析/修复记录会自动保存到这里。')}\n`);
332
+ return;
333
+ }
334
+
335
+ // Parse limit from args
336
+ let limit = 20;
337
+ const idx = args.indexOf('--watch-history');
338
+ if (idx >= 0 && args[idx + 1] && /^\d+$/.test(args[idx + 1])) {
339
+ limit = parseInt(args[idx + 1]);
340
+ }
341
+
342
+ // Read JSONL log
343
+ const lines = fs.readFileSync(logPath, 'utf-8').trim().split('\n').filter(Boolean);
344
+ const entries = lines.map(l => { try { return JSON.parse(l); } catch (_) { return null; } }).filter(Boolean);
345
+
346
+ if (entries.length === 0) {
347
+ console.log(` ${c.yellow('暂无监控历史记录。')}\n`);
348
+ return;
349
+ }
350
+
351
+ // Show recent entries (newest first)
352
+ const recent = entries.slice(-limit).reverse();
353
+ console.log(` ${c.gray(`共 ${entries.length} 条记录,显示最近 ${recent.length} 条:`)}\n`);
354
+
355
+ // Summary stats
356
+ const totalEvents = entries.length;
357
+ const totalFindings = entries.reduce((s, e) => s + (e.total || 0), 0);
358
+ const totalBlocks = entries.reduce((s, e) => s + (e.blocks || 0), 0);
359
+ const totalFixed = entries.reduce((s, e) => s + (e.fixed || 0), 0);
360
+ const aiEvents = entries.filter(e => e.aiUsed).length;
361
+
362
+ console.log(` ${c.bold('汇总统计:')}`);
363
+ console.log(` 监控事件:${totalEvents} 次`);
364
+ console.log(` 发现问题:${totalFindings} 个(其中 ${c.red(totalBlocks + ' 阻断')})`);
365
+ console.log(` 自动修复:${c.green(totalFixed + ' 处')}`);
366
+ console.log(` AI 分析:${aiEvents} 次\n`);
367
+
368
+ // Detail table
369
+ console.log(` ${c.bold('最近事件:')}\n`);
370
+ console.log(` ${c.gray('时间'.padEnd(20))} ${c.gray('文件'.padEnd(30))} ${c.gray('问题'.padEnd(8))} ${c.gray('修复'.padEnd(6))} ${c.gray('引擎')}`);
371
+ console.log(` ${'─'.repeat(90)}`);
372
+
373
+ for (const e of recent) {
374
+ const time = new Date(e.timestamp).toLocaleString('zh-CN', { month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit' });
375
+ const file = (e.file || '').length > 28 ? '...' + (e.file || '').slice(-25) : (e.file || '');
376
+ const issues = e.total > 0 ? c.yellow(String(e.total)) : c.green('0');
377
+ const blocks = e.blocks > 0 ? c.red(`(${e.blocks}阻断)`) : '';
378
+ const fixed = e.fixed > 0 ? c.green(String(e.fixed)) : '-';
379
+ const engines = e.engines || '-';
380
+
381
+ console.log(` ${time.padEnd(20)} ${file.padEnd(30)} ${issues} ${blocks}`.padEnd(58) + ` ${fixed.toString().padEnd(6)} ${engines}`);
382
+ }
383
+
384
+ // Latest report detail
385
+ const latestPath = path.join(projectRoot, '.devassist', 'reports', 'latest.json');
386
+ if (fs.existsSync(latestPath)) {
387
+ console.log(`\n ${c.gray('最近一次完整报告:')} ${latestPath}`);
388
+ console.log(` ${c.gray('历史报告目录:')} ${historyDir}`);
389
+ }
390
+
391
+ // Watch files with most issues
392
+ const fileStats = {};
393
+ for (const e of entries) {
394
+ if (!fileStats[e.file]) fileStats[e.file] = { count: 0, issues: 0, blocks: 0, fixed: 0 };
395
+ fileStats[e.file].count++;
396
+ fileStats[e.file].issues += e.total || 0;
397
+ fileStats[e.file].blocks += e.blocks || 0;
398
+ fileStats[e.file].fixed += e.fixed || 0;
399
+ }
400
+
401
+ const hotFiles = Object.entries(fileStats)
402
+ .sort((a, b) => b[1].issues - a[1].issues)
403
+ .filter(([, s]) => s.issues > 0)
404
+ .slice(0, 5);
405
+
406
+ if (hotFiles.length > 0) {
407
+ console.log(`\n ${c.bold('问题热点文件 TOP 5:')}\n`);
408
+ for (const [file, s] of hotFiles) {
409
+ console.log(` ${file} — ${s.issues} 个问题,${s.blocks} 阻断,${s.fixed} 已修复(${s.count} 次变更)`);
410
+ }
411
+ }
412
+
413
+ console.log('');
414
+ }
@@ -171,9 +171,6 @@ module.exports = async function watch(projectRoot, args, ui) {
171
171
  // Apply inline ignores
172
172
  allFindings = filterIgnored(allFindings, [{ path: relPath, content }]);
173
173
 
174
- // Save findings for ai --analyze
175
- saveWatchFindings(projectRoot, allFindings);
176
-
177
174
  // Print header
178
175
  console.log(`\n ${c.gray(`[${timestamp}]`)} ${c.bold(relPath)}`);
179
176
 
@@ -270,13 +267,37 @@ module.exports = async function watch(projectRoot, args, ui) {
270
267
  console.log(` ${c.gray('—')} ${parts.join(c.gray(' · '))}`);
271
268
 
272
269
  // --- Auto Fix (if enabled) ---
270
+ let fixResult = null;
273
271
  if (autoFix && suspectFindings.length > 0) {
274
- const fixedCount = await autoFixFile(filePath, content, projectRoot, c);
275
- if (fixedCount > 0) {
276
- console.log(` ${c.green('自动修复')} ${fixedCount} 处问题`);
272
+ fixResult = await autoFixFile(filePath, content, projectRoot, c);
273
+ if (fixResult.count > 0) {
274
+ console.log(` ${c.green('自动修复')} ${fixResult.count} 处问题`);
275
+ for (const d of fixResult.details) {
276
+ const typeLabel = d.type === 'fetch-catch' ? 'fetch 添加 .catch()' : '添加 await';
277
+ console.log(` ${c.gray(`第${d.line}行`)} ${typeLabel}`);
278
+ }
277
279
  }
278
280
  }
279
281
 
282
+ // --- Save comprehensive report for retrospective ---
283
+ saveWatchReport(projectRoot, {
284
+ timestamp: new Date().toISOString(),
285
+ file: relPath,
286
+ findings: finalFindings,
287
+ aiAnalysisUsed: useAI && readyEngines.length > 0 && suspectFindings.length > 0,
288
+ compareMode: useCompare && readyEngines.length >= 2,
289
+ engines: readyEngines.map(e => ({ provider: e.provider, model: e.model, name: e.name })),
290
+ fixes: fixResult ? fixResult.details : [],
291
+ fixBackupPath: fixResult ? fixResult.backupPath : null,
292
+ stats: {
293
+ total: finalFindings.length,
294
+ blocks: blocks.length,
295
+ warnings: warns.length,
296
+ infos: infos.length,
297
+ fixed: fixResult ? fixResult.count : 0,
298
+ },
299
+ });
300
+
280
301
  } finally {
281
302
  analyzingFiles.delete(filePath);
282
303
  }
@@ -346,7 +367,7 @@ async function autoFixFile(filePath, content, projectRoot, c) {
346
367
  const trimmed = line.trimEnd();
347
368
  if (trimmed.endsWith(')') && !trimmed.endsWith('.catch()')) {
348
369
  const fixedLine = trimmed.replace(/\)\s*$/, ").catch(err => console.error('fetch error:', err))");
349
- changes.push({ line: i, newLine: fixedLine, type: 'fetch-catch' });
370
+ changes.push({ line: i + 1, type: 'fetch-catch', before: line, after: fixedLine });
350
371
  fixCount++;
351
372
  continue;
352
373
  }
@@ -366,7 +387,7 @@ async function autoFixFile(filePath, content, projectRoot, c) {
366
387
  'await $1('
367
388
  );
368
389
  if (fixedLine !== line) {
369
- changes.push({ line: i, newLine: fixedLine, type: 'add-await' });
390
+ changes.push({ line: i + 1, type: 'add-await', before: line, after: fixedLine });
370
391
  fixCount++;
371
392
  continue;
372
393
  }
@@ -374,11 +395,11 @@ async function autoFixFile(filePath, content, projectRoot, c) {
374
395
  }
375
396
  }
376
397
 
377
- if (fixCount === 0) return 0;
398
+ if (fixCount === 0) return { count: 0, details: [] };
378
399
 
379
400
  // Apply changes
380
401
  for (const ch of changes) {
381
- lines[ch.line] = ch.newLine;
402
+ lines[ch.line - 1] = ch.after;
382
403
  }
383
404
  const newContent = lines.join('\n');
384
405
 
@@ -391,22 +412,60 @@ async function autoFixFile(filePath, content, projectRoot, c) {
391
412
  // Write fixed content
392
413
  fs.writeFileSync(filePath, newContent, 'utf-8');
393
414
 
394
- return fixCount;
415
+ return { count: fixCount, details: changes, backupPath };
395
416
  }
396
417
 
397
- function saveWatchFindings(projectRoot, findings) {
418
+ /**
419
+ * Save comprehensive watch report for retrospective.
420
+ * - latest.json: always overwritten with the latest event
421
+ * - history/watch-<timestamp>.json: per-event detail, kept for 90 days
422
+ * - history/watch-log.jsonl: append-only summary log for quick querying
423
+ */
424
+ function saveWatchReport(projectRoot, report) {
398
425
  const reportDir = path.join(projectRoot, '.devassist', 'reports');
399
426
  if (!fs.existsSync(reportDir)) fs.mkdirSync(reportDir, { recursive: true });
400
- const reportPath = path.join(reportDir, 'latest.json');
401
- fs.writeFileSync(reportPath, JSON.stringify({
402
- generatedAt: new Date().toISOString(),
403
- source: 'watch',
404
- findings,
405
- stats: {
406
- total: findings.length,
407
- blocks: findings.filter(f => f.severity === 'block').length,
408
- warnings: findings.filter(f => f.severity === 'warn').length,
409
- infos: findings.filter(f => f.severity === 'info').length,
410
- },
411
- }, null, 2), 'utf-8');
427
+
428
+ // 1. Save latest.json (full detail, always latest)
429
+ const latestPath = path.join(reportDir, 'latest.json');
430
+ fs.writeFileSync(latestPath, JSON.stringify(report, null, 2), 'utf-8');
431
+
432
+ // 2. Save timestamped history
433
+ const historyDir = path.join(projectRoot, '.devassist', 'history', 'watch');
434
+ if (!fs.existsSync(historyDir)) fs.mkdirSync(historyDir, { recursive: true });
435
+
436
+ const ts = new Date();
437
+ const tsStr = ts.toISOString().replace(/[:.]/g, '-');
438
+ const historyPath = path.join(historyDir, `watch-${tsStr}.json`);
439
+ fs.writeFileSync(historyPath, JSON.stringify(report, null, 2), 'utf-8');
440
+
441
+ // 3. Append summary to JSONL log
442
+ const logPath = path.join(historyDir, 'watch-log.jsonl');
443
+ const summary = {
444
+ timestamp: report.timestamp,
445
+ file: report.file,
446
+ total: report.stats.total,
447
+ blocks: report.stats.blocks,
448
+ warnings: report.stats.warnings,
449
+ infos: report.stats.infos,
450
+ fixed: report.stats.fixed,
451
+ aiUsed: report.aiAnalysisUsed,
452
+ compare: report.compareMode,
453
+ engines: report.engines.map(e => e.provider).join('+'),
454
+ };
455
+ fs.appendFileSync(logPath, JSON.stringify(summary) + '\n', 'utf-8');
456
+
457
+ // 4. Auto-cleanup: remove history files older than 90 days
458
+ try {
459
+ const files = fs.readdirSync(historyDir);
460
+ const now = Date.now();
461
+ const maxAge = 90 * 24 * 60 * 60 * 1000; // 90 days in ms
462
+ for (const f of files) {
463
+ if (!f.startsWith('watch-') || !f.endsWith('.json')) continue;
464
+ const filePath = path.join(historyDir, f);
465
+ const stat = fs.statSync(filePath);
466
+ if (now - stat.mtime.getTime() > maxAge) {
467
+ fs.unlinkSync(filePath);
468
+ }
469
+ }
470
+ } catch (_) {}
412
471
  }