devassist-agent 1.0.3 → 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
@@ -166,12 +166,13 @@ function printHelp() {
166
166
  printHeader('开发守护 CLI');
167
167
  console.log(`${c.bold('用法:')} devassist <命令> [选项]\n`);
168
168
  console.log(`${c.bold('命令:')}`);
169
- console.log(` ${c.green('start')} 一键启动:自动初始化 + AI多引擎实时监控`);
169
+ console.log(` ${c.green('start')} 一键启动:自动初始化 + AI双引擎实时监控 + 自动修复`);
170
170
  console.log(` ${c.green('init')} 初始化项目——创建 .devassist/ 配置与基础代码`);
171
171
  console.log(` ${c.green('check')} 运行全部开发时检查(代码质量 + 约定 + 模式 + 架构)`);
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')} 管理项目约定`);
@@ -189,18 +190,19 @@ function printHelp() {
189
190
  console.log(` ${c.gray('--ai')} 启用 AI 深度分析(需配置)`);
190
191
  console.log(` ${c.gray('--compare')} 多引擎对比分析(所有引擎并行)`);
191
192
  console.log(` ${c.gray('--json')} 输出 JSON 格式(用于 CI 集成)`);
193
+ console.log(` ${c.gray('--fix')} 自动修复安全项(watch 模式下生效)`);
192
194
  console.log(` ${c.gray('--fail-on <级别>')} 在以下级别非零退出:block(默认)或 warn`);
193
195
  console.log(` ${c.gray('--base-code')} 安装运行时基础设施(Layer 2)`);
194
196
  console.log(` ${c.gray('--force')} 覆盖已有配置`);
195
197
  console.log(` ${c.gray('-v, --version')} 显示版本`);
196
198
  console.log(` ${c.gray('-h, --help')} 显示帮助\n`);
197
199
  console.log(`${c.bold('示例:')}`);
198
- console.log(` ${c.gray('$')} devassist start # 一键启动(自动init+AI多引擎监控)`);
200
+ console.log(` ${c.gray('$')} devassist start # 一键启动(自动init+AI双引擎+自动修复)`);
199
201
  console.log(` ${c.gray('$')} devassist start --check # 一键检查(不启动监控)`);
200
202
  console.log(` ${c.gray('$')} devassist init`);
201
203
  console.log(` ${c.gray('$')} devassist check --ai`);
202
204
  console.log(` ${c.gray('$')} devassist watch # 开发时实时监控`);
203
- console.log(` ${c.gray('$')} devassist watch --ai --compare # 实时监控+AI多引擎对比`);
205
+ console.log(` ${c.gray('$')} devassist watch --ai --compare --fix # 全自动:检测+AI+修复`);
204
206
  console.log(` ${c.gray('$')} devassist diff --staged # 提交前检查暂存文件`);
205
207
  console.log(` ${c.gray('$')} devassist report --open # 生成并查看 HTML 报告`);
206
208
  console.log(` ${c.gray('$')} devassist convention add --id naming-list --rule "使用 data.list 而非 data.items" --severity block`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "devassist-agent",
3
- "version": "1.0.3",
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
+ }
@@ -7,8 +7,9 @@
7
7
  * 3. Start watch mode with --ai --compare
8
8
  *
9
9
  * Usage:
10
- * devassist start # Full auto: init + watch --ai --compare
10
+ * devassist start # Full auto: init + watch --ai --compare --fix
11
11
  * devassist start --no-ai # Init + watch (rules only, no AI)
12
+ * devassist start --no-fix # Init + watch --ai --compare (no auto-fix)
12
13
  * devassist start --check # Init + one-time check --ai --compare (no watch)
13
14
  */
14
15
 
@@ -20,6 +21,7 @@ module.exports = async function start(projectRoot, args, ui) {
20
21
  const { printHeader, printFinding, printStats, c } = ui;
21
22
 
22
23
  const noAI = args.includes('--no-ai');
24
+ const noFix = args.includes('--no-fix');
23
25
  const checkOnly = args.includes('--check');
24
26
 
25
27
  printHeader('一键启动');
@@ -51,6 +53,9 @@ module.exports = async function start(projectRoot, args, ui) {
51
53
  if (aiReady && !noAI) {
52
54
  const engineNames = readyEngines.map(e => e.name || e.provider).join(' + ');
53
55
  console.log(` ${c.green('2/3')} AI 引擎已就绪 ✓ (${configSource}:${engineNames})`);
56
+ if (!noFix) {
57
+ console.log(` ${c.gray('自动修复已启用(--fix)')}`);
58
+ }
54
59
  } else if (readyEngines.length === 1 && !noAI) {
55
60
  console.log(` ${c.green('2/3')} AI 引擎已就绪 ✓ (${configSource}:${readyEngines[0].name},仅单引擎)`);
56
61
  console.log(` ${c.gray('提示:配置第二个引擎可启用对比模式')}`);
@@ -79,6 +84,9 @@ module.exports = async function start(projectRoot, args, ui) {
79
84
  if (!noAI) {
80
85
  watchArgs.push('--ai', '--compare');
81
86
  }
87
+ if (!noFix) {
88
+ watchArgs.push('--fix');
89
+ }
82
90
  await watchCmd(projectRoot, watchArgs, ui);
83
91
  }
84
92
  };
@@ -1,18 +1,20 @@
1
1
  /**
2
- * devassist watch - Real-time file monitoring
2
+ * devassist watch - Real-time file monitoring with full automation
3
3
  *
4
4
  * Watches project files for changes and runs checks on modified files.
5
- * Provides instant feedback during AI-assisted development.
5
+ * With --ai flag, automatically runs AI analysis on findings.
6
+ * With --compare flag, runs multi-engine comparison.
7
+ * With --fix flag, automatically applies safe fixes.
6
8
  *
7
9
  * Usage:
8
- * devassist watch # Watch all source files
9
- * devassist watch --ai # Watch + AI analysis on findings
10
- * devassist watch --dir src # Watch specific directory
10
+ * devassist watch # Watch all source files (rules only)
11
+ * devassist watch --ai # Watch + AI analysis on findings
12
+ * devassist watch --ai --compare # Watch + multi-engine comparison
13
+ * devassist watch --ai --compare --fix # Full auto: detect + AI + fix
11
14
  */
12
15
 
13
16
  const fs = require('fs');
14
17
  const path = require('path');
15
- const { execSync } = require('child_process');
16
18
 
17
19
  // Core
18
20
  const { engine, RuleEngine } = require('../../core/rule-engine');
@@ -25,14 +27,29 @@ const { ArchRiskAssessor } = require('../../modules/dev-time/arch-risk-assessor'
25
27
  // Shared utilities
26
28
  const { loadConfig } = require('../shared/config-loader');
27
29
  const { filterIgnored } = require('../shared/inline-ignore');
30
+ const { enrichWithContext } = require('../shared/code-context');
31
+
32
+ // AI
33
+ const ai = require('../../ai/adapter');
34
+ const qwen = require('../../ai/providers/qwen');
35
+ const openai = require('../../ai/providers/openai');
36
+ const claude = require('../../ai/providers/claude');
37
+ const hunyuan = require('../../ai/providers/hunyuan');
38
+
39
+ // Register AI providers
40
+ ai.registerProvider('qwen', qwen);
41
+ ai.registerProvider('openai', openai);
42
+ ai.registerProvider('claude', claude);
43
+ ai.registerProvider('hunyuan', hunyuan);
28
44
 
29
45
  const CODE_EXTENSIONS = /\.(js|ts|jsx|tsx|py|java)$/;
30
46
  const SKIP_DIRS = ['node_modules', '.git', '.devassist', 'dist', 'build', 'vendor', '.gradle', '__pycache__'];
31
47
 
32
48
  module.exports = async function watch(projectRoot, args, ui) {
33
- const { printHeader, printFinding, c } = ui;
49
+ const { printHeader, printFinding, printStats, c } = ui;
34
50
  const useAI = args.includes('--ai');
35
51
  const useCompare = args.includes('--compare');
52
+ const autoFix = args.includes('--fix');
36
53
  const watchDir = args.includes('--dir') ? args[args.indexOf('--dir') + 1] : null;
37
54
 
38
55
  printHeader('监控模式');
@@ -40,6 +57,7 @@ module.exports = async function watch(projectRoot, args, ui) {
40
57
  console.log(` ${c.gray('监控目录:')} ${watchDir || '整个项目'}`);
41
58
  console.log(` ${c.gray('AI 分析:')} ${useAI ? '已启用' : '已禁用'}`);
42
59
  console.log(` ${c.gray('多引擎对比:')} ${useCompare ? '已启用' : '已禁用'}`);
60
+ console.log(` ${c.gray('自动修复:')} ${autoFix ? '已启用' : '已禁用'}`);
43
61
  console.log(` ${c.gray('按 Ctrl+C 停止')}\n`);
44
62
 
45
63
  // Initialize convention store
@@ -62,9 +80,28 @@ module.exports = async function watch(projectRoot, args, ui) {
62
80
  }
63
81
  });
64
82
 
83
+ // Load AI config (global or project-level)
84
+ let aiConfig = null;
85
+ let readyEngines = [];
86
+ if (useAI) {
87
+ const { loadAIConfig } = require('./ai');
88
+ aiConfig = loadAIConfig(projectRoot);
89
+ if (aiConfig && aiConfig.engines) {
90
+ readyEngines = aiConfig.engines.filter(e => e.enabled && e.apiKey);
91
+ }
92
+ if (readyEngines.length === 0) {
93
+ console.log(` ${c.yellow('警告:')} 未检测到可用的 AI 引擎,将仅使用规则引擎\n`);
94
+ } else if (useCompare && readyEngines.length < 2) {
95
+ console.log(` ${c.yellow('提示:')} 对比模式需要 2+ 引擎,当前仅 ${readyEngines.length} 个,将使用单引擎\n`);
96
+ } else {
97
+ console.log(` ${c.green('AI 引擎就绪:')} ${readyEngines.map(e => e.name || e.provider).join(' + ')}\n`);
98
+ }
99
+ }
100
+
65
101
  // Track watchers for cleanup
66
102
  const watchers = [];
67
103
  const debounceTimers = new Map();
104
+ const analyzingFiles = new Set(); // Lock: prevent overlapping AI analysis on same file
68
105
 
69
106
  // Watch a single file
70
107
  function watchFile(filePath) {
@@ -72,14 +109,17 @@ module.exports = async function watch(projectRoot, args, ui) {
72
109
  const watcher = fs.watch(filePath, { persistent: true }, (eventType) => {
73
110
  if (eventType !== 'change') return;
74
111
 
75
- // Debounce: wait 300ms after last change before checking
112
+ // Debounce: wait 500ms after last change before checking (longer with AI)
113
+ const debounceMs = useAI ? 500 : 300;
76
114
  if (debounceTimers.has(filePath)) {
77
115
  clearTimeout(debounceTimers.get(filePath));
78
116
  }
79
117
  debounceTimers.set(filePath, setTimeout(() => {
80
118
  debounceTimers.delete(filePath);
81
- checkFile(filePath);
82
- }, 300));
119
+ checkFile(filePath).catch(err => {
120
+ console.log(` ${c.red('检查出错:')} ${err.message}`);
121
+ });
122
+ }, debounceMs));
83
123
  });
84
124
  watcher.on('error', () => {});
85
125
  watchers.push(watcher);
@@ -102,61 +142,165 @@ module.exports = async function watch(projectRoot, args, ui) {
102
142
  }
103
143
  }
104
144
 
105
- // Check a single file
106
- function checkFile(filePath) {
107
- const relPath = path.relative(projectRoot, filePath);
108
- const timestamp = new Date().toLocaleTimeString();
145
+ // Check a single file — async for AI analysis
146
+ async function checkFile(filePath) {
147
+ // Skip if already analyzing this file (AI takes time)
148
+ if (analyzingFiles.has(filePath)) return;
149
+ analyzingFiles.add(filePath);
109
150
 
110
- let content = '';
111
- try { content = fs.readFileSync(filePath, 'utf-8'); } catch (_) { return; }
151
+ try {
152
+ const relPath = path.relative(projectRoot, filePath);
153
+ const timestamp = new Date().toLocaleTimeString();
112
154
 
113
- // Quick skip for HTML without script tags
114
- if (/\.html?$/.test(filePath) && !/<script[\s>]/i.test(content)) return;
155
+ let content = '';
156
+ try { content = fs.readFileSync(filePath, 'utf-8'); } catch (_) { return; }
115
157
 
116
- const ctx = {
117
- files: [{ path: relPath, content, fullPath: filePath }],
118
- projectRoot,
119
- config: loadConfig(projectRoot),
120
- };
158
+ // Quick skip for HTML without script tags
159
+ if (/\.html?$/.test(filePath) && !/<script[\s>]/i.test(content)) return;
121
160
 
122
- const { findings: ruleFindings } = engine.run(ctx);
123
- const archFindings = archAssessor.analyzeFile(relPath, content);
124
- let allFindings = [...ruleFindings, ...archFindings];
161
+ const ctx = {
162
+ files: [{ path: relPath, content, fullPath: filePath }],
163
+ projectRoot,
164
+ config: loadConfig(projectRoot),
165
+ };
125
166
 
126
- // Apply inline ignores
127
- allFindings = filterIgnored(allFindings, [{ path: relPath, content }]);
167
+ const { findings: ruleFindings } = engine.run(ctx);
168
+ const archFindings = archAssessor.analyzeFile(relPath, content);
169
+ let allFindings = [...ruleFindings, ...archFindings];
128
170
 
129
- // Save findings for ai --analyze
130
- saveWatchFindings(projectRoot, allFindings);
171
+ // Apply inline ignores
172
+ allFindings = filterIgnored(allFindings, [{ path: relPath, content }]);
131
173
 
132
- // Print header
133
- console.log(`\n ${c.gray(`[${timestamp}]`)} ${c.bold(relPath)}`);
174
+ // Print header
175
+ console.log(`\n ${c.gray(`[${timestamp}]`)} ${c.bold(relPath)}`);
134
176
 
135
- if (allFindings.length === 0) {
136
- console.log(` ${c.green('OK')} — 未发现问题`);
137
- return;
138
- }
177
+ if (allFindings.length === 0) {
178
+ console.log(` ${c.green('OK')} — 未发现问题`);
179
+ return;
180
+ }
139
181
 
140
- // Print findings
141
- const blocks = allFindings.filter(f => f.severity === 'block');
142
- const warns = allFindings.filter(f => f.severity === 'warn');
143
- const infos = allFindings.filter(f => f.severity === 'info');
144
-
145
- for (const f of allFindings) {
146
- const icon = f.severity === 'block' ? c.red('[阻断]') :
147
- f.severity === 'warn' ? c.yellow('[警告]') :
148
- c.gray('[提示]');
149
- console.log(` ${icon} ${f.message}`);
150
- if (f.line) console.log(` ${c.gray(`第 ${f.line} 行`)}`);
151
- if (f.suggestion) console.log(` ${c.gray(f.suggestion)}`);
152
- }
182
+ // --- AI Analysis (if enabled and there are warn/block findings) ---
183
+ let finalFindings = allFindings;
184
+ const suspectFindings = allFindings.filter(f => f.severity === 'block' || f.severity === 'warn');
185
+
186
+ if (useAI && readyEngines.length > 0 && suspectFindings.length > 0) {
187
+ console.log(` ${c.cyan('AI 分析中...')} (${suspectFindings.length} 个疑似问题)`);
188
+
189
+ // Enrich with code context
190
+ const fileContents = [{ path: relPath, content }];
191
+ finalFindings = enrichWithContext(allFindings, fileContents);
192
+
193
+ const aiContext = {
194
+ conventions: conventionStore.getRelevant({}),
195
+ projectInfo: { root: projectRoot },
196
+ };
197
+
198
+ try {
199
+ if (useCompare && readyEngines.length >= 2) {
200
+ finalFindings = await ai.multiAnalyze(readyEngines, finalFindings, aiContext);
201
+ } else {
202
+ finalFindings = await ai.analyze(readyEngines[0], finalFindings, aiContext);
203
+ }
204
+ } catch (err) {
205
+ console.log(` ${c.yellow('AI 分析失败:')} ${err.message}`);
206
+ console.log(` ${c.gray('已回退到规则引擎结果')}`);
207
+ }
208
+ }
153
209
 
154
- // Summary
155
- const parts = [];
156
- if (blocks.length > 0) parts.push(c.red(`${blocks.length} 阻断`));
157
- if (warns.length > 0) parts.push(c.yellow(`${warns.length} 警告`));
158
- if (infos.length > 0) parts.push(c.gray(`${infos.length} 提示`));
159
- console.log(` ${c.gray('—')} ${parts.join(c.gray(' · '))}`);
210
+ // --- Print findings ---
211
+ const blocks = finalFindings.filter(f => f.severity === 'block');
212
+ const warns = finalFindings.filter(f => f.severity === 'warn');
213
+ const infos = finalFindings.filter(f => f.severity === 'info');
214
+
215
+ for (const f of finalFindings) {
216
+ const icon = f.severity === 'block' ? c.red('[阻断]') :
217
+ f.severity === 'warn' ? c.yellow('[警告]') :
218
+ c.gray('[提示]');
219
+ console.log(` ${icon} ${f.message}`);
220
+ if (f.line) console.log(` ${c.gray(`第 ${f.line} 行`)}`);
221
+ if (f.suggestion) console.log(` ${c.gray(f.suggestion)}`);
222
+
223
+ // Print AI analysis if available
224
+ if (f.aiAnalysis) {
225
+ const a = f.aiAnalysis;
226
+ if (a.falsePositive) {
227
+ console.log(` ${c.gray('AI 判定:误报 — ' + (a.falsePositiveReason || '不适用'))}`);
228
+ } else {
229
+ const rootCause = a.rootCause || a.root_cause || '';
230
+ const fixSuggestion = a.fixSuggestion || a.fix_suggestion || '';
231
+ const confidence = a.confidence || '';
232
+ if (rootCause) console.log(` ${c.cyan('AI 根因:')} ${rootCause}`);
233
+ if (fixSuggestion) console.log(` ${c.cyan('AI 建议:')} ${fixSuggestion}`);
234
+ if (confidence) console.log(` ${c.gray(`置信度:${confidence}`)}`);
235
+ }
236
+ }
237
+
238
+ // Print multi-engine comparison
239
+ if (f.aiAnalyses && f.aiAnalyses.length >= 2) {
240
+ for (const ea of f.aiAnalyses) {
241
+ if (ea.analysis && !ea.failed) {
242
+ const engineName = ea.engineName || ea.provider;
243
+ const a = ea.analysis;
244
+ if (a.falsePositive) {
245
+ console.log(` ${c.gray(`[${engineName}] 误报 — ${a.falsePositiveReason || ''}`)}`);
246
+ } else {
247
+ const rootCause = a.rootCause || a.root_cause || '';
248
+ if (rootCause) console.log(` ${c.cyan(`[${engineName}]`)} ${rootCause}`);
249
+ }
250
+ } else if (ea.failed) {
251
+ console.log(` ${c.gray(`[${ea.engineName || ea.provider}] 分析失败`)}`);
252
+ }
253
+ }
254
+ // Agreement indicator
255
+ if (f.aiAnalyses.filter(ea => ea.analysis && !ea.failed && !ea.analysis.falsePositive).length === 0
256
+ && f.aiAnalyses.filter(ea => ea.analysis && !ea.failed && ea.analysis.falsePositive).length >= 2) {
257
+ console.log(` ${c.green('引擎一致:均判定为误报')}`);
258
+ }
259
+ }
260
+ }
261
+
262
+ // --- Summary ---
263
+ const parts = [];
264
+ if (blocks.length > 0) parts.push(c.red(`${blocks.length} 阻断`));
265
+ if (warns.length > 0) parts.push(c.yellow(`${warns.length} 警告`));
266
+ if (infos.length > 0) parts.push(c.gray(`${infos.length} 提示`));
267
+ console.log(` ${c.gray('—')} ${parts.join(c.gray(' · '))}`);
268
+
269
+ // --- Auto Fix (if enabled) ---
270
+ let fixResult = null;
271
+ if (autoFix && suspectFindings.length > 0) {
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
+ }
279
+ }
280
+ }
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
+
301
+ } finally {
302
+ analyzingFiles.delete(filePath);
303
+ }
160
304
  }
161
305
 
162
306
  // Start watching
@@ -199,19 +343,129 @@ module.exports = async function watch(projectRoot, args, ui) {
199
343
  });
200
344
  };
201
345
 
202
- function saveWatchFindings(projectRoot, findings) {
346
+ /**
347
+ * Auto-fix a single file in-place (safe fixes only).
348
+ * Returns the number of fixes applied.
349
+ */
350
+ async function autoFixFile(filePath, content, projectRoot, c) {
351
+ const lines = content.split('\n');
352
+ let fixCount = 0;
353
+ const changes = [];
354
+
355
+ for (let i = 0; i < lines.length; i++) {
356
+ const line = lines[i];
357
+ if (/^\s*(\/\/|\/\*|\*)/.test(line)) continue;
358
+
359
+ // Fix 1: fetch() without .catch()
360
+ if (/\bfetch\s*\(/.test(line)) {
361
+ const context = lines.slice(i, Math.min(i + 5, lines.length)).join('\n');
362
+ if (/\.catch\s*\(/.test(context)) continue;
363
+ const hasAwait = /\bawait\b/.test(line);
364
+ const hasTryCatch = /try\s*\{/.test(lines.slice(Math.max(0, i - 5), i + 1).join('\n'));
365
+ if (hasAwait && hasTryCatch) continue;
366
+
367
+ const trimmed = line.trimEnd();
368
+ if (trimmed.endsWith(')') && !trimmed.endsWith('.catch()')) {
369
+ const fixedLine = trimmed.replace(/\)\s*$/, ").catch(err => console.error('fetch error:', err))");
370
+ changes.push({ line: i + 1, type: 'fetch-catch', before: line, after: fixedLine });
371
+ fixCount++;
372
+ continue;
373
+ }
374
+ }
375
+
376
+ // Fix 2: Add await to async calls (fetch, axios, etc.)
377
+ if (!/\bawait\b/.test(line)) {
378
+ const asyncMatch = line.match(/(?<![\w.])(fetch|axios\.(get|post|put|delete|patch)|\.save\(|\.findOne\(|\.findById\(|\.create\()\s*/);
379
+ if (asyncMatch) {
380
+ const trimmed = line.trim();
381
+ if (/^\s*(const|let|var|if|while|return|throw)/.test(trimmed)) continue;
382
+ if (/^\s*(async\s+)?function\s+/.test(line)) continue;
383
+ if (/\.then\s*\(|\.catch\s*\(|\.finally\s*\(/.test(line)) continue;
384
+
385
+ const fixedLine = line.replace(
386
+ new RegExp('(?<![\\w.])(' + asyncMatch[1] + '\\w+)\\s*\\('),
387
+ 'await $1('
388
+ );
389
+ if (fixedLine !== line) {
390
+ changes.push({ line: i + 1, type: 'add-await', before: line, after: fixedLine });
391
+ fixCount++;
392
+ continue;
393
+ }
394
+ }
395
+ }
396
+ }
397
+
398
+ if (fixCount === 0) return { count: 0, details: [] };
399
+
400
+ // Apply changes
401
+ for (const ch of changes) {
402
+ lines[ch.line - 1] = ch.after;
403
+ }
404
+ const newContent = lines.join('\n');
405
+
406
+ // Backup original
407
+ const backupDir = path.join(projectRoot, '.devassist', 'backups', `watch-fix-${Date.now()}`);
408
+ if (!fs.existsSync(backupDir)) fs.mkdirSync(backupDir, { recursive: true });
409
+ const backupPath = path.join(backupDir, path.basename(filePath));
410
+ fs.writeFileSync(backupPath, content, 'utf-8');
411
+
412
+ // Write fixed content
413
+ fs.writeFileSync(filePath, newContent, 'utf-8');
414
+
415
+ return { count: fixCount, details: changes, backupPath };
416
+ }
417
+
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) {
203
425
  const reportDir = path.join(projectRoot, '.devassist', 'reports');
204
426
  if (!fs.existsSync(reportDir)) fs.mkdirSync(reportDir, { recursive: true });
205
- const reportPath = path.join(reportDir, 'latest.json');
206
- fs.writeFileSync(reportPath, JSON.stringify({
207
- generatedAt: new Date().toISOString(),
208
- source: 'watch',
209
- findings,
210
- stats: {
211
- total: findings.length,
212
- blocks: findings.filter(f => f.severity === 'block').length,
213
- warnings: findings.filter(f => f.severity === 'warn').length,
214
- infos: findings.filter(f => f.severity === 'info').length,
215
- },
216
- }, 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 (_) {}
217
471
  }