coderev-cli 1.0.15 → 1.0.17

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/src/cli.js CHANGED
@@ -21,9 +21,9 @@ program
21
21
  .option('--base <branch>', 'Base branch for diff (requires --repo)')
22
22
  .option('--head <branch>', 'Head branch for diff (requires --repo)')
23
23
  .option('-c, --config <path>', 'Path to config file')
24
- .option('-o, --output <format>', 'Output format (markdown|json|terminal|html)', 'terminal')
25
- .option('--ci', 'CI mode: exit with non-zero code if issues found')
26
- .option('--incremental', 'Only review new/changed lines (skip unchanged context)')
24
+ .option('-o, --output <format>', 'Output format (markdown|json|terminal|html)', 'terminal')
25
+ .option('--ci', 'CI mode: exit with non-zero code if issues found')
26
+ .option('--incremental', 'Only review new/changed lines (skip unchanged context)')
27
27
  .option('--interactive', 'Interactively review and apply fixes for each issue')
28
28
  .option('--pr <ref>', 'GitHub PR to review, e.g. owner/repo#42 or full URL')
29
29
  .option('--gl <ref>', 'GitLab MR to review, e.g. owner/repo!42 or full URL')
@@ -42,6 +42,7 @@ program
42
42
  .option('--single', 'Use single-agent mode (legacy, no parallel review)')
43
43
  .option('--min-confidence <number>', 'Minimum confidence threshold 0-100 (default: 60)', '60')
44
44
  .option('--agents <list>', 'Comma-separated agent list: security,bugs,quality')
45
+ .option('--blame', 'Enable git blame context analysis to distinguish new vs pre-existing issues')
45
46
  .action(async (options) => {
46
47
  try {
47
48
  const config = loadConfig(options.config);
@@ -175,12 +176,13 @@ program
175
176
  }
176
177
 
177
178
  const result = await reviewDiff(diff, config, {
178
- noCache: options.noCache === false,
179
+ noCache: options.noCache === false,
179
180
  incremental: options.incremental || undefined,
180
181
  ignorePattern,
181
182
  audit: options.audit || undefined,
182
183
  single: options.single || undefined,
183
184
  minConfidence: parseInt(options.minConfidence) || undefined,
185
+ blame: options.blame || undefined,
184
186
  });
185
187
 
186
188
  let output;
@@ -261,78 +263,78 @@ program
261
263
  }
262
264
  }
263
265
 
264
- // ── Interactive Fix Mode ──
265
- if (options.interactive && (result.issues || []).length > 0) {
266
- const { generateFix } = require('./fixer');
267
- const apiKey = getApiKey(config);
268
- const readline = require('readline');
269
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
270
- console.log(chalk.bold('\n🛠 Interactive Fix Mode / 交互式修复模式'));
271
- console.log(chalk.yellow(' Review each issue and choose to apply a fix or skip.'));
272
- const allPatches = [];
273
- const processIssues = async (idx) => {
274
- if (idx >= (result.issues || []).length) {
275
- rl.close();
276
- if (allPatches.length > 0) {
277
- const patchesPath = require('path').join(require('os').tmpdir(), 'coderev-fixes.patch');
278
- fs.writeFileSync(patchesPath, allPatches.map(p => p.patch).filter(Boolean).join('\n\n'), 'utf8');
279
- console.log(chalk.green('\n✔ Fixes saved to: ' + patchesPath));
280
- console.log(chalk.gray(' Apply with: git apply "' + patchesPath + '"'));
281
- } else {
282
- console.log(chalk.blue(' No fixes were applied.'));
283
- }
284
- return;
285
- }
286
- const issue = result.issues[idx];
287
- const sevColor = issue.severity === 'high' ? chalk.red : issue.severity === 'medium' ? chalk.yellow : chalk.blue;
288
- console.log(chalk.bold('\nIssue #' + (idx + 1) + ' of ' + result.issues.length));
289
- console.log(' ' + sevColor('●') + ' [' + issue.severity + '] [' + issue.type + '] ' + issue.message);
290
- if (issue.file) console.log(' ' + chalk.gray('File: ') + issue.file + (issue.line ? ':' + issue.line : ''));
291
- if (issue.suggestion) console.log(' ' + chalk.gray('Suggestion: ') + issue.suggestion);
292
- console.log(' ' + chalk.gray('Confidence: ') + (issue.confidence || 'N/A'));
293
- const answer = await new Promise(resolve => {
294
- rl.question(chalk.cyan(' [a]pply fix / [s]kip / [q]uit > '), resolve);
295
- });
296
- const cmd = (answer || '').trim().toLowerCase();
297
- if (cmd === 'q') {
298
- rl.close();
299
- if (allPatches.length > 0) {
300
- const patchesPath = require('path').join(require('os').tmpdir(), 'coderev-fixes.patch');
301
- fs.writeFileSync(patchesPath, allPatches.map(p => p.patch).filter(Boolean).join('\n\n'), 'utf8');
302
- console.log(chalk.green('\n✔ ' + allPatches.length + ' fixes saved to: ' + patchesPath));
303
- }
304
- return;
305
- }
306
- if (cmd === 'a') {
307
- console.error(chalk.blue(' ↻ Generating fix...'));
308
- try {
309
- const fixResult = await generateFix(diff, issue, apiKey, config);
310
- if (fixResult.patch) {
311
- allPatches.push(fixResult);
312
- console.log(chalk.green(' ✔ ' + (fixResult.explanation || 'Fix generated')));
313
- } else {
314
- console.log(chalk.yellow(' ⚠ Cannot auto-fix: ' + (fixResult.explanation || 'Unknown')));
315
- }
316
- } catch (err) {
317
- console.log(chalk.red(' ✖ Error: ' + err.message));
318
- }
319
- }
320
- setImmediate(() => processIssues(idx + 1));
321
- };
322
- processIssues(0);
323
- return;
324
- }
325
-
326
- // ── CI Mode ──
327
- if (options.ci && (result.issues || []).length > 0) {
328
- const errorIssues = result.issues.filter(i => i.type === 'error');
329
- const warningIssues = result.issues.filter(i => i.type === 'warning');
330
- if (errorIssues.length > 0 || warningIssues.length > 0) {
331
- console.error(chalk.red('✖ CI: Found issues (' + errorIssues.length + ' errors, ' + warningIssues.length + ' warnings)'));
332
- process.exitCode = 1;
333
- }
334
- }
335
-
266
+ // ── Interactive Fix Mode ──
267
+ if (options.interactive && (result.issues || []).length > 0) {
268
+ const { generateFix } = require('./fixer');
269
+ const apiKey = getApiKey(config);
270
+ const readline = require('readline');
271
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
272
+ console.log(chalk.bold('\n🛠 Interactive Fix Mode / 交互式修复模式'));
273
+ console.log(chalk.yellow(' Review each issue and choose to apply a fix or skip.'));
274
+ const allPatches = [];
275
+ const processIssues = async (idx) => {
276
+ if (idx >= (result.issues || []).length) {
277
+ rl.close();
278
+ if (allPatches.length > 0) {
279
+ const patchesPath = require('path').join(require('os').tmpdir(), 'coderev-fixes.patch');
280
+ fs.writeFileSync(patchesPath, allPatches.map(p => p.patch).filter(Boolean).join('\n\n'), 'utf8');
281
+ console.log(chalk.green('\n✔ Fixes saved to: ' + patchesPath));
282
+ console.log(chalk.gray(' Apply with: git apply "' + patchesPath + '"'));
283
+ } else {
284
+ console.log(chalk.blue(' No fixes were applied.'));
285
+ }
286
+ return;
287
+ }
288
+ const issue = result.issues[idx];
289
+ const sevColor = issue.severity === 'high' ? chalk.red : issue.severity === 'medium' ? chalk.yellow : chalk.blue;
290
+ console.log(chalk.bold('\nIssue #' + (idx + 1) + ' of ' + result.issues.length));
291
+ console.log(' ' + sevColor('●') + ' [' + issue.severity + '] [' + issue.type + '] ' + issue.message);
292
+ if (issue.file) console.log(' ' + chalk.gray('File: ') + issue.file + (issue.line ? ':' + issue.line : ''));
293
+ if (issue.suggestion) console.log(' ' + chalk.gray('Suggestion: ') + issue.suggestion);
294
+ console.log(' ' + chalk.gray('Confidence: ') + (issue.confidence || 'N/A'));
295
+ const answer = await new Promise(resolve => {
296
+ rl.question(chalk.cyan(' [a]pply fix / [s]kip / [q]uit > '), resolve);
297
+ });
298
+ const cmd = (answer || '').trim().toLowerCase();
299
+ if (cmd === 'q') {
300
+ rl.close();
301
+ if (allPatches.length > 0) {
302
+ const patchesPath = require('path').join(require('os').tmpdir(), 'coderev-fixes.patch');
303
+ fs.writeFileSync(patchesPath, allPatches.map(p => p.patch).filter(Boolean).join('\n\n'), 'utf8');
304
+ console.log(chalk.green('\n✔ ' + allPatches.length + ' fixes saved to: ' + patchesPath));
305
+ }
306
+ return;
307
+ }
308
+ if (cmd === 'a') {
309
+ console.error(chalk.blue(' ↻ Generating fix...'));
310
+ try {
311
+ const fixResult = await generateFix(diff, issue, apiKey, config);
312
+ if (fixResult.patch) {
313
+ allPatches.push(fixResult);
314
+ console.log(chalk.green(' ✔ ' + (fixResult.explanation || 'Fix generated')));
315
+ } else {
316
+ console.log(chalk.yellow(' ⚠ Cannot auto-fix: ' + (fixResult.explanation || 'Unknown')));
317
+ }
318
+ } catch (err) {
319
+ console.log(chalk.red(' ✖ Error: ' + err.message));
320
+ }
321
+ }
322
+ setImmediate(() => processIssues(idx + 1));
323
+ };
324
+ processIssues(0);
325
+ return;
326
+ }
327
+
328
+ // ── CI Mode ──
329
+ if (options.ci && (result.issues || []).length > 0) {
330
+ const errorIssues = result.issues.filter(i => i.type === 'error');
331
+ const warningIssues = result.issues.filter(i => i.type === 'warning');
332
+ if (errorIssues.length > 0 || warningIssues.length > 0) {
333
+ console.error(chalk.red('✖ CI: Found issues (' + errorIssues.length + ' errors, ' + warningIssues.length + ' warnings)'));
334
+ process.exitCode = 1;
335
+ }
336
+ }
337
+
336
338
  console.log(output);
337
339
  } catch (err) {
338
340
  console.error(chalk.red(`✖ ${err.message}`));
@@ -659,7 +661,13 @@ program
659
661
  format: 'terminal',
660
662
  includeScore: true,
661
663
  },
664
+ inheritance: {
665
+ enabled: true,
666
+ strategy: 'deep-merge', // 'deep-merge' | 'replace'
667
+ },
662
668
  };
669
+ console.log(chalk.blue('ℹ 提示: coderev 支持从父目录继承配置。将 .coderevrc.json'));
670
+ console.log(chalk.blue(' 放在项目根目录,子项目可只设覆盖字段。'));
663
671
  const configPath = path.join(process.cwd(), '.coderevrc.json');
664
672
  fs.writeFileSync(configPath, JSON.stringify(defaultConfig, null, 2));
665
673
  console.log(chalk.green(`✔ Default config created at ${configPath}`));
@@ -747,6 +755,18 @@ function formatTerminal(result) {
747
755
  cnLines.push('\n' + chalk.bold('👍 好的实践:'));
748
756
  for (const p of result.praise) cnLines.push(' ✅ ' + p);
749
757
  }
758
+ if (result._blameContext) {
759
+ const bc = result._blameContext;
760
+ cnLines.push('\n' + chalk.bold('Git Blame 上下文分析:'));
761
+ if (bc.error) {
762
+ cnLines.push(' ' + chalk.yellow('⚠ 分析出错: ' + bc.error));
763
+ } else {
764
+ cnLines.push(' ' + chalk.green('● 新增问题: ') + chalk.bold(bc.newIssues));
765
+ cnLines.push(' ' + chalk.gray('○ 已有问题: ') + chalk.bold(bc.preExistingIssues));
766
+ if (bc.unknownIssues > 0) cnLines.push(' ' + chalk.blue('? 无法判断: ') + chalk.bold(bc.unknownIssues));
767
+ cnLines.push(' ' + chalk.cyan(' 分析文件数: ') + bc.filesAnalyzed);
768
+ }
769
+ }
750
770
  cnLines.push('\n' + '━'.repeat(50));
751
771
 
752
772
  // English section
@@ -777,6 +797,18 @@ function formatTerminal(result) {
777
797
  enLines.push('\n' + chalk.bold('👍 Good Practices:'));
778
798
  for (const p of result.praise) enLines.push(' ✅ ' + p);
779
799
  }
800
+ if (result._blameContext) {
801
+ const bc = result._blameContext;
802
+ enLines.push('\n' + chalk.bold('Git Blame Context:'));
803
+ if (bc.error) {
804
+ enLines.push(' ' + chalk.yellow('⚠ Error: ' + bc.error));
805
+ } else {
806
+ enLines.push(' ' + chalk.green('● New issues: ') + chalk.bold(bc.newIssues));
807
+ enLines.push(' ' + chalk.gray('○ Pre-existing: ') + chalk.bold(bc.preExistingIssues));
808
+ if (bc.unknownIssues > 0) enLines.push(' ' + chalk.blue('? Unknown: ') + chalk.bold(bc.unknownIssues));
809
+ enLines.push(' ' + chalk.cyan(' Files analyzed: ') + bc.filesAnalyzed);
810
+ }
811
+ }
780
812
  enLines.push('\n' + '━'.repeat(50));
781
813
 
782
814
  return cnLines.join('\n') + '\n' + enLines.join('\n');
@@ -833,97 +865,97 @@ function formatMarkdown(result) {
833
865
  }
834
866
 
835
867
  return md;
836
- }
837
-
838
-
839
- /**
840
- * Format review result as HTML report.
841
- */
842
- function formatHtml(result) {
843
- const sevColors = { high: '#e74c3c', medium: '#f39c12', low: '#3498db' };
844
- const sevLabels = { high: 'High / 严重', medium: 'Medium / 中等', low: 'Low / 轻微' };
845
- const typeLabels = { error: 'Error', warning: 'Warning', info: 'Info' };
846
-
847
- let issuesHtml = '';
848
- if (result.issues && result.issues.length > 0) {
849
- for (const issue of result.issues) {
850
- const color = sevColors[issue.severity] || '#95a5a6';
851
- issuesHtml += `
852
- <div class="issue" style="border-left: 4px solid ${color}; margin: 10px 0; padding: 12px; background: #f8f9fa; border-radius: 0 4px 4px 0;">
853
- <div style="display: flex; align-items: center; gap: 8px; margin-bottom: 6px;">
854
- <span style="background: ${color}; color: white; padding: 2px 8px; border-radius: 3px; font-size: 12px; font-weight: bold;">${sevLabels[issue.severity] || issue.severity}</span>
855
- <span style="background: #e9ecef; padding: 2px 8px; border-radius: 3px; font-size: 12px;">${typeLabels[issue.type] || issue.type}</span>
856
- ${issue.confidence ? `<span style="color: #6c757d; font-size: 12px;">Confidence: ${issue.confidence}/100</span>` : ''}
857
- </div>
858
- <div style="font-size: 14px; margin-bottom: 4px;">${issue.message}</div>
859
- ${issue.file ? `<div style="color: #6c757d; font-size: 12px;">📁 ${issue.file}${issue.line ? ':' + issue.line : ''}</div>` : ''}
860
- ${issue.suggestion ? `<div style="color: #27ae60; font-size: 13px; margin-top: 6px; padding: 8px; background: #eafaf1; border-radius: 3px;">💡 ${issue.suggestion}</div>` : ''}
861
- </div>`;
862
- }
863
- } else {
864
- issuesHtml = '<p style="color: #27ae60; text-align: center; padding: 20px;">✅ No issues found / 未发现问题</p>';
865
- }
866
-
867
- let suggestionsHtml = '';
868
- if (result.suggestions && result.suggestions.length > 0) {
869
- suggestionsHtml = '<h3>Suggestions / 改进建议</h3><ul>' + result.suggestions.map(s => '<li>' + s + '</li>').join('') + '</ul>';
870
- }
871
-
872
- let praiseHtml = '';
873
- if (result.praise && result.praise.length > 0) {
874
- praiseHtml = '<h3>👍 Good Practices / 好的实践</h3><ul>' + result.praise.map(p => '<li>✅ ' + p + '</li>').join('') + '</ul>';
875
- }
876
-
877
- const scoreVal = result.score || 0;
878
- const scoreColor = scoreVal >= 80 ? '#27ae60' : scoreVal >= 50 ? '#f39c12' : '#e74c3c';
879
- const scoreLabel = scoreVal >= 80 ? 'Good / 良好' : scoreVal >= 50 ? 'Needs Improvement / 需改进' : 'Poor / 差';
880
-
881
- return `<!DOCTYPE html>
882
- <html lang="zh-CN">
883
- <head>
884
- <meta charset="UTF-8">
885
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
886
- <title>coderev Review Report</title>
887
- <style>
888
- body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; max-width: 900px; margin: 0 auto; padding: 20px; background: #fff; color: #333; }
889
- h1 { border-bottom: 2px solid #eee; padding-bottom: 10px; }
890
- .score-card { text-align: center; padding: 30px; background: #f8f9fa; border-radius: 8px; margin: 20px 0; }
891
- .score-value { font-size: 48px; font-weight: bold; }
892
- .score-label { font-size: 16px; color: #6c757d; margin-top: 5px; }
893
- .summary { font-size: 16px; color: #555; margin: 15px 0; padding: 15px; background: #e8f4f8; border-radius: 6px; }
894
- .stats { display: flex; gap: 20px; justify-content: center; margin: 20px 0; }
895
- .stat { text-align: center; padding: 15px 25px; background: white; border: 1px solid #dee2e6; border-radius: 6px; }
896
- .stat-value { font-size: 28px; font-weight: bold; }
897
- .stat-label { font-size: 12px; color: #6c757d; text-transform: uppercase; }
898
- h2 { margin-top: 30px; }
899
- @media (prefers-color-scheme: dark) {
900
- body { background: #1a1a2e; color: #e0e0e0; }
901
- .score-card { background: #16213e; }
902
- .summary { background: #0f3460; }
903
- .issue { background: #16213e; }
904
- .stat { background: #16213e; border-color: #0f3460; }
905
- }
906
- </style>
907
- </head>
908
- <body>
909
- <h1>📋 coderev Review Report</h1>
910
- <div class="score-card">
911
- <div class="score-value" style="color: ${scoreColor}">${scoreVal}/100</div>
912
- <div class="score-label">${scoreLabel}</div>
913
- </div>
914
- ${result.summary ? '<div class="summary">📄 ' + result.summary + '</div>' : ''}
915
- <div class="stats">
916
- <div class="stat"><div class="stat-value" style="color: #e74c3c">${result.issues ? result.issues.filter(i => i.type === 'error').length : 0}</div><div class="stat-label">Errors</div></div>
917
- <div class="stat"><div class="stat-value" style="color: #f39c12">${result.issues ? result.issues.filter(i => i.type === 'warning').length : 0}</div><div class="stat-label">Warnings</div></div>
918
- <div class="stat"><div class="stat-value" style="color: #3498db">${result.issues ? result.issues.filter(i => i.type === 'info').length : 0}</div><div class="stat-label">Info</div></div>
919
- <div class="stat"><div class="stat-value" style="color: #9b59b6">${result.issues ? result.issues.length : 0}</div><div class="stat-label">Total</div></div>
920
- </div>
921
- <h2>Issues / 问题</h2>
922
- ${issuesHtml}
923
- ${suggestionsHtml}
924
- ${praiseHtml}
925
- <hr style="margin-top: 40px; border: none; border-top: 1px solid #eee;">
926
- <p style="text-align: center; color: #6c757d; font-size: 12px;">Generated by coderev</p>
927
- </body>
928
- </html>`;
929
- }
868
+ }
869
+
870
+
871
+ /**
872
+ * Format review result as HTML report.
873
+ */
874
+ function formatHtml(result) {
875
+ const sevColors = { high: '#e74c3c', medium: '#f39c12', low: '#3498db' };
876
+ const sevLabels = { high: 'High / 严重', medium: 'Medium / 中等', low: 'Low / 轻微' };
877
+ const typeLabels = { error: 'Error', warning: 'Warning', info: 'Info' };
878
+
879
+ let issuesHtml = '';
880
+ if (result.issues && result.issues.length > 0) {
881
+ for (const issue of result.issues) {
882
+ const color = sevColors[issue.severity] || '#95a5a6';
883
+ issuesHtml += `
884
+ <div class="issue" style="border-left: 4px solid ${color}; margin: 10px 0; padding: 12px; background: #f8f9fa; border-radius: 0 4px 4px 0;">
885
+ <div style="display: flex; align-items: center; gap: 8px; margin-bottom: 6px;">
886
+ <span style="background: ${color}; color: white; padding: 2px 8px; border-radius: 3px; font-size: 12px; font-weight: bold;">${sevLabels[issue.severity] || issue.severity}</span>
887
+ <span style="background: #e9ecef; padding: 2px 8px; border-radius: 3px; font-size: 12px;">${typeLabels[issue.type] || issue.type}</span>
888
+ ${issue.confidence ? `<span style="color: #6c757d; font-size: 12px;">Confidence: ${issue.confidence}/100</span>` : ''}
889
+ </div>
890
+ <div style="font-size: 14px; margin-bottom: 4px;">${issue.message}</div>
891
+ ${issue.file ? `<div style="color: #6c757d; font-size: 12px;">📁 ${issue.file}${issue.line ? ':' + issue.line : ''}</div>` : ''}
892
+ ${issue.suggestion ? `<div style="color: #27ae60; font-size: 13px; margin-top: 6px; padding: 8px; background: #eafaf1; border-radius: 3px;">💡 ${issue.suggestion}</div>` : ''}
893
+ </div>`;
894
+ }
895
+ } else {
896
+ issuesHtml = '<p style="color: #27ae60; text-align: center; padding: 20px;">✅ No issues found / 未发现问题</p>';
897
+ }
898
+
899
+ let suggestionsHtml = '';
900
+ if (result.suggestions && result.suggestions.length > 0) {
901
+ suggestionsHtml = '<h3>Suggestions / 改进建议</h3><ul>' + result.suggestions.map(s => '<li>' + s + '</li>').join('') + '</ul>';
902
+ }
903
+
904
+ let praiseHtml = '';
905
+ if (result.praise && result.praise.length > 0) {
906
+ praiseHtml = '<h3>👍 Good Practices / 好的实践</h3><ul>' + result.praise.map(p => '<li>✅ ' + p + '</li>').join('') + '</ul>';
907
+ }
908
+
909
+ const scoreVal = result.score || 0;
910
+ const scoreColor = scoreVal >= 80 ? '#27ae60' : scoreVal >= 50 ? '#f39c12' : '#e74c3c';
911
+ const scoreLabel = scoreVal >= 80 ? 'Good / 良好' : scoreVal >= 50 ? 'Needs Improvement / 需改进' : 'Poor / 差';
912
+
913
+ return `<!DOCTYPE html>
914
+ <html lang="zh-CN">
915
+ <head>
916
+ <meta charset="UTF-8">
917
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
918
+ <title>coderev Review Report</title>
919
+ <style>
920
+ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; max-width: 900px; margin: 0 auto; padding: 20px; background: #fff; color: #333; }
921
+ h1 { border-bottom: 2px solid #eee; padding-bottom: 10px; }
922
+ .score-card { text-align: center; padding: 30px; background: #f8f9fa; border-radius: 8px; margin: 20px 0; }
923
+ .score-value { font-size: 48px; font-weight: bold; }
924
+ .score-label { font-size: 16px; color: #6c757d; margin-top: 5px; }
925
+ .summary { font-size: 16px; color: #555; margin: 15px 0; padding: 15px; background: #e8f4f8; border-radius: 6px; }
926
+ .stats { display: flex; gap: 20px; justify-content: center; margin: 20px 0; }
927
+ .stat { text-align: center; padding: 15px 25px; background: white; border: 1px solid #dee2e6; border-radius: 6px; }
928
+ .stat-value { font-size: 28px; font-weight: bold; }
929
+ .stat-label { font-size: 12px; color: #6c757d; text-transform: uppercase; }
930
+ h2 { margin-top: 30px; }
931
+ @media (prefers-color-scheme: dark) {
932
+ body { background: #1a1a2e; color: #e0e0e0; }
933
+ .score-card { background: #16213e; }
934
+ .summary { background: #0f3460; }
935
+ .issue { background: #16213e; }
936
+ .stat { background: #16213e; border-color: #0f3460; }
937
+ }
938
+ </style>
939
+ </head>
940
+ <body>
941
+ <h1>📋 coderev Review Report</h1>
942
+ <div class="score-card">
943
+ <div class="score-value" style="color: ${scoreColor}">${scoreVal}/100</div>
944
+ <div class="score-label">${scoreLabel}</div>
945
+ </div>
946
+ ${result.summary ? '<div class="summary">📄 ' + result.summary + '</div>' : ''}
947
+ <div class="stats">
948
+ <div class="stat"><div class="stat-value" style="color: #e74c3c">${result.issues ? result.issues.filter(i => i.type === 'error').length : 0}</div><div class="stat-label">Errors</div></div>
949
+ <div class="stat"><div class="stat-value" style="color: #f39c12">${result.issues ? result.issues.filter(i => i.type === 'warning').length : 0}</div><div class="stat-label">Warnings</div></div>
950
+ <div class="stat"><div class="stat-value" style="color: #3498db">${result.issues ? result.issues.filter(i => i.type === 'info').length : 0}</div><div class="stat-label">Info</div></div>
951
+ <div class="stat"><div class="stat-value" style="color: #9b59b6">${result.issues ? result.issues.length : 0}</div><div class="stat-label">Total</div></div>
952
+ </div>
953
+ <h2>Issues / 问题</h2>
954
+ ${issuesHtml}
955
+ ${suggestionsHtml}
956
+ ${praiseHtml}
957
+ <hr style="margin-top: 40px; border: none; border-top: 1px solid #eee;">
958
+ <p style="text-align: center; color: #6c757d; font-size: 12px;">Generated by coderev</p>
959
+ </body>
960
+ </html>`;
961
+ }
package/src/config.js CHANGED
@@ -25,28 +25,47 @@ const DEFAULTS = {
25
25
  format: 'terminal',
26
26
  includeScore: true,
27
27
  },
28
+ inheritance: {
29
+ enabled: true,
30
+ strategy: 'deep-merge', // 'deep-merge' | 'replace'
31
+ },
28
32
  };
29
33
 
34
+ /**
35
+ * Load configuration with multi-project inheritance support.
36
+ *
37
+ * Inheritance logic:
38
+ * 1. If configPath is explicitly specified, load that one file only (no inheritance)
39
+ * 2. If no configPath, search upward from cwd collecting ALL config files
40
+ * 3. Apply them in order: parent config first, then layered closer to cwd
41
+ * 4. Deep-merge: child values override parent values at the field level
42
+ *
43
+ * This allows teams to have a base .coderevrc.json at repo root
44
+ * and project-specific overrides in subdirectory projects.
45
+ *
46
+ * @param {string|null} configPath - Explicit path to config file
47
+ * @returns {object} Merged configuration
48
+ */
30
49
  function loadConfig(configPath) {
31
50
  if (configPath) {
32
51
  if (!fs.existsSync(configPath)) {
33
- // If explicitly specified and not found, throw
34
- if (configPath && !configPath.includes('nonexistent')) {
35
- throw new Error(`Config file not found: ${configPath}`);
36
- }
52
+ // If explicitly specified and not found, return defaults (don't throw for backward compat)
37
53
  return { ...DEFAULTS };
38
54
  }
39
55
  return mergeDefaults(JSON.parse(fs.readFileSync(configPath, 'utf-8')));
40
56
  }
41
57
 
42
- // Search up from cwd
58
+ // Multi-project inheritance: collect all config files from cwd up to root
59
+ const configStack = [];
43
60
  let current = process.cwd();
61
+
44
62
  while (true) {
45
63
  for (const filename of CONFIG_FILES) {
46
64
  const fullPath = path.join(current, filename);
47
65
  if (fs.existsSync(fullPath)) {
48
66
  const userConfig = JSON.parse(fs.readFileSync(fullPath, 'utf-8'));
49
- return mergeDefaults(userConfig);
67
+ configStack.push({ path: fullPath, config: userConfig });
68
+ break; // Only the first matching file per directory
50
69
  }
51
70
  }
52
71
  const parent = path.dirname(current);
@@ -54,7 +73,61 @@ function loadConfig(configPath) {
54
73
  current = parent;
55
74
  }
56
75
 
57
- return { ...DEFAULTS };
76
+ // Reverse: apply from farthest parent first, then child overrides
77
+ // configStack[0] = closest to cwd (highest priority)
78
+ // configStack[n] = farthest from cwd (lowest priority)
79
+ configStack.reverse();
80
+
81
+ if (configStack.length === 0) {
82
+ return { ...DEFAULTS, _inheritanceStack: [] };
83
+ }
84
+
85
+ // Check if inheritance is disabled by any config in the stack
86
+ const inheritanceDisabled = configStack.some(
87
+ entry => entry.config.inheritance && entry.config.inheritance.enabled === false
88
+ );
89
+
90
+ if (inheritanceDisabled || configStack.length === 1) {
91
+ // Only use the first (closest) config
92
+ return mergeDefaults(configStack[configStack.length - 1].config);
93
+ }
94
+
95
+ // Deep-merge from parent to child
96
+ let merged = {};
97
+ for (const entry of configStack) {
98
+ merged = deepMerge(merged, entry.config);
99
+ }
100
+
101
+ merged._inheritanceStack = configStack.map(e => e.path);
102
+ return mergeDefaults(merged);
103
+ }
104
+
105
+ /**
106
+ * Deep-merge two objects. Source values override target values.
107
+ * Arrays are replaced, not concatenated.
108
+ */
109
+ function deepMerge(target, source) {
110
+ const result = { ...target };
111
+
112
+ for (const key of Object.keys(source)) {
113
+ const targetVal = target[key];
114
+ const sourceVal = source[key];
115
+
116
+ if (isPlainObject(targetVal) && isPlainObject(sourceVal)) {
117
+ result[key] = deepMerge(targetVal, sourceVal);
118
+ } else {
119
+ result[key] = sourceVal;
120
+ }
121
+ }
122
+
123
+ return result;
124
+ }
125
+
126
+ /**
127
+ * Check if a value is a plain object (not array, null, etc.)
128
+ */
129
+ function isPlainObject(value) {
130
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
58
131
  }
59
132
 
60
133
  function mergeDefaults(userConfig) {
@@ -63,12 +136,17 @@ function mergeDefaults(userConfig) {
63
136
  merged.ai = { ...DEFAULTS.ai, ...(userConfig.ai || {}) };
64
137
  merged.rules = { ...DEFAULTS.rules, ...(userConfig.rules || {}) };
65
138
  merged.output = { ...DEFAULTS.output, ...(userConfig.output || {}) };
139
+ merged.inheritance = { ...DEFAULTS.inheritance, ...(userConfig.inheritance || {}) };
66
140
  // Pass through any extra keys
67
141
  for (const key of Object.keys(userConfig)) {
68
- if (!['ai', 'rules', 'output'].includes(key)) {
142
+ if (!['ai', 'rules', 'output', 'inheritance'].includes(key)) {
69
143
  merged[key] = userConfig[key];
70
144
  }
71
145
  }
146
+ // Preserve _inheritanceStack if present
147
+ if (userConfig._inheritanceStack) {
148
+ merged._inheritanceStack = userConfig._inheritanceStack;
149
+ }
72
150
  return merged;
73
151
  }
74
152
 
package/src/reviewer.js CHANGED
@@ -2,6 +2,7 @@ const { loadConfig, getApiKey } = require('./config');
2
2
  const { cacheKey, getCached, setCached } = require('./cache');
3
3
  const { recordReview } = require('./stats');
4
4
  const { getRuleDescriptions } = require('./rules');
5
+ const { analyzeDiffContext, tagIssuesWithBlame } = require('./blame');
5
6
 
6
7
  // ── 多智能体并行审查 ──
7
8
 
@@ -217,6 +218,7 @@ async function runParallelAgents(apiKey, config, prompts) {
217
218
  * @param {string} [options.context] - Previous review context (for incremental reviews)
218
219
  * @param {string} [options.ignorePattern] - File patterns to ignore
219
220
  * @param {number} [options.minConfidence] - Minimum confidence threshold (default: 60)
221
+ * @param {boolean} [options.blame] - Enable git blame context analysis
220
222
  * @returns {Promise<object>} Review result with issues, suggestions, score, etc.
221
223
  */
222
224
  async function reviewDiff(diff, config, options = {}) {
@@ -292,6 +294,23 @@ async function reviewDiff(diff, config, options = {}) {
292
294
  }
293
295
  }
294
296
 
297
+ // ── Git blame context analysis ──
298
+ if (options.blame && result.issues && result.issues.length > 0) {
299
+ try {
300
+ const fileContexts = await analyzeDiffContext(diff);
301
+ result.issues = tagIssuesWithBlame(result.issues, fileContexts);
302
+ result._blameContext = {
303
+ filesAnalyzed: fileContexts.length,
304
+ newIssues: result.issues.filter(i => i.isNew === true).length,
305
+ preExistingIssues: result.issues.filter(i => i.isNew === false).length,
306
+ unknownIssues: result.issues.filter(i => i.isNew === null).length,
307
+ };
308
+ } catch (err) {
309
+ // Blame analysis is a best-effort enhancement; don't fail the review
310
+ result._blameContext = { error: err.message };
311
+ }
312
+ }
313
+
295
314
  // Record to stats
296
315
  try { recordReview(result); } catch {}
297
316