neuro-cli 4.1.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.
Files changed (175) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +510 -0
  3. package/dist/advisor/advisor.d.ts +50 -0
  4. package/dist/advisor/advisor.js +178 -0
  5. package/dist/agents/base.d.ts +62 -0
  6. package/dist/agents/base.js +215 -0
  7. package/dist/agents/orchestrator.d.ts +46 -0
  8. package/dist/agents/orchestrator.js +192 -0
  9. package/dist/agents/team.d.ts +51 -0
  10. package/dist/agents/team.js +210 -0
  11. package/dist/api/models.d.ts +23 -0
  12. package/dist/api/models.js +514 -0
  13. package/dist/api/ollama.d.ts +153 -0
  14. package/dist/api/ollama.js +751 -0
  15. package/dist/api/openrouter.d.ts +55 -0
  16. package/dist/api/openrouter.js +223 -0
  17. package/dist/commands/commands.d.ts +43 -0
  18. package/dist/commands/commands.js +308 -0
  19. package/dist/config/config.d.ts +8 -0
  20. package/dist/config/config.js +311 -0
  21. package/dist/context/compaction.d.ts +54 -0
  22. package/dist/context/compaction.js +251 -0
  23. package/dist/context/custom-agents.d.ts +107 -0
  24. package/dist/context/custom-agents.js +397 -0
  25. package/dist/context/custom-tools.d.ts +120 -0
  26. package/dist/context/custom-tools.js +564 -0
  27. package/dist/context/git-checkpoint.d.ts +52 -0
  28. package/dist/context/git-checkpoint.js +240 -0
  29. package/dist/context/neuro-md.d.ts +48 -0
  30. package/dist/context/neuro-md.js +202 -0
  31. package/dist/context/neuroignore.d.ts +102 -0
  32. package/dist/context/neuroignore.js +441 -0
  33. package/dist/context/repo-map.d.ts +38 -0
  34. package/dist/context/repo-map.js +220 -0
  35. package/dist/context/skill-standard.d.ts +262 -0
  36. package/dist/context/skill-standard.js +1156 -0
  37. package/dist/context/skill-system.d.ts +75 -0
  38. package/dist/context/skill-system.js +578 -0
  39. package/dist/context/tree-sitter.d.ts +99 -0
  40. package/dist/context/tree-sitter.js +1956 -0
  41. package/dist/core/acp.d.ts +325 -0
  42. package/dist/core/acp.js +1498 -0
  43. package/dist/core/api-server.d.ts +143 -0
  44. package/dist/core/api-server.js +550 -0
  45. package/dist/core/approval.d.ts +81 -0
  46. package/dist/core/approval.js +432 -0
  47. package/dist/core/auto-compact.d.ts +127 -0
  48. package/dist/core/auto-compact.js +436 -0
  49. package/dist/core/auto-mode.d.ts +232 -0
  50. package/dist/core/auto-mode.js +831 -0
  51. package/dist/core/background-session.d.ts +166 -0
  52. package/dist/core/background-session.js +696 -0
  53. package/dist/core/cicd.d.ts +257 -0
  54. package/dist/core/cicd.js +1443 -0
  55. package/dist/core/cloud-sync.d.ts +156 -0
  56. package/dist/core/cloud-sync.js +582 -0
  57. package/dist/core/code-review.d.ts +132 -0
  58. package/dist/core/code-review.js +1191 -0
  59. package/dist/core/completion.d.ts +49 -0
  60. package/dist/core/completion.js +384 -0
  61. package/dist/core/context.d.ts +38 -0
  62. package/dist/core/context.js +144 -0
  63. package/dist/core/diff-preview.d.ts +35 -0
  64. package/dist/core/diff-preview.js +173 -0
  65. package/dist/core/doom-loop.d.ts +51 -0
  66. package/dist/core/doom-loop.js +179 -0
  67. package/dist/core/engine.d.ts +183 -0
  68. package/dist/core/engine.js +942 -0
  69. package/dist/core/extended-thinking.d.ts +103 -0
  70. package/dist/core/extended-thinking.js +269 -0
  71. package/dist/core/fallback.d.ts +54 -0
  72. package/dist/core/fallback.js +104 -0
  73. package/dist/core/git-worktree.d.ts +139 -0
  74. package/dist/core/git-worktree.js +614 -0
  75. package/dist/core/headless.d.ts +30 -0
  76. package/dist/core/headless.js +101 -0
  77. package/dist/core/i18n.d.ts +83 -0
  78. package/dist/core/i18n.js +739 -0
  79. package/dist/core/linting.d.ts +129 -0
  80. package/dist/core/linting.js +699 -0
  81. package/dist/core/model-router.d.ts +109 -0
  82. package/dist/core/model-router.js +581 -0
  83. package/dist/core/multi-model.d.ts +243 -0
  84. package/dist/core/multi-model.js +1099 -0
  85. package/dist/core/multi-session.d.ts +144 -0
  86. package/dist/core/multi-session.js +442 -0
  87. package/dist/core/multimodal.d.ts +125 -0
  88. package/dist/core/multimodal.js +286 -0
  89. package/dist/core/observability.d.ts +93 -0
  90. package/dist/core/observability.js +737 -0
  91. package/dist/core/os-sandbox.d.ts +122 -0
  92. package/dist/core/os-sandbox.js +1193 -0
  93. package/dist/core/outcome-grading.d.ts +228 -0
  94. package/dist/core/outcome-grading.js +1123 -0
  95. package/dist/core/output-styles.d.ts +57 -0
  96. package/dist/core/output-styles.js +382 -0
  97. package/dist/core/parallel-agents.d.ts +183 -0
  98. package/dist/core/parallel-agents.js +563 -0
  99. package/dist/core/plugin-bundle.d.ts +236 -0
  100. package/dist/core/plugin-bundle.js +887 -0
  101. package/dist/core/plugin-sdk.d.ts +139 -0
  102. package/dist/core/plugin-sdk.js +273 -0
  103. package/dist/core/prompt-cache.d.ts +163 -0
  104. package/dist/core/prompt-cache.js +599 -0
  105. package/dist/core/sandbox.d.ts +127 -0
  106. package/dist/core/sandbox.js +369 -0
  107. package/dist/core/scheduled-tasks.d.ts +151 -0
  108. package/dist/core/scheduled-tasks.js +502 -0
  109. package/dist/core/security-scanner.d.ts +160 -0
  110. package/dist/core/security-scanner.js +1494 -0
  111. package/dist/core/session.d.ts +83 -0
  112. package/dist/core/session.js +269 -0
  113. package/dist/core/shell-completion.d.ts +51 -0
  114. package/dist/core/shell-completion.js +674 -0
  115. package/dist/core/smart-monitor.d.ts +146 -0
  116. package/dist/core/smart-monitor.js +1199 -0
  117. package/dist/core/spec-driven.d.ts +233 -0
  118. package/dist/core/spec-driven.js +1485 -0
  119. package/dist/core/spending-warnings.d.ts +123 -0
  120. package/dist/core/spending-warnings.js +456 -0
  121. package/dist/core/sub-agent.d.ts +298 -0
  122. package/dist/core/sub-agent.js +1023 -0
  123. package/dist/core/telemetry.d.ts +157 -0
  124. package/dist/core/telemetry.js +412 -0
  125. package/dist/core/terminal-ux.d.ts +134 -0
  126. package/dist/core/terminal-ux.js +649 -0
  127. package/dist/core/testing.d.ts +146 -0
  128. package/dist/core/testing.js +1200 -0
  129. package/dist/core/types.d.ts +439 -0
  130. package/dist/core/types.js +6 -0
  131. package/dist/core/undo-redo.d.ts +112 -0
  132. package/dist/core/undo-redo.js +290 -0
  133. package/dist/core/updater.d.ts +159 -0
  134. package/dist/core/updater.js +608 -0
  135. package/dist/core/vim-mode.d.ts +151 -0
  136. package/dist/core/vim-mode.js +771 -0
  137. package/dist/core/voice.d.ts +137 -0
  138. package/dist/core/voice.js +538 -0
  139. package/dist/core/web-dashboard.d.ts +109 -0
  140. package/dist/core/web-dashboard.js +484 -0
  141. package/dist/hooks/hooks.d.ts +74 -0
  142. package/dist/hooks/hooks.js +160 -0
  143. package/dist/hooks/llm-evaluator.d.ts +165 -0
  144. package/dist/hooks/llm-evaluator.js +560 -0
  145. package/dist/index.d.ts +3 -0
  146. package/dist/index.js +1186 -0
  147. package/dist/lsp/lsp-manager.d.ts +63 -0
  148. package/dist/lsp/lsp-manager.js +351 -0
  149. package/dist/mcp/client.d.ts +133 -0
  150. package/dist/mcp/client.js +684 -0
  151. package/dist/mcp/mcp-apps.d.ts +70 -0
  152. package/dist/mcp/mcp-apps.js +1007 -0
  153. package/dist/tools/bash.d.ts +5 -0
  154. package/dist/tools/bash.js +195 -0
  155. package/dist/tools/browser.d.ts +92 -0
  156. package/dist/tools/browser.js +1570 -0
  157. package/dist/tools/extended.d.ts +6 -0
  158. package/dist/tools/extended.js +191 -0
  159. package/dist/tools/file.d.ts +10 -0
  160. package/dist/tools/file.js +382 -0
  161. package/dist/tools/github.d.ts +389 -0
  162. package/dist/tools/github.js +759 -0
  163. package/dist/tools/index.d.ts +9 -0
  164. package/dist/tools/index.js +40 -0
  165. package/dist/tools/memory.d.ts +6 -0
  166. package/dist/tools/memory.js +197 -0
  167. package/dist/tools/registry.d.ts +29 -0
  168. package/dist/tools/registry.js +64 -0
  169. package/dist/tools/web.d.ts +6 -0
  170. package/dist/tools/web.js +150 -0
  171. package/dist/ui/renderer.d.ts +97 -0
  172. package/dist/ui/renderer.js +279 -0
  173. package/dist/ui/theme.d.ts +27 -0
  174. package/dist/ui/theme.js +106 -0
  175. package/package.json +83 -0
@@ -0,0 +1,1191 @@
1
+ // ============================================================
2
+ // NeuroCLI - Code Review System
3
+ // Automatic code review with pattern-based analysis
4
+ // Covers security, performance, style, correctness,
5
+ // dead code, complexity, and best practice violations
6
+ // ============================================================
7
+ import { resolve, extname, relative } from 'path';
8
+ import { existsSync, readFileSync } from 'fs';
9
+ import { execSync } from 'child_process';
10
+ // -----------------------------------------------------------
11
+ // Default configuration
12
+ // -----------------------------------------------------------
13
+ const DEFAULT_CODE_REVIEW_CONFIG = {
14
+ enabled: true,
15
+ autoReviewOnChange: false,
16
+ focusAreas: ['security', 'performance', 'correctness', 'best-practices', 'dead-code', 'complexity', 'style'],
17
+ severityThreshold: 'minor',
18
+ excludePatterns: [
19
+ 'node_modules/**',
20
+ '.git/**',
21
+ 'dist/**',
22
+ 'build/**',
23
+ '__pycache__/**',
24
+ 'target/**',
25
+ '.next/**',
26
+ 'vendor/**',
27
+ '*.min.js',
28
+ '*.min.css',
29
+ '*.bundle.js',
30
+ 'package-lock.json',
31
+ 'bun.lock',
32
+ 'yarn.lock',
33
+ 'go.sum',
34
+ ],
35
+ };
36
+ // -----------------------------------------------------------
37
+ // Extension → language mapping
38
+ // -----------------------------------------------------------
39
+ const EXTENSION_LANGUAGE_MAP = {
40
+ '.js': 'javascript',
41
+ '.jsx': 'javascript',
42
+ '.mjs': 'javascript',
43
+ '.cjs': 'javascript',
44
+ '.ts': 'typescript',
45
+ '.tsx': 'typescript',
46
+ '.mts': 'typescript',
47
+ '.cts': 'typescript',
48
+ '.py': 'python',
49
+ '.pyi': 'python',
50
+ '.go': 'go',
51
+ '.rs': 'rust',
52
+ '.java': 'java',
53
+ '.c': 'c',
54
+ '.h': 'c',
55
+ '.cpp': 'cpp',
56
+ '.cc': 'cpp',
57
+ '.cxx': 'cpp',
58
+ '.hpp': 'cpp',
59
+ '.hxx': 'cpp',
60
+ '.rb': 'ruby',
61
+ '.php': 'php',
62
+ '.sh': 'shell',
63
+ '.bash': 'shell',
64
+ '.sql': 'sql',
65
+ '.html': 'html',
66
+ '.css': 'css',
67
+ '.scss': 'scss',
68
+ '.json': 'json',
69
+ '.yaml': 'yaml',
70
+ '.yml': 'yaml',
71
+ '.toml': 'toml',
72
+ };
73
+ // Severity order for threshold filtering
74
+ const SEVERITY_ORDER = {
75
+ critical: 0,
76
+ major: 1,
77
+ minor: 2,
78
+ suggestion: 3,
79
+ info: 4,
80
+ };
81
+ // -----------------------------------------------------------
82
+ // Review patterns database
83
+ // -----------------------------------------------------------
84
+ const REVIEW_PATTERNS = [
85
+ // ---- Security ----
86
+ {
87
+ rule: 'SEC001',
88
+ category: 'security',
89
+ severity: 'critical',
90
+ pattern: /eval\s*\(/,
91
+ message: 'Use of eval() is a security risk — can execute arbitrary code.',
92
+ suggestion: 'Use JSON.parse() for JSON data, or a sandboxed interpreter for dynamic code.',
93
+ languages: ['javascript', 'typescript'],
94
+ },
95
+ {
96
+ rule: 'SEC002',
97
+ category: 'security',
98
+ severity: 'critical',
99
+ pattern: /innerHTML\s*[+=]/,
100
+ message: 'Direct innerHTML assignment can lead to XSS vulnerabilities.',
101
+ suggestion: 'Use textContent for plain text, or a sanitization library like DOMPurify for HTML.',
102
+ languages: ['javascript', 'typescript'],
103
+ },
104
+ {
105
+ rule: 'SEC003',
106
+ category: 'security',
107
+ severity: 'critical',
108
+ pattern: /document\.write\s*\(/,
109
+ message: 'document.write() can lead to XSS and overrides the entire document.',
110
+ suggestion: 'Use DOM manipulation methods like createElement() and appendChild().',
111
+ languages: ['javascript', 'typescript'],
112
+ },
113
+ {
114
+ rule: 'SEC004',
115
+ category: 'security',
116
+ severity: 'critical',
117
+ pattern: /password\s*=\s*["'][^"']+["']/i,
118
+ message: 'Hardcoded password detected. Never store credentials in source code.',
119
+ suggestion: 'Use environment variables or a secrets manager.',
120
+ },
121
+ {
122
+ rule: 'SEC005',
123
+ category: 'security',
124
+ severity: 'critical',
125
+ pattern: /api[_-]?key\s*=\s*["'][^"']+["']/i,
126
+ message: 'Hardcoded API key detected. Never store API keys in source code.',
127
+ suggestion: 'Use environment variables or a secrets manager.',
128
+ },
129
+ {
130
+ rule: 'SEC006',
131
+ category: 'security',
132
+ severity: 'critical',
133
+ pattern: /secret\s*=\s*["'][^"']+["']/i,
134
+ message: 'Hardcoded secret detected. Never store secrets in source code.',
135
+ suggestion: 'Use environment variables or a secrets manager.',
136
+ },
137
+ {
138
+ rule: 'SEC007',
139
+ category: 'security',
140
+ severity: 'major',
141
+ pattern: /exec\s*\(\s*[^)]*\$/,
142
+ message: 'Possible command injection via string interpolation in exec().',
143
+ suggestion: 'Use execFile() with an args array, or sanitize input thoroughly.',
144
+ languages: ['javascript', 'typescript'],
145
+ },
146
+ {
147
+ rule: 'SEC008',
148
+ category: 'security',
149
+ severity: 'critical',
150
+ pattern: /subprocess\.(call|run|Popen)\s*\([^)]*shell\s*=\s*True/,
151
+ message: 'Shell injection risk: subprocess with shell=True.',
152
+ suggestion: 'Pass arguments as a list instead of using shell=True.',
153
+ languages: ['python'],
154
+ },
155
+ {
156
+ rule: 'SEC009',
157
+ category: 'security',
158
+ severity: 'critical',
159
+ pattern: /os\.system\s*\(/,
160
+ message: 'os.system() is vulnerable to shell injection.',
161
+ suggestion: 'Use subprocess.run() with a list of arguments.',
162
+ languages: ['python'],
163
+ },
164
+ {
165
+ rule: 'SEC010',
166
+ category: 'security',
167
+ severity: 'major',
168
+ pattern: /SELECT\s+.*\s+FROM\s+.*\s*(?:WHERE|AND|OR)\s+.*\+\s*|f["']SELECT/i,
169
+ message: 'Possible SQL injection: string concatenation or f-string in SQL query.',
170
+ suggestion: 'Use parameterized queries with placeholders.',
171
+ languages: ['python', 'javascript', 'typescript'],
172
+ },
173
+ {
174
+ rule: 'SEC011',
175
+ category: 'security',
176
+ severity: 'major',
177
+ pattern: /cors\(\s*\)|Access-Control-Allow-Origin.*\*/,
178
+ message: 'CORS configured to allow all origins.',
179
+ suggestion: 'Restrict CORS to specific trusted domains.',
180
+ languages: ['javascript', 'typescript'],
181
+ },
182
+ {
183
+ rule: 'SEC012',
184
+ category: 'security',
185
+ severity: 'minor',
186
+ pattern: /console\.(log|debug|info|warn|error)\s*\(.*password/i,
187
+ message: 'Potential sensitive data logged to console.',
188
+ suggestion: 'Avoid logging sensitive information such as passwords.',
189
+ languages: ['javascript', 'typescript'],
190
+ },
191
+ {
192
+ rule: 'SEC013',
193
+ category: 'security',
194
+ severity: 'major',
195
+ pattern: /unsafe\s+fn|unsafe\s+impl|unsafe\s+trait/,
196
+ message: 'Unsafe Rust block detected — bypasses safety guarantees.',
197
+ suggestion: 'Ensure the unsafe block is absolutely necessary and well-documented.',
198
+ languages: ['rust'],
199
+ },
200
+ {
201
+ rule: 'SEC014',
202
+ category: 'security',
203
+ severity: 'major',
204
+ pattern: /panic!\s*\(/,
205
+ message: 'panic!() can crash the application. Prefer Result-based error handling.',
206
+ suggestion: 'Return Result<T, E> and propagate errors explicitly.',
207
+ languages: ['rust'],
208
+ },
209
+ // ---- Performance ----
210
+ {
211
+ rule: 'PERF001',
212
+ category: 'performance',
213
+ severity: 'major',
214
+ pattern: /\.forEach\s*\([^)]*\)\s*\.forEach|\.map\s*\([^)]*\)\s*\.map/,
215
+ message: 'Chained array iterations — each creates a new pass over the data.',
216
+ suggestion: 'Combine multiple operations into a single .reduce() or for loop.',
217
+ languages: ['javascript', 'typescript'],
218
+ },
219
+ {
220
+ rule: 'PERF002',
221
+ category: 'performance',
222
+ severity: 'major',
223
+ pattern: /JSON\.(parse|stringify)\s*\([^)]*\)\s*(?:inside|within)|for\s*\(.*JSON\./,
224
+ message: 'JSON parse/stringify inside a loop — expensive operation repeated.',
225
+ suggestion: 'Move JSON operations outside the loop when possible.',
226
+ languages: ['javascript', 'typescript'],
227
+ },
228
+ {
229
+ rule: 'PERF003',
230
+ category: 'performance',
231
+ severity: 'minor',
232
+ pattern: /document\.querySelector(?:All)?\s*\([^)]+\)\s*(?:inside|within)|for\s*\(.*querySelector/,
233
+ message: 'DOM query inside a loop — cache the result outside the loop.',
234
+ suggestion: 'Query the DOM once before the loop and reuse the reference.',
235
+ languages: ['javascript', 'typescript'],
236
+ },
237
+ {
238
+ rule: 'PERF004',
239
+ category: 'performance',
240
+ severity: 'major',
241
+ pattern: /\.push\s*\([^)]*\)\s*(?:\.push|\.concat)|Array\s*\(\d{4,}\)/,
242
+ message: 'Large array allocation or repeated push operations detected.',
243
+ suggestion: 'Pre-allocate arrays with known sizes, or use typed arrays for numeric data.',
244
+ languages: ['javascript', 'typescript'],
245
+ },
246
+ {
247
+ rule: 'PERF005',
248
+ category: 'performance',
249
+ severity: 'minor',
250
+ pattern: /Object\.assign\s*\(\s*\{\s*\}/,
251
+ message: 'Unnecessary Object.assign with empty object — use spread syntax instead.',
252
+ suggestion: 'Use { ...source } instead of Object.assign({}, source).',
253
+ languages: ['javascript', 'typescript'],
254
+ },
255
+ {
256
+ rule: 'PERF006',
257
+ category: 'performance',
258
+ severity: 'major',
259
+ pattern: /for\s+\w+\s+in\s+range\s*\(\s*\d{5,}/,
260
+ message: 'Large range iteration detected — may cause performance issues.',
261
+ suggestion: 'Consider using generators, itertools, or a more efficient algorithm.',
262
+ languages: ['python'],
263
+ },
264
+ {
265
+ rule: 'PERF007',
266
+ category: 'performance',
267
+ severity: 'minor',
268
+ pattern: /list\s*\(\s*map\s*\(|list\s*\(\s*filter\s*\(/,
269
+ message: 'Unnecessary list() around map/filter — iterate lazily unless you need a list.',
270
+ suggestion: 'Remove list() if you only need to iterate, or keep it if you need indexing.',
271
+ languages: ['python'],
272
+ },
273
+ {
274
+ rule: 'PERF008',
275
+ category: 'performance',
276
+ severity: 'major',
277
+ pattern: /string\s*\+=|\.append\s*\(\s*["'].*["']\s*\).*for/,
278
+ message: 'String concatenation in a loop — use string builder pattern.',
279
+ suggestion: 'Collect parts in an array/list and join them at the end.',
280
+ },
281
+ {
282
+ rule: 'PERF009',
283
+ category: 'performance',
284
+ severity: 'minor',
285
+ pattern: /clone\s*\(\s*\)/,
286
+ message: 'Unnecessary .clone() — cloning can be expensive in performance-critical paths.',
287
+ suggestion: 'Use references where possible, or verify the clone is necessary.',
288
+ languages: ['rust'],
289
+ },
290
+ {
291
+ rule: 'PERF010',
292
+ category: 'performance',
293
+ severity: 'minor',
294
+ pattern: /unwrap\s*\(\s*\)/,
295
+ message: '.unwrap() can panic at runtime and hides error handling.',
296
+ suggestion: 'Use pattern matching or .unwrap_or_default() / .unwrap_or(fallback).',
297
+ languages: ['rust'],
298
+ },
299
+ // ---- Style / Consistency ----
300
+ {
301
+ rule: 'STYLE001',
302
+ category: 'style',
303
+ severity: 'minor',
304
+ pattern: /var\s+/,
305
+ message: 'Use of var — prefer const or let for block scoping.',
306
+ suggestion: 'Replace var with const (for constants) or let (for reassignable variables).',
307
+ languages: ['javascript', 'typescript'],
308
+ },
309
+ {
310
+ rule: 'STYLE002',
311
+ category: 'style',
312
+ severity: 'minor',
313
+ pattern: /==(?!=)/,
314
+ message: 'Use of == — prefer strict equality ===.',
315
+ suggestion: 'Use === for strict comparison to avoid type coercion bugs.',
316
+ languages: ['javascript', 'typescript'],
317
+ },
318
+ {
319
+ rule: 'STYLE003',
320
+ category: 'style',
321
+ severity: 'suggestion',
322
+ pattern: /\/\/\s*TODO|\/\/\s*FIXME|\/\/\s*HACK|\/\/\s*XXX/i,
323
+ message: 'TODO/FIXME/HACK comment found — track these and resolve them.',
324
+ suggestion: 'Create an issue or ticket to track this item.',
325
+ },
326
+ {
327
+ rule: 'STYLE004',
328
+ category: 'style',
329
+ severity: 'minor',
330
+ pattern: /console\.(log|debug|info)\s*\(/,
331
+ message: 'Console log statement detected — should be removed for production.',
332
+ suggestion: 'Remove console.log or replace with a proper logging library.',
333
+ languages: ['javascript', 'typescript'],
334
+ },
335
+ {
336
+ rule: 'STYLE005',
337
+ category: 'style',
338
+ severity: 'minor',
339
+ pattern: /print\s*\([^)]*\)/,
340
+ message: 'Print statement detected — should use a logging framework instead.',
341
+ suggestion: 'Replace with logging.debug(), logging.info(), etc.',
342
+ languages: ['python'],
343
+ },
344
+ {
345
+ rule: 'STYLE006',
346
+ category: 'style',
347
+ severity: 'suggestion',
348
+ pattern: /class\s+\w+\s*:\s*$/,
349
+ message: 'Class without docstring detected.',
350
+ suggestion: 'Add a docstring to document the class purpose.',
351
+ languages: ['python'],
352
+ },
353
+ {
354
+ rule: 'STYLE007',
355
+ category: 'style',
356
+ severity: 'minor',
357
+ pattern: /except\s*:/,
358
+ message: 'Bare except clause — catches all exceptions including SystemExit and KeyboardInterrupt.',
359
+ suggestion: 'Use except Exception: or a more specific exception type.',
360
+ languages: ['python'],
361
+ },
362
+ // ---- Correctness / Bug-prone patterns ----
363
+ {
364
+ rule: 'BUG001',
365
+ category: 'correctness',
366
+ severity: 'major',
367
+ pattern: /if\s*\([^)]*=[^=][^)]*\)/,
368
+ message: 'Possible assignment in conditional — likely intended == comparison.',
369
+ suggestion: 'Use === for comparison. If assignment is intended, wrap in extra parens.',
370
+ languages: ['javascript', 'typescript'],
371
+ },
372
+ {
373
+ rule: 'BUG002',
374
+ category: 'correctness',
375
+ severity: 'critical',
376
+ pattern: /await\s+[^;\n]*[^)]*(?:\n|$)/,
377
+ message: 'Possible missing await — async operation may not complete before use.',
378
+ suggestion: 'Ensure all async operations are properly awaited or handled.',
379
+ languages: ['javascript', 'typescript'],
380
+ },
381
+ {
382
+ rule: 'BUG003',
383
+ category: 'correctness',
384
+ severity: 'major',
385
+ pattern: /catch\s*\(\s*\w+\s*\)\s*\{\s*\}/,
386
+ message: 'Empty catch block — errors are silently swallowed.',
387
+ suggestion: 'Log the error, re-throw it, or handle it meaningfully.',
388
+ languages: ['javascript', 'typescript'],
389
+ },
390
+ {
391
+ rule: 'BUG004',
392
+ category: 'correctness',
393
+ severity: 'major',
394
+ pattern: /except\s+\w+\s*:\s*pass/,
395
+ message: 'Empty except block with pass — errors are silently ignored.',
396
+ suggestion: 'Log the error, re-raise it, or handle it meaningfully.',
397
+ languages: ['python'],
398
+ },
399
+ {
400
+ rule: 'BUG005',
401
+ category: 'correctness',
402
+ severity: 'critical',
403
+ pattern: /setInterval\s*\([^,]+,\s*\d+\)/,
404
+ message: 'setInterval without clearInterval — may cause memory leaks.',
405
+ suggestion: 'Store the interval ID and call clearInterval() on cleanup.',
406
+ languages: ['javascript', 'typescript'],
407
+ },
408
+ {
409
+ rule: 'BUG006',
410
+ category: 'correctness',
411
+ severity: 'major',
412
+ pattern: /setTimeout\s*\([^,]+,\s*0\s*\)/,
413
+ message: 'setTimeout with 0ms delay — often indicates a race condition workaround.',
414
+ suggestion: 'Restructure code to avoid relying on event loop timing.',
415
+ languages: ['javascript', 'typescript'],
416
+ },
417
+ {
418
+ rule: 'BUG007',
419
+ category: 'correctness',
420
+ severity: 'major',
421
+ pattern: /return\s+new\s+Promise\s*\(\s*(?:async|function)/,
422
+ message: 'Returning new Promise from an async function — double-wrapping.',
423
+ suggestion: 'Async functions already return promises; remove the new Promise wrapper.',
424
+ languages: ['javascript', 'typescript'],
425
+ },
426
+ {
427
+ rule: 'BUG008',
428
+ category: 'correctness',
429
+ severity: 'minor',
430
+ pattern: /\.sort\s*\(\s*\)/,
431
+ message: 'Array.sort() without comparator — sorts lexicographically, not numerically.',
432
+ suggestion: 'Pass a comparator: .sort((a, b) => a - b) for numeric sorting.',
433
+ languages: ['javascript', 'typescript'],
434
+ },
435
+ {
436
+ rule: 'BUG009',
437
+ category: 'correctness',
438
+ severity: 'critical',
439
+ pattern: /if\s+\w+\s*=\s*None/,
440
+ message: 'Using = for None comparison — should use "is None".',
441
+ suggestion: 'Use "if x is None:" or "if x is not None:" for None comparisons.',
442
+ languages: ['python'],
443
+ },
444
+ {
445
+ rule: 'BUG010',
446
+ category: 'correctness',
447
+ severity: 'major',
448
+ pattern: /def\s+\w+\s*\([^)]*mutable_default[^)]*\)|def\s+\w+\s*\([^)]*=\s*\[\]|def\s+\w+\s*\([^)]*=\s*\{/,
449
+ message: 'Mutable default argument — shared across all calls, causing subtle bugs.',
450
+ suggestion: 'Use None as default and initialize inside the function body.',
451
+ languages: ['python'],
452
+ },
453
+ // ---- Best Practices ----
454
+ {
455
+ rule: 'BP001',
456
+ category: 'best-practices',
457
+ severity: 'major',
458
+ pattern: /any\s+(?:as\s+any|:\s*any)/,
459
+ message: 'Use of TypeScript any — defeats type safety.',
460
+ suggestion: 'Use a specific type, unknown, or a generic parameter.',
461
+ languages: ['typescript'],
462
+ },
463
+ {
464
+ rule: 'BP002',
465
+ category: 'best-practices',
466
+ severity: 'minor',
467
+ pattern: /@ts-ignore|@ts-nocheck/,
468
+ message: 'TypeScript suppression directive — hides type errors.',
469
+ suggestion: 'Fix the underlying type error instead of suppressing it.',
470
+ languages: ['typescript'],
471
+ },
472
+ {
473
+ rule: 'BP003',
474
+ category: 'best-practices',
475
+ severity: 'minor',
476
+ pattern: /import\s+\*\s+as/,
477
+ message: 'Namespace import — may increase bundle size with unused exports.',
478
+ suggestion: 'Import only the specific items you need: import { item } from ...',
479
+ languages: ['javascript', 'typescript'],
480
+ },
481
+ {
482
+ rule: 'BP004',
483
+ category: 'best-practices',
484
+ severity: 'suggestion',
485
+ pattern: /function\s+\w+\s*\([^)]*\)\s*\{[\s\S]{500,}/,
486
+ message: 'Function is very long — consider breaking it into smaller functions.',
487
+ suggestion: 'Extract logical sections into well-named helper functions.',
488
+ },
489
+ {
490
+ rule: 'BP005',
491
+ category: 'best-practices',
492
+ severity: 'major',
493
+ pattern: /\.bind\s*\(\s*this\s*\)/,
494
+ message: 'Using .bind(this) — prefer arrow functions for lexical this.',
495
+ suggestion: 'Use arrow functions or class properties instead of .bind(this).',
496
+ languages: ['javascript', 'typescript'],
497
+ },
498
+ {
499
+ rule: 'BP006',
500
+ category: 'best-practices',
501
+ severity: 'minor',
502
+ pattern: /new\s+Date\s*\(\s*\)/,
503
+ message: 'Direct Date construction makes code hard to test.',
504
+ suggestion: 'Inject a clock/timestamp function for testability.',
505
+ languages: ['javascript', 'typescript'],
506
+ },
507
+ {
508
+ rule: 'BP007',
509
+ category: 'best-practices',
510
+ severity: 'minor',
511
+ pattern: /import\s+\w+\s+from\s+["']\.+(?!\/)/,
512
+ message: 'Relative import without proper path — may break on refactoring.',
513
+ suggestion: 'Use path aliases configured in tsconfig.json or jsconfig.json.',
514
+ languages: ['javascript', 'typescript'],
515
+ },
516
+ // ---- Dead Code ----
517
+ {
518
+ rule: 'DEAD001',
519
+ category: 'dead-code',
520
+ severity: 'minor',
521
+ pattern: /\/\/\s*eslint-disable-next-line|\/\/\s*eslint-disable\s/,
522
+ message: 'ESLint disable directive — may hide issues that should be fixed.',
523
+ suggestion: 'Fix the underlying issue instead of disabling the rule.',
524
+ languages: ['javascript', 'typescript'],
525
+ },
526
+ {
527
+ rule: 'DEAD002',
528
+ category: 'dead-code',
529
+ severity: 'minor',
530
+ pattern: /\/\/\s*noqa/,
531
+ message: 'noqa directive — may hide lint issues that should be fixed.',
532
+ suggestion: 'Fix the underlying issue instead of suppressing the warning.',
533
+ languages: ['python'],
534
+ },
535
+ {
536
+ rule: 'DEAD003',
537
+ category: 'dead-code',
538
+ severity: 'minor',
539
+ pattern: /\/\*[\s\S]*?\*\//g,
540
+ message: 'Large block comment — could indicate dead/commented-out code.',
541
+ suggestion: 'Remove commented-out code; rely on version control for history.',
542
+ multiline: true,
543
+ },
544
+ // ---- Complexity ----
545
+ {
546
+ rule: 'COMPLEX001',
547
+ category: 'complexity',
548
+ severity: 'major',
549
+ pattern: /if\s*\(.*(?:&&|\|\|).*(?:&&|\|\|).*(?:&&|\|\|)/,
550
+ message: 'Complex boolean expression — hard to read and error-prone.',
551
+ suggestion: 'Extract sub-conditions into well-named boolean variables.',
552
+ },
553
+ {
554
+ rule: 'COMPLEX002',
555
+ category: 'complexity',
556
+ severity: 'major',
557
+ pattern: /(?:if|elif|else)\s*(?:if)?\s*.*(?:if|elif|else)\s*(?:if)?\s*.*(?:if|elif|else)\s*(?:if)?\s*.*(?:if|elif|else)/,
558
+ message: 'Deeply nested conditionals — consider early returns or a switch/match.',
559
+ suggestion: 'Use guard clauses (early returns) or polymorphism to reduce nesting.',
560
+ },
561
+ {
562
+ rule: 'COMPLEX003',
563
+ category: 'complexity',
564
+ severity: 'minor',
565
+ pattern: /try\s*\{[\s\S]*?\}\s*catch\s*\{[\s\S]*?\}\s*finally\s*\{/,
566
+ message: 'try/catch/finally block — ensure each section is simple and clear.',
567
+ suggestion: 'Keep try blocks minimal; move logic out of finally where possible.',
568
+ languages: ['javascript', 'typescript'],
569
+ multiline: true,
570
+ },
571
+ ];
572
+ // -----------------------------------------------------------
573
+ // CodeReviewSystem class
574
+ // -----------------------------------------------------------
575
+ export class CodeReviewSystem {
576
+ config;
577
+ projectRoot;
578
+ comments = [];
579
+ reports = [];
580
+ callbacks = [];
581
+ focusAreas;
582
+ commentCounter = 0;
583
+ constructor(projectRoot, config) {
584
+ this.projectRoot = projectRoot ?? process.cwd();
585
+ this.config = { ...DEFAULT_CODE_REVIEW_CONFIG, ...config };
586
+ this.focusAreas = new Set(this.config.focusAreas);
587
+ }
588
+ // ----------------------------------------------------------
589
+ // Public API
590
+ // ----------------------------------------------------------
591
+ /**
592
+ * Review a single file for issues.
593
+ * Returns a ReviewReport with all detected comments and a quality score.
594
+ */
595
+ async reviewFile(filePath) {
596
+ const absPath = resolve(this.projectRoot, filePath);
597
+ if (!existsSync(absPath)) {
598
+ return this.emptyReport([filePath]);
599
+ }
600
+ const analysis = this.analyzeFile(absPath);
601
+ if (!analysis) {
602
+ return this.emptyReport([filePath]);
603
+ }
604
+ // Check exclusion
605
+ if (this.isExcluded(filePath)) {
606
+ return this.emptyReport([filePath]);
607
+ }
608
+ const comments = this.runPatterns(analysis);
609
+ this.comments = this.comments.concat(comments);
610
+ return this.buildReport([filePath], comments);
611
+ }
612
+ /**
613
+ * Review all uncommitted changes (or changes vs. a base branch).
614
+ */
615
+ async reviewChanges(baseBranch) {
616
+ const changedFiles = this.getChangedFiles(baseBranch);
617
+ const allComments = [];
618
+ for (const file of changedFiles) {
619
+ const absPath = resolve(this.projectRoot, file);
620
+ if (!existsSync(absPath))
621
+ continue;
622
+ if (this.isExcluded(file))
623
+ continue;
624
+ const analysis = this.analyzeFile(absPath);
625
+ if (!analysis)
626
+ continue;
627
+ const comments = this.runPatterns(analysis);
628
+ allComments.push(...comments);
629
+ }
630
+ this.comments = this.comments.concat(allComments);
631
+ return this.buildReport(changedFiles, allComments);
632
+ }
633
+ /**
634
+ * Review a diff string for issues.
635
+ * Only analyzes the new/changed lines in the diff.
636
+ */
637
+ async reviewDiff(diff) {
638
+ const parsed = this.parseDiff(diff);
639
+ const allComments = [];
640
+ const reviewedFiles = [];
641
+ for (const [filePath, changedLines] of parsed) {
642
+ const absPath = resolve(this.projectRoot, filePath);
643
+ if (!existsSync(absPath))
644
+ continue;
645
+ if (this.isExcluded(filePath))
646
+ continue;
647
+ const analysis = this.analyzeFile(absPath);
648
+ if (!analysis)
649
+ continue;
650
+ // Only check patterns on changed lines
651
+ const comments = this.runPatterns(analysis).filter(comment => changedLines.includes(comment.line));
652
+ allComments.push(...comments);
653
+ reviewedFiles.push(filePath);
654
+ }
655
+ this.comments = this.comments.concat(allComments);
656
+ return this.buildReport(reviewedFiles, allComments);
657
+ }
658
+ /**
659
+ * Review a GitHub PR by URL.
660
+ * Fetches the diff and reviews it.
661
+ */
662
+ async reviewPR(prUrl) {
663
+ // Extract owner, repo, and PR number from URL
664
+ const prMatch = prUrl.match(/github\.com\/([^/]+)\/([^/]+)\/pull\/(\d+)/);
665
+ if (!prMatch) {
666
+ throw new Error(`Invalid GitHub PR URL: ${prUrl}`);
667
+ }
668
+ const [, owner, repo, prNumber] = prMatch;
669
+ try {
670
+ // Use gh CLI to fetch the diff
671
+ const diff = execSync(`gh pr diff ${prNumber} --repo ${owner}/${repo}`, {
672
+ encoding: 'utf-8',
673
+ timeout: 30_000,
674
+ cwd: this.projectRoot,
675
+ });
676
+ return this.reviewDiff(diff);
677
+ }
678
+ catch (error) {
679
+ // Fallback: try curl with GitHub API
680
+ try {
681
+ const diff = execSync(`curl -s -H "Accept: application/vnd.github.v3.diff" https://api.github.com/repos/${owner}/${repo}/pulls/${prNumber}`, {
682
+ encoding: 'utf-8',
683
+ timeout: 30_000,
684
+ cwd: this.projectRoot,
685
+ });
686
+ return this.reviewDiff(diff);
687
+ }
688
+ catch {
689
+ throw new Error(`Failed to fetch PR diff. Ensure 'gh' CLI is installed and authenticated, or the repository is public.`);
690
+ }
691
+ }
692
+ }
693
+ /**
694
+ * Get review comments for a specific file.
695
+ */
696
+ getReviewComments(filePath) {
697
+ if (filePath) {
698
+ const absPath = resolve(this.projectRoot, filePath);
699
+ return this.comments.filter(c => resolve(this.projectRoot, c.file) === absPath);
700
+ }
701
+ return [...this.comments];
702
+ }
703
+ /**
704
+ * Filter comments by severity level.
705
+ * Returns comments at or above the given severity threshold.
706
+ */
707
+ severityFilter(severity) {
708
+ const threshold = SEVERITY_ORDER[severity];
709
+ return this.comments.filter(c => SEVERITY_ORDER[c.severity] <= threshold);
710
+ }
711
+ /**
712
+ * Generate a full review report covering all tracked comments.
713
+ */
714
+ generateReport() {
715
+ const files = [...new Set(this.comments.map(c => c.file))];
716
+ return this.buildReport(files, this.comments);
717
+ }
718
+ /**
719
+ * Set review focus areas. Only patterns in these categories will be evaluated.
720
+ */
721
+ setFocus(areas) {
722
+ this.focusAreas = new Set(areas);
723
+ this.config.focusAreas = areas;
724
+ }
725
+ /**
726
+ * Register a callback for review reports.
727
+ */
728
+ onReviewResult(callback) {
729
+ this.callbacks.push(callback);
730
+ }
731
+ /**
732
+ * Remove a previously registered callback.
733
+ */
734
+ offReviewResult(callback) {
735
+ this.callbacks = this.callbacks.filter(cb => cb !== callback);
736
+ }
737
+ /**
738
+ * Get the current review configuration.
739
+ */
740
+ getConfig() {
741
+ return { ...this.config };
742
+ }
743
+ /**
744
+ * Update the review configuration.
745
+ */
746
+ updateConfig(updates) {
747
+ Object.assign(this.config, updates);
748
+ if (updates.focusAreas) {
749
+ this.focusAreas = new Set(updates.focusAreas);
750
+ }
751
+ }
752
+ /**
753
+ * Clear all stored comments.
754
+ */
755
+ clearComments() {
756
+ this.comments = [];
757
+ this.commentCounter = 0;
758
+ }
759
+ /**
760
+ * Get all historical reports.
761
+ */
762
+ getReports() {
763
+ return [...this.reports];
764
+ }
765
+ /**
766
+ * Print a review report summary.
767
+ */
768
+ printSummary(report) {
769
+ console.log('');
770
+ console.log('--- Code Review Summary ---');
771
+ console.log(` Report ID: ${report.id}`);
772
+ console.log(` Timestamp: ${new Date(report.timestamp).toISOString()}`);
773
+ console.log(` Files: ${report.files.length}`);
774
+ console.log(` Score: ${report.score}/100`);
775
+ console.log('');
776
+ const s = report.summary;
777
+ console.log(' Summary:');
778
+ console.log(` Critical: ${s.critical}`);
779
+ console.log(` Major: ${s.major}`);
780
+ console.log(` Minor: ${s.minor}`);
781
+ console.log(` Suggestions:${s.suggestions}`);
782
+ console.log(` Total: ${s.totalComments}`);
783
+ console.log('');
784
+ if (Object.keys(s.categories).length > 0) {
785
+ console.log(' By Category:');
786
+ for (const [category, count] of Object.entries(s.categories)) {
787
+ console.log(` ${category}: ${count}`);
788
+ }
789
+ console.log('');
790
+ }
791
+ // Print top critical/major comments
792
+ const significant = report.comments
793
+ .filter(c => c.severity === 'critical' || c.severity === 'major')
794
+ .sort((a, b) => SEVERITY_ORDER[a.severity] - SEVERITY_ORDER[b.severity])
795
+ .slice(0, 15);
796
+ if (significant.length > 0) {
797
+ console.log(' Significant Issues:');
798
+ for (const comment of significant) {
799
+ const sevLabel = comment.severity === 'critical' ? '!!' : '!';
800
+ console.log(` [${sevLabel}] ${comment.file}:${comment.line} — ${comment.rule ?? 'N/A'}: ${comment.message}`);
801
+ if (comment.suggestion) {
802
+ console.log(` → ${comment.suggestion}`);
803
+ }
804
+ }
805
+ console.log('');
806
+ }
807
+ console.log('----------------------------');
808
+ console.log('');
809
+ }
810
+ // ----------------------------------------------------------
811
+ // Private helpers
812
+ // ----------------------------------------------------------
813
+ analyzeFile(absPath) {
814
+ try {
815
+ const content = readFileSync(absPath, 'utf-8');
816
+ const lines = content.split('\n');
817
+ const ext = extname(absPath).toLowerCase();
818
+ const language = EXTENSION_LANGUAGE_MAP[ext] ?? 'unknown';
819
+ return {
820
+ path: relative(this.projectRoot, absPath),
821
+ content,
822
+ lines,
823
+ extension: ext,
824
+ language,
825
+ lineCount: lines.length,
826
+ };
827
+ }
828
+ catch {
829
+ return null;
830
+ }
831
+ }
832
+ runPatterns(analysis) {
833
+ const comments = [];
834
+ for (const pattern of REVIEW_PATTERNS) {
835
+ // Skip if focus area doesn't include this category
836
+ if (!this.focusAreas.has(pattern.category))
837
+ continue;
838
+ // Skip if pattern is language-specific and doesn't match
839
+ if (pattern.languages &&
840
+ pattern.languages.length > 0 &&
841
+ !pattern.languages.includes(analysis.language)) {
842
+ continue;
843
+ }
844
+ // Skip if below severity threshold
845
+ if (SEVERITY_ORDER[pattern.severity] > SEVERITY_ORDER[this.config.severityThreshold]) {
846
+ continue;
847
+ }
848
+ // Run pattern against each line
849
+ for (let i = 0; i < analysis.lines.length; i++) {
850
+ const line = analysis.lines[i];
851
+ const match = pattern.pattern.test(line);
852
+ if (match) {
853
+ this.commentCounter++;
854
+ comments.push({
855
+ id: `RC-${this.commentCounter.toString().padStart(5, '0')}`,
856
+ file: analysis.path,
857
+ line: i + 1,
858
+ severity: pattern.severity,
859
+ category: pattern.category,
860
+ message: pattern.message,
861
+ suggestion: pattern.suggestion,
862
+ rule: pattern.rule,
863
+ });
864
+ // Reset regex lastIndex for patterns with global flag
865
+ pattern.pattern.lastIndex = 0;
866
+ }
867
+ }
868
+ // Multi-line patterns: check against full content
869
+ if (pattern.multiline) {
870
+ const fullMatch = pattern.pattern.test(analysis.content);
871
+ if (fullMatch) {
872
+ // Find the approximate line number
873
+ const matchIndex = analysis.content.search(pattern.pattern);
874
+ const lineNumber = matchIndex >= 0
875
+ ? analysis.content.substring(0, matchIndex).split('\n').length
876
+ : 1;
877
+ // Check if we already added this comment for this line
878
+ const alreadyAdded = comments.some(c => c.file === analysis.path && c.line === lineNumber && c.rule === pattern.rule);
879
+ if (!alreadyAdded) {
880
+ this.commentCounter++;
881
+ comments.push({
882
+ id: `RC-${this.commentCounter.toString().padStart(5, '0')}`,
883
+ file: analysis.path,
884
+ line: lineNumber,
885
+ severity: pattern.severity,
886
+ category: pattern.category,
887
+ message: pattern.message,
888
+ suggestion: pattern.suggestion,
889
+ rule: pattern.rule,
890
+ });
891
+ }
892
+ pattern.pattern.lastIndex = 0;
893
+ }
894
+ }
895
+ }
896
+ // Add complexity analysis based on file structure
897
+ comments.push(...this.analyzeComplexity(analysis));
898
+ return comments;
899
+ }
900
+ analyzeComplexity(analysis) {
901
+ const comments = [];
902
+ // File too long
903
+ if (analysis.lineCount > 500) {
904
+ this.commentCounter++;
905
+ comments.push({
906
+ id: `RC-${this.commentCounter.toString().padStart(5, '0')}`,
907
+ file: analysis.path,
908
+ line: 1,
909
+ severity: analysis.lineCount > 1000 ? 'major' : 'minor',
910
+ category: 'complexity',
911
+ message: `File is ${analysis.lineCount} lines long — consider splitting into smaller modules.`,
912
+ suggestion: 'Extract logical sections into separate files/modules.',
913
+ rule: 'COMPLEX004',
914
+ });
915
+ }
916
+ // Detect deep nesting
917
+ let maxNesting = 0;
918
+ let maxNestingLine = 0;
919
+ for (let i = 0; i < analysis.lines.length; i++) {
920
+ const line = analysis.lines[i];
921
+ const leadingSpaces = line.match(/^(\s*)/)?.[1].length ?? 0;
922
+ const nesting = Math.floor(leadingSpaces / 2); // Assuming 2-space indent
923
+ if (nesting > maxNesting) {
924
+ maxNesting = nesting;
925
+ maxNestingLine = i + 1;
926
+ }
927
+ }
928
+ if (maxNesting > 5) {
929
+ this.commentCounter++;
930
+ comments.push({
931
+ id: `RC-${this.commentCounter.toString().padStart(5, '0')}`,
932
+ file: analysis.path,
933
+ line: maxNestingLine,
934
+ severity: 'major',
935
+ category: 'complexity',
936
+ message: `Deep nesting detected (level ${maxNesting}) — code is hard to follow.`,
937
+ suggestion: 'Use guard clauses, early returns, or extract nested logic into functions.',
938
+ rule: 'COMPLEX005',
939
+ });
940
+ }
941
+ // Detect long functions (simple heuristic)
942
+ let functionStart = -1;
943
+ let functionIndent = 0;
944
+ for (let i = 0; i < analysis.lines.length; i++) {
945
+ const line = analysis.lines[i];
946
+ const trimmed = line.trim();
947
+ // Detect function starts (JS/TS, Python, Go)
948
+ if (/^(export\s+)?(async\s+)?function\s|^(const|let|var)\s+\w+\s*=\s*(async\s*)?\(|^def\s+\w+|^func\s+\w+/.test(trimmed)) {
949
+ if (functionStart >= 0 && i - functionStart > 50) {
950
+ this.commentCounter++;
951
+ comments.push({
952
+ id: `RC-${this.commentCounter.toString().padStart(5, '0')}`,
953
+ file: analysis.path,
954
+ line: functionStart + 1,
955
+ severity: 'minor',
956
+ category: 'complexity',
957
+ message: `Function is ${i - functionStart} lines long — consider breaking it up.`,
958
+ suggestion: 'Extract logical sections into well-named helper functions.',
959
+ rule: 'COMPLEX006',
960
+ });
961
+ }
962
+ functionStart = i;
963
+ functionIndent = (line.match(/^(\s*)/)?.[1].length ?? 0);
964
+ }
965
+ // If we're back at same or lower indent and it's a closing brace/end
966
+ if (functionStart >= 0 &&
967
+ i > functionStart &&
968
+ trimmed.startsWith('}') &&
969
+ (line.match(/^(\s*)/)?.[1].length ?? 0) <= functionIndent) {
970
+ if (i - functionStart > 50) {
971
+ this.commentCounter++;
972
+ comments.push({
973
+ id: `RC-${this.commentCounter.toString().padStart(5, '0')}`,
974
+ file: analysis.path,
975
+ line: functionStart + 1,
976
+ severity: 'minor',
977
+ category: 'complexity',
978
+ message: `Function is ${i - functionStart} lines long — consider breaking it up.`,
979
+ suggestion: 'Extract logical sections into well-named helper functions.',
980
+ rule: 'COMPLEX006',
981
+ });
982
+ }
983
+ functionStart = -1;
984
+ }
985
+ }
986
+ return comments;
987
+ }
988
+ buildReport(files, comments) {
989
+ const id = `RR-${Date.now().toString(36)}-${Math.random().toString(36).substring(2, 8)}`;
990
+ const timestamp = Date.now();
991
+ // Build summary
992
+ const categories = {};
993
+ let critical = 0;
994
+ let major = 0;
995
+ let minor = 0;
996
+ let suggestions = 0;
997
+ for (const comment of comments) {
998
+ switch (comment.severity) {
999
+ case 'critical':
1000
+ critical++;
1001
+ break;
1002
+ case 'major':
1003
+ major++;
1004
+ break;
1005
+ case 'minor':
1006
+ minor++;
1007
+ break;
1008
+ case 'suggestion':
1009
+ suggestions++;
1010
+ break;
1011
+ default: break;
1012
+ }
1013
+ categories[comment.category] = (categories[comment.category] ?? 0) + 1;
1014
+ }
1015
+ const summary = {
1016
+ totalComments: comments.length,
1017
+ critical,
1018
+ major,
1019
+ minor,
1020
+ suggestions,
1021
+ categories,
1022
+ };
1023
+ // Calculate quality score (0-100)
1024
+ const score = this.calculateScore(summary, files.length);
1025
+ const report = {
1026
+ id,
1027
+ timestamp,
1028
+ files,
1029
+ comments,
1030
+ summary,
1031
+ score,
1032
+ };
1033
+ this.reports.push(report);
1034
+ this.emit(report);
1035
+ return report;
1036
+ }
1037
+ /**
1038
+ * Calculate a code quality score from 0 to 100.
1039
+ * Start at 100 and deduct points based on issue severity.
1040
+ */
1041
+ calculateScore(summary, fileCount) {
1042
+ let score = 100;
1043
+ // Deductions per severity
1044
+ score -= summary.critical * 15;
1045
+ score -= summary.major * 5;
1046
+ score -= summary.minor * 2;
1047
+ score -= summary.suggestions * 0.5;
1048
+ // Normalize by file count (more files = more expected issues)
1049
+ if (fileCount > 1) {
1050
+ const normalization = Math.max(0.5, 1 - (fileCount - 1) * 0.02);
1051
+ const deduction = 100 - score;
1052
+ score = 100 - deduction * normalization;
1053
+ }
1054
+ return Math.max(0, Math.min(100, Math.round(score)));
1055
+ }
1056
+ emptyReport(files) {
1057
+ const id = `RR-${Date.now().toString(36)}-${Math.random().toString(36).substring(2, 8)}`;
1058
+ return {
1059
+ id,
1060
+ timestamp: Date.now(),
1061
+ files,
1062
+ comments: [],
1063
+ summary: {
1064
+ totalComments: 0,
1065
+ critical: 0,
1066
+ major: 0,
1067
+ minor: 0,
1068
+ suggestions: 0,
1069
+ categories: {},
1070
+ },
1071
+ score: 100,
1072
+ };
1073
+ }
1074
+ emit(report) {
1075
+ for (const cb of this.callbacks) {
1076
+ try {
1077
+ cb(report);
1078
+ }
1079
+ catch {
1080
+ // Callback errors should not interrupt the flow
1081
+ }
1082
+ }
1083
+ }
1084
+ isExcluded(filePath) {
1085
+ for (const pattern of this.config.excludePatterns) {
1086
+ const regex = this.globToRegex(pattern);
1087
+ if (regex.test(filePath))
1088
+ return true;
1089
+ }
1090
+ return false;
1091
+ }
1092
+ globToRegex(pattern) {
1093
+ const escaped = pattern
1094
+ .replace(/[.+^${}()|[\]\\]/g, '\\$&')
1095
+ .replace(/\*\*/g, '{{GLOBSTAR}}')
1096
+ .replace(/\*/g, '[^/]*')
1097
+ .replace(/\?/g, '[^/]')
1098
+ .replace(/\{\{GLOBSTAR\}\}/g, '.*');
1099
+ return new RegExp('^' + escaped + '$');
1100
+ }
1101
+ /**
1102
+ * Get changed files from git (uncommitted or vs. a base branch).
1103
+ */
1104
+ getChangedFiles(baseBranch) {
1105
+ try {
1106
+ if (baseBranch) {
1107
+ const output = execSync(`git diff --name-only ${baseBranch}...HEAD`, { encoding: 'utf-8', cwd: this.projectRoot, timeout: 10_000 });
1108
+ return output.trim().split('\n').filter(Boolean);
1109
+ }
1110
+ // Uncommitted changes (staged + unstaged)
1111
+ const staged = execSync('git diff --name-only --cached', {
1112
+ encoding: 'utf-8',
1113
+ cwd: this.projectRoot,
1114
+ timeout: 10_000,
1115
+ });
1116
+ const unstaged = execSync('git diff --name-only', {
1117
+ encoding: 'utf-8',
1118
+ cwd: this.projectRoot,
1119
+ timeout: 10_000,
1120
+ });
1121
+ const untracked = execSync('git ls-files --others --exclude-standard', {
1122
+ encoding: 'utf-8',
1123
+ cwd: this.projectRoot,
1124
+ timeout: 10_000,
1125
+ });
1126
+ const allFiles = [
1127
+ ...staged.trim().split('\n'),
1128
+ ...unstaged.trim().split('\n'),
1129
+ ...untracked.trim().split('\n'),
1130
+ ];
1131
+ return [...new Set(allFiles.filter(Boolean))];
1132
+ }
1133
+ catch {
1134
+ return [];
1135
+ }
1136
+ }
1137
+ /**
1138
+ * Parse a unified diff into a map of file paths to changed line numbers.
1139
+ */
1140
+ parseDiff(diff) {
1141
+ const result = new Map();
1142
+ let currentFile = '';
1143
+ let currentLines = [];
1144
+ let lineOffset = 0;
1145
+ const lines = diff.split('\n');
1146
+ for (const line of lines) {
1147
+ // New file in diff
1148
+ const fileMatch = line.match(/^---\s+a\/(.+)$|^diff --git\s+a\/(.+?)\s+b\/(.+)$/);
1149
+ if (fileMatch) {
1150
+ if (currentFile && currentLines.length > 0) {
1151
+ result.set(currentFile, currentLines);
1152
+ }
1153
+ currentFile = fileMatch[1] ?? fileMatch[3] ?? '';
1154
+ currentLines = [];
1155
+ lineOffset = 0;
1156
+ continue;
1157
+ }
1158
+ // Also handle +++ line
1159
+ const plusFileMatch = line.match(/^\+\+\+\s+b\/(.+)$/);
1160
+ if (plusFileMatch && !currentFile) {
1161
+ currentFile = plusFileMatch[1];
1162
+ currentLines = [];
1163
+ continue;
1164
+ }
1165
+ // Hunk header: @@ -a,b +c,d @@
1166
+ const hunkMatch = line.match(/^@@\s+-\d+(?:,\d+)?\s+\+(\d+)(?:,(\d+))?\s+@@/);
1167
+ if (hunkMatch) {
1168
+ lineOffset = parseInt(hunkMatch[1], 10) - 1;
1169
+ continue;
1170
+ }
1171
+ // Added line
1172
+ if (line.startsWith('+') && !line.startsWith('+++')) {
1173
+ lineOffset++;
1174
+ currentLines.push(lineOffset);
1175
+ }
1176
+ else if (line.startsWith('-')) {
1177
+ // Removed line — don't increment offset
1178
+ }
1179
+ else {
1180
+ // Context line
1181
+ lineOffset++;
1182
+ }
1183
+ }
1184
+ // Flush last file
1185
+ if (currentFile && currentLines.length > 0) {
1186
+ result.set(currentFile, currentLines);
1187
+ }
1188
+ return result;
1189
+ }
1190
+ }
1191
+ //# sourceMappingURL=code-review.js.map