coderev-cli 1.0.0
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/.env.example +14 -0
- package/README.md +188 -0
- package/package.json +47 -0
- package/src/bitbucket.js +130 -0
- package/src/cache.js +71 -0
- package/src/cli.js +712 -0
- package/src/coderev.test.js +164 -0
- package/src/config.js +90 -0
- package/src/gitcode.js +99 -0
- package/src/gitee.js +139 -0
- package/src/github.js +365 -0
- package/src/gitlab.js +144 -0
- package/src/index.js +8 -0
- package/src/reviewer.js +546 -0
- package/src/rules.js +195 -0
- package/src/stats.js +126 -0
package/src/reviewer.js
ADDED
|
@@ -0,0 +1,546 @@
|
|
|
1
|
+
const { loadConfig, getApiKey } = require('./config');
|
|
2
|
+
const { cacheKey, getCached, setCached } = require('./cache');
|
|
3
|
+
const { recordReview } = require('./stats');
|
|
4
|
+
const { getRuleDescriptions } = require('./rules');
|
|
5
|
+
|
|
6
|
+
// ── 多智能体并行审查 ──
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Agent roles for parallel review
|
|
10
|
+
*/
|
|
11
|
+
const AGENT_ROLES = [
|
|
12
|
+
{
|
|
13
|
+
name: 'security',
|
|
14
|
+
label: '🔒 Security Auditor',
|
|
15
|
+
focus: `Focus on security vulnerabilities:
|
|
16
|
+
- SQL injection, command injection
|
|
17
|
+
- XSS, CSRF, SSRF
|
|
18
|
+
- Hardcoded secrets, weak auth
|
|
19
|
+
- Insecure deserialization
|
|
20
|
+
- Path traversal, IDOR
|
|
21
|
+
- Rate limiting, missing access control
|
|
22
|
+
- Insecure cryptography
|
|
23
|
+
- Supply chain risks (unpinned deps)`,
|
|
24
|
+
weight: 1.0,
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
name: 'bugs',
|
|
28
|
+
label: '🐛 Bug Detector',
|
|
29
|
+
focus: `Focus on bugs and correctness:
|
|
30
|
+
- Null pointer / undefined access
|
|
31
|
+
- Race conditions, async issues
|
|
32
|
+
- Off-by-one errors
|
|
33
|
+
- Type mismatches
|
|
34
|
+
- Memory leaks
|
|
35
|
+
- Uncaught exceptions
|
|
36
|
+
- Logic errors
|
|
37
|
+
- Infinite loops, recursion
|
|
38
|
+
- Incorrect API usage`,
|
|
39
|
+
weight: 1.0,
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
name: 'quality',
|
|
43
|
+
label: '📐 Code Quality',
|
|
44
|
+
focus: `Focus on code quality and conventions:
|
|
45
|
+
- Project conventions and style
|
|
46
|
+
- DRY violations
|
|
47
|
+
- Over-engineering / complexity
|
|
48
|
+
- Naming conventions
|
|
49
|
+
- Error handling patterns
|
|
50
|
+
- Test coverage gaps
|
|
51
|
+
- Documentation quality
|
|
52
|
+
- Performance anti-patterns
|
|
53
|
+
- Dead code / unused imports`,
|
|
54
|
+
weight: 1.0,
|
|
55
|
+
},
|
|
56
|
+
];
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Merge results from multiple agents with confidence scoring.
|
|
60
|
+
* Each issue gets a confidence score 0-100 as the agent's confidence
|
|
61
|
+
* that this is a real, actionable issue.
|
|
62
|
+
*/
|
|
63
|
+
function mergeAgentResults(agents, diff, projectHint) {
|
|
64
|
+
const allIssues = [];
|
|
65
|
+
const seenMessages = new Set();
|
|
66
|
+
|
|
67
|
+
for (const agentResult of agents) {
|
|
68
|
+
if (!agentResult.success || !agentResult.issues) continue;
|
|
69
|
+
|
|
70
|
+
for (const issue of agentResult.issues) {
|
|
71
|
+
// Deduplicate by message signature
|
|
72
|
+
const sig = `${issue.file || ''}:${issue.line || 0}:${issue.message.slice(0, 60)}`;
|
|
73
|
+
if (seenMessages.has(sig)) continue;
|
|
74
|
+
seenMessages.add(sig);
|
|
75
|
+
|
|
76
|
+
// Assign confidence based on agent's confidence in this issue
|
|
77
|
+
// If agent didn't provide a score, calculate based on issue type
|
|
78
|
+
const confidence = issue.confidence ?? calculateConfidence(issue, agentResult.name);
|
|
79
|
+
|
|
80
|
+
allIssues.push({
|
|
81
|
+
...issue,
|
|
82
|
+
confidence,
|
|
83
|
+
detectedBy: agentResult.name,
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Sort by confidence (highest first)
|
|
89
|
+
allIssues.sort((a, b) => (b.confidence || 0) - (a.confidence || 0));
|
|
90
|
+
|
|
91
|
+
// Filter: only keep confidence >= 60 (below threshold = false positive)
|
|
92
|
+
const filtered = allIssues.filter(i => i.confidence >= 60);
|
|
93
|
+
const filteredCount = allIssues.length - filtered.length;
|
|
94
|
+
|
|
95
|
+
return { issues: filtered, filteredCount };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Calculate confidence score based on issue characteristics.
|
|
100
|
+
*/
|
|
101
|
+
function calculateConfidence(issue, agentName) {
|
|
102
|
+
let base = 70;
|
|
103
|
+
|
|
104
|
+
// Error types are higher confidence
|
|
105
|
+
if (issue.type === 'error') base += 15;
|
|
106
|
+
else if (issue.type === 'warning') base += 5;
|
|
107
|
+
|
|
108
|
+
// Issues with file + line are more actionable
|
|
109
|
+
if (issue.file) base += 5;
|
|
110
|
+
if (issue.line) base += 5;
|
|
111
|
+
if (issue.suggestion) base += 5;
|
|
112
|
+
|
|
113
|
+
// Cap at 99
|
|
114
|
+
return Math.min(99, base);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Build parallel agent prompts for multi-perspective review.
|
|
119
|
+
*/
|
|
120
|
+
function buildAgentPrompts(diff, config, options = {}) {
|
|
121
|
+
const rulesConfig = config.rules || {};
|
|
122
|
+
const ruleLines = getRuleDescriptions(rulesConfig, diff);
|
|
123
|
+
const auditBlock = options.audit ? '\n- **SECURITY AUDIT MODE ACTIVE**' : '';
|
|
124
|
+
|
|
125
|
+
return AGENT_ROLES.map(role => {
|
|
126
|
+
return {
|
|
127
|
+
role,
|
|
128
|
+
messages: [
|
|
129
|
+
{
|
|
130
|
+
role: 'system',
|
|
131
|
+
content: `You are ${role.label}, an expert code reviewer.
|
|
132
|
+
|
|
133
|
+
Your task: Review the provided git diff and ${role.focus}
|
|
134
|
+
|
|
135
|
+
${options.projectHint ? `Project context:
|
|
136
|
+
${options.projectHint}
|
|
137
|
+
|
|
138
|
+
` : ''}
|
|
139
|
+
Return a JSON object:
|
|
140
|
+
\`\`\`json
|
|
141
|
+
{
|
|
142
|
+
"issues": [
|
|
143
|
+
{
|
|
144
|
+
"type": "error|warning|info",
|
|
145
|
+
"severity": "high|medium|low",
|
|
146
|
+
"confidence": <number 0-100, how confident you are this is a real issue>,
|
|
147
|
+
"file": "filename (optional)",
|
|
148
|
+
"line": <line_number> (optional),
|
|
149
|
+
"message": "Description",
|
|
150
|
+
"suggestion": "How to fix (optional)"
|
|
151
|
+
}
|
|
152
|
+
],
|
|
153
|
+
"summary": "Brief summary of findings from this perspective"
|
|
154
|
+
}
|
|
155
|
+
\`\`\`
|
|
156
|
+
|
|
157
|
+
Confidence scoring guide:
|
|
158
|
+
- 80-100: Absolutely certain, definitely a real issue that should be fixed
|
|
159
|
+
- 60-79: Highly likely, strong evidence but not 100%
|
|
160
|
+
- 40-59: Possible, some evidence but could be false positive
|
|
161
|
+
- 20-39: Weak signal, low confidence
|
|
162
|
+
- 0-19: Not confident, noise
|
|
163
|
+
|
|
164
|
+
Important: Return ONLY valid JSON. No markdown wrapping.${auditBlock}`,
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
role: 'user',
|
|
168
|
+
content: `Git diff to review:
|
|
169
|
+
|
|
170
|
+
\`\`\`diff
|
|
171
|
+
${diff.slice(0, 8000)}
|
|
172
|
+
\`\`\``,
|
|
173
|
+
},
|
|
174
|
+
],
|
|
175
|
+
};
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Run parallel agents and collect results.
|
|
181
|
+
*/
|
|
182
|
+
async function runParallelAgents(apiKey, config, prompts) {
|
|
183
|
+
const results = [];
|
|
184
|
+
const errors = [];
|
|
185
|
+
|
|
186
|
+
const tasks = prompts.map(async (p) => {
|
|
187
|
+
try {
|
|
188
|
+
const text = await callAI(apiKey, p.messages, config);
|
|
189
|
+
const parsed = parseReviewResponse(text);
|
|
190
|
+
return { name: p.role.name, success: true, ...parsed };
|
|
191
|
+
} catch (err) {
|
|
192
|
+
return { name: p.role.name, success: false, error: err.message, issues: [] };
|
|
193
|
+
}
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
// Run all agents in parallel
|
|
197
|
+
const settled = await Promise.allSettled(tasks);
|
|
198
|
+
for (const s of settled) {
|
|
199
|
+
if (s.status === 'fulfilled') {
|
|
200
|
+
results.push(s.value);
|
|
201
|
+
} else {
|
|
202
|
+
errors.push(s.reason);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return { results, errors };
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Review a git diff string using multi-agent parallel review.
|
|
211
|
+
* @param {string} diff - The git diff text
|
|
212
|
+
* @param {object} config - Configuration object (optional, loaded if omitted)
|
|
213
|
+
* @param {object} [options] - Additional options
|
|
214
|
+
* @param {boolean} [options.noCache] - Skip cache
|
|
215
|
+
* @param {boolean} [options.single] - Use single agent (legacy mode, no parallel)
|
|
216
|
+
* @param {boolean} [options.audit] - Security audit mode
|
|
217
|
+
* @param {string} [options.context] - Previous review context (for incremental reviews)
|
|
218
|
+
* @param {string} [options.ignorePattern] - File patterns to ignore
|
|
219
|
+
* @param {number} [options.minConfidence] - Minimum confidence threshold (default: 60)
|
|
220
|
+
* @returns {Promise<object>} Review result with issues, suggestions, score, etc.
|
|
221
|
+
*/
|
|
222
|
+
async function reviewDiff(diff, config, options = {}) {
|
|
223
|
+
if (!config) config = loadConfig();
|
|
224
|
+
|
|
225
|
+
// Apply ignore patterns: strip ignored files from diff
|
|
226
|
+
if (options.ignorePattern) {
|
|
227
|
+
diff = filterDiffByPattern(diff, options.ignorePattern);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Check cache
|
|
231
|
+
if (!options.noCache && !options.single && !options.audit) {
|
|
232
|
+
const ckey = cacheKey(diff);
|
|
233
|
+
const cached = getCached(ckey);
|
|
234
|
+
if (cached) {
|
|
235
|
+
return { ...cached, _cached: true };
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const apiKey = getApiKey(config);
|
|
240
|
+
const projectHint = loadProjectHint();
|
|
241
|
+
const minConfidence = options.minConfidence ?? 60;
|
|
242
|
+
|
|
243
|
+
let result;
|
|
244
|
+
|
|
245
|
+
if (options.single) {
|
|
246
|
+
// Legacy single-agent mode
|
|
247
|
+
const prompt = buildReviewPrompt(diff, config, { ...options, projectHint });
|
|
248
|
+
const aiResponse = await callAI(apiKey, prompt, config);
|
|
249
|
+
result = parseReviewResponse(aiResponse);
|
|
250
|
+
// Add default confidence to legacy results
|
|
251
|
+
if (result.issues) {
|
|
252
|
+
result.issues = result.issues.map(i => ({
|
|
253
|
+
...i,
|
|
254
|
+
confidence: calculateConfidence(i, 'legacy'),
|
|
255
|
+
})).filter(i => i.confidence >= minConfidence);
|
|
256
|
+
}
|
|
257
|
+
} else {
|
|
258
|
+
// Multi-agent parallel review
|
|
259
|
+
const prompts = buildAgentPrompts(diff, config, { ...options, projectHint });
|
|
260
|
+
const { results: agentResults, errors } = await runParallelAgents(apiKey, config, prompts);
|
|
261
|
+
|
|
262
|
+
// Merge and score issues
|
|
263
|
+
const { issues, filteredCount } = mergeAgentResults(agentResults, diff, projectHint);
|
|
264
|
+
|
|
265
|
+
// Build final summary
|
|
266
|
+
const totalAgentIssues = agentResults.reduce((s, a) => s + (a.issues?.length || 0), 0);
|
|
267
|
+
const agentSummaries = agentResults.map(a => ` ${a.name}: ${a.issues?.length || 0} issues`).join('\n');
|
|
268
|
+
|
|
269
|
+
result = {
|
|
270
|
+
summary: agentResults.map(a => a.summary).filter(Boolean).join(' | '),
|
|
271
|
+
score: calculateOverallScore(issues, agentResults),
|
|
272
|
+
issues,
|
|
273
|
+
suggestions: [],
|
|
274
|
+
praise: [],
|
|
275
|
+
_agents: {
|
|
276
|
+
total: agentResults.length,
|
|
277
|
+
summary: agentSummaries,
|
|
278
|
+
totalIssuesFound: totalAgentIssues,
|
|
279
|
+
filteredLowConfidence: filteredCount,
|
|
280
|
+
minConfidence,
|
|
281
|
+
errors: errors.length,
|
|
282
|
+
},
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
// Generate overall suggestions from top issues
|
|
286
|
+
if (issues.length > 0) {
|
|
287
|
+
result.suggestions = issues.slice(0, 3).map(i => i.suggestion).filter(Boolean);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// Record to stats
|
|
292
|
+
try { recordReview(result); } catch {}
|
|
293
|
+
|
|
294
|
+
// Cache result
|
|
295
|
+
if (!options.noCache) {
|
|
296
|
+
const ckey = cacheKey(diff);
|
|
297
|
+
setCached(ckey, result);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
return result;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Calculate overall quality score from merged multi-agent results.
|
|
305
|
+
*/
|
|
306
|
+
function calculateOverallScore(issues, agentResults) {
|
|
307
|
+
if (issues.length === 0) return 100;
|
|
308
|
+
|
|
309
|
+
const errorCount = issues.filter(i => i.type === 'error').length;
|
|
310
|
+
const warningCount = issues.filter(i => i.type === 'warning').length;
|
|
311
|
+
const infoCount = issues.filter(i => i.type === 'info').length;
|
|
312
|
+
|
|
313
|
+
const highConfidence = issues.filter(i => i.confidence >= 85).length;
|
|
314
|
+
const mediumConfidence = issues.filter(i => i.confidence >= 70 && i.confidence < 85).length;
|
|
315
|
+
|
|
316
|
+
// Base deductions
|
|
317
|
+
let score = 100;
|
|
318
|
+
score -= errorCount * 15;
|
|
319
|
+
score -= warningCount * 8;
|
|
320
|
+
score -= infoCount * 3;
|
|
321
|
+
score -= highConfidence * 5; // Extra deduction for high-confidence issues
|
|
322
|
+
score -= mediumConfidence * 2;
|
|
323
|
+
|
|
324
|
+
return Math.max(0, Math.min(100, score));
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Build the system prompt for code review.
|
|
329
|
+
*/
|
|
330
|
+
function buildReviewPrompt(diff, config, options = {}) {
|
|
331
|
+
const rulesConfig = config.rules || {};
|
|
332
|
+
const ruleLines = getRuleDescriptions(rulesConfig, diff);
|
|
333
|
+
if (ruleLines.length === 0) ruleLines.push('- General code quality best practices');
|
|
334
|
+
|
|
335
|
+
let contextBlock = '';
|
|
336
|
+
if (options.context) {
|
|
337
|
+
contextBlock = `\n\nPrevious review context (address these if still relevant, and don\'t repeat issues that were already fixed):\n${options.context}`;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
let hintBlock = '';
|
|
341
|
+
if (options.projectHint) {
|
|
342
|
+
hintBlock = `
|
|
343
|
+
|
|
344
|
+
Project context (use this to guide your review):
|
|
345
|
+
${options.projectHint}`;
|
|
346
|
+
}
|
|
347
|
+
let auditBlock = '';
|
|
348
|
+
if (options.audit) {
|
|
349
|
+
auditBlock = `\n\n## SECURITY AUDIT MODE
|
|
350
|
+
You are now in security audit mode. Focus on the following:
|
|
351
|
+
|
|
352
|
+
### CRITICAL (must check):
|
|
353
|
+
- SQL injection: parameterized queries vs string concatenation
|
|
354
|
+
- Command injection: shell command construction with user input
|
|
355
|
+
- Hardcoded secrets: API keys, passwords, tokens, certificates
|
|
356
|
+
- Authentication/authorization: missing access control, privilege escalation
|
|
357
|
+
- Cross-site scripting (XSS): unescaped user input in HTML/output
|
|
358
|
+
- Insecure direct object references (IDOR): accessing data by user-supplied IDs
|
|
359
|
+
|
|
360
|
+
### HIGH (should check):
|
|
361
|
+
- Path traversal: user-supplied file paths without validation
|
|
362
|
+
- Insecure deserialization: parsing untrusted data without validation
|
|
363
|
+
- Server-side request forgery (SSRF): making requests to user-supplied URLs
|
|
364
|
+
- Insecure cryptography: weak algorithms, hardcoded keys, improper IV usage
|
|
365
|
+
- Rate limiting: endpoints without throttling
|
|
366
|
+
|
|
367
|
+
### MEDIUM (good to check):
|
|
368
|
+
- Information disclosure: stack traces, debug endpoints, verbose error messages
|
|
369
|
+
- Missing security headers: CSP, X-Frame-Options, HSTS
|
|
370
|
+
- Insecure direct file access
|
|
371
|
+
- Session management: cookie flags, token expiry, CSRF protection
|
|
372
|
+
- Input validation: insufficient validation on user-supplied data
|
|
373
|
+
|
|
374
|
+
For each issue found, assign a CVSS-like score (1-10) and output it as "cvss" field.
|
|
375
|
+
Be aggressive - it's better to flag a false positive than miss a real vulnerability.
|
|
376
|
+
`;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
return [
|
|
381
|
+
{
|
|
382
|
+
role: 'system',
|
|
383
|
+
content: `You are an expert code reviewer. Analyze the provided git diff and return a JSON object with the following structure:
|
|
384
|
+
|
|
385
|
+
\`\`\`json
|
|
386
|
+
{
|
|
387
|
+
"summary": "Brief one-line summary of the change",
|
|
388
|
+
"score": <number 0-100>,
|
|
389
|
+
"issues": [
|
|
390
|
+
{
|
|
391
|
+
"type": "error|warning|info",
|
|
392
|
+
"severity": "high|medium|low",
|
|
393
|
+
"file": "filename (optional)",
|
|
394
|
+
"line": <line_number> (optional),
|
|
395
|
+
"message": "Description of the issue",
|
|
396
|
+
"suggestion": "How to fix it (optional)"
|
|
397
|
+
}
|
|
398
|
+
],
|
|
399
|
+
"suggestions": ["improvement suggestion 1", "suggestion 2"],
|
|
400
|
+
"praise": ["good thing 1", "good thing 2"]
|
|
401
|
+
}
|
|
402
|
+
\`\`\`
|
|
403
|
+
|
|
404
|
+
Rules to enforce:
|
|
405
|
+
${ruleLines.join('\n')}
|
|
406
|
+
|
|
407
|
+
Important:
|
|
408
|
+
- Score should reflect overall quality. 90+ = excellent, 70-89 = good, 50-69 = needs improvement, <50 = major issues.
|
|
409
|
+
- Be constructive. Include praise for good practices.
|
|
410
|
+
- Return ONLY valid JSON, no markdown wrapping, no explanations outside the JSON.${contextBlock}${hintBlock}${auditBlock}`,
|
|
411
|
+
},
|
|
412
|
+
{
|
|
413
|
+
role: 'user',
|
|
414
|
+
content: `Here is the git diff to review:\n\n\`\`\`diff\n${diff}\n\`\`\``,
|
|
415
|
+
},
|
|
416
|
+
];
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/**
|
|
420
|
+
* Call the AI provider.
|
|
421
|
+
*/
|
|
422
|
+
async function callAI(apiKey, messages, config) {
|
|
423
|
+
const aiConfig = config.ai || {};
|
|
424
|
+
const provider = aiConfig.provider || 'openai';
|
|
425
|
+
|
|
426
|
+
const OpenAI = require('openai');
|
|
427
|
+
|
|
428
|
+
// Determine base URL from provider or config
|
|
429
|
+
let baseURL = aiConfig.baseURL;
|
|
430
|
+
if (!baseURL) {
|
|
431
|
+
if (provider === 'deepseek') baseURL = 'https://api.deepseek.com';
|
|
432
|
+
// openai and all others default to undefined (official endpoint)
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
const defaultModels = {
|
|
436
|
+
openai: 'gpt-4o',
|
|
437
|
+
deepseek: 'deepseek-chat',
|
|
438
|
+
};
|
|
439
|
+
|
|
440
|
+
const client = new OpenAI({ apiKey, baseURL: baseURL || undefined });
|
|
441
|
+
|
|
442
|
+
const response = await client.chat.completions.create({
|
|
443
|
+
model: aiConfig.model || defaultModels[provider] || provider,
|
|
444
|
+
temperature: aiConfig.temperature ?? 0.3,
|
|
445
|
+
max_tokens: aiConfig.maxTokens || 4096,
|
|
446
|
+
messages,
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
return response.choices[0]?.message?.content || '';
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
/**
|
|
453
|
+
* Parse AI response into structured review result.
|
|
454
|
+
*/
|
|
455
|
+
function parseReviewResponse(text) {
|
|
456
|
+
try {
|
|
457
|
+
// Try direct parse first
|
|
458
|
+
return JSON.parse(text);
|
|
459
|
+
} catch {
|
|
460
|
+
// Try extracting JSON from markdown
|
|
461
|
+
const jsonMatch = text.match(/```(?:json)?\s*(\{[\s\S]*?\})\s*```/);
|
|
462
|
+
if (jsonMatch) {
|
|
463
|
+
try {
|
|
464
|
+
return JSON.parse(jsonMatch[1]);
|
|
465
|
+
} catch {
|
|
466
|
+
// fall through
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
// Try finding any JSON object
|
|
471
|
+
const braceMatch = text.match(/\{[\s\S]*\}/);
|
|
472
|
+
if (braceMatch) {
|
|
473
|
+
try {
|
|
474
|
+
return JSON.parse(braceMatch[0]);
|
|
475
|
+
} catch {
|
|
476
|
+
// fall through
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// Last resort: return raw text as structured error
|
|
481
|
+
return {
|
|
482
|
+
summary: 'Could not parse review response',
|
|
483
|
+
score: 0,
|
|
484
|
+
issues: [{ type: 'error', severity: 'high', message: 'AI returned unparseable response', suggestion: text.slice(0, 500) }],
|
|
485
|
+
suggestions: [],
|
|
486
|
+
praise: [],
|
|
487
|
+
};
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
/**
|
|
492
|
+
* Filter diff to exclude files matching ignore patterns.
|
|
493
|
+
* Support simple glob patterns: *.md, test/**
|
|
494
|
+
*/
|
|
495
|
+
function filterDiffByPattern(diff, ignorePattern) {
|
|
496
|
+
if (!ignorePattern) return diff;
|
|
497
|
+
|
|
498
|
+
const patterns = ignorePattern.split(',').map(p => {
|
|
499
|
+
const glob = p.trim();
|
|
500
|
+
// Convert simple glob to regex
|
|
501
|
+
const regex = glob
|
|
502
|
+
.replace(/[.+^${}()|[\]\\]/g, '\\$&')
|
|
503
|
+
.replace(/\*/g, '[^/]*')
|
|
504
|
+
.replace(/\*\*/g, '.*');
|
|
505
|
+
return new RegExp(regex);
|
|
506
|
+
});
|
|
507
|
+
|
|
508
|
+
const lines = diff.split('\n');
|
|
509
|
+
const filtered = [];
|
|
510
|
+
let skipBlock = false;
|
|
511
|
+
|
|
512
|
+
for (const line of lines) {
|
|
513
|
+
const fileMatch = line.match(/^\+\+\+ b\/(.*)/) || line.match(/^diff --git a\/(.*) b\//);
|
|
514
|
+
if (fileMatch) {
|
|
515
|
+
const filePath = fileMatch[1];
|
|
516
|
+
skipBlock = patterns.some(p => p.test(filePath));
|
|
517
|
+
}
|
|
518
|
+
if (!skipBlock) {
|
|
519
|
+
filtered.push(line);
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
return filtered.join('\n');
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
|
|
527
|
+
/**
|
|
528
|
+
* Load .coderevhint file from current or parent directories.
|
|
529
|
+
*/
|
|
530
|
+
function loadProjectHint() {
|
|
531
|
+
const fs = require('fs');
|
|
532
|
+
const path = require('path');
|
|
533
|
+
let current = process.cwd();
|
|
534
|
+
while (true) {
|
|
535
|
+
const hintPath = path.join(current, '.coderevhint');
|
|
536
|
+
if (fs.existsSync(hintPath)) {
|
|
537
|
+
return fs.readFileSync(hintPath, 'utf-8').trim();
|
|
538
|
+
}
|
|
539
|
+
const parent = path.dirname(current);
|
|
540
|
+
if (parent === current) break;
|
|
541
|
+
current = parent;
|
|
542
|
+
}
|
|
543
|
+
return '';
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
module.exports = { reviewDiff, parseReviewResponse };
|