minovative-mind-cli 2.5.1 → 2.6.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 (43) hide show
  1. package/README.md +28 -25
  2. package/dist/commands/chat.js +1 -1
  3. package/dist/services/agent/inputHandler.d.ts +9 -0
  4. package/dist/services/agent/inputHandler.js +34 -0
  5. package/dist/services/agent/slashCommands.js +158 -37
  6. package/dist/services/agent/syntaxAgent.d.ts +40 -0
  7. package/dist/services/agent/syntaxAgent.js +237 -23
  8. package/dist/services/agent/toolLoop.js +10 -1
  9. package/dist/services/agent/types.d.ts +1 -0
  10. package/dist/services/agent-tools.d.ts +156 -1
  11. package/dist/services/agent-tools.js +259 -67
  12. package/dist/services/agent.d.ts +74 -0
  13. package/dist/services/agent.js +192 -30
  14. package/dist/services/ai.d.ts +5 -0
  15. package/dist/services/ai.js +80 -87
  16. package/dist/services/chatHistoryService.d.ts +11 -0
  17. package/dist/services/chatHistoryService.js +20 -1
  18. package/dist/services/contextAgent.d.ts +1 -1
  19. package/dist/services/contextAgent.js +9 -29
  20. package/dist/services/orchestration/investigationAgent.d.ts +2 -1
  21. package/dist/services/orchestration/investigationAgent.js +7 -2
  22. package/dist/services/orchestration/investigationOrchestrator.d.ts +1 -1
  23. package/dist/services/orchestration/investigationOrchestrator.js +13 -2
  24. package/dist/services/orchestration/orchestrator.js +21 -4
  25. package/dist/services/orchestration/subAgent.d.ts +2 -1
  26. package/dist/services/orchestration/subAgent.js +12 -6
  27. package/dist/services/workspaceRegistry.d.ts +7 -0
  28. package/dist/services/workspaceRegistry.js +22 -0
  29. package/dist/utils/analysisRunner.d.ts +27 -4
  30. package/dist/utils/analysisRunner.js +100 -20
  31. package/dist/utils/config.d.ts +2 -0
  32. package/dist/utils/config.js +2 -0
  33. package/dist/utils/fuzzyMatch.d.ts +32 -0
  34. package/dist/utils/fuzzyMatch.js +215 -27
  35. package/dist/utils/localSyntaxValidator.d.ts +2 -2
  36. package/dist/utils/localSyntaxValidator.js +280 -81
  37. package/dist/utils/performanceAuditor.d.ts +2 -7
  38. package/dist/utils/performanceAuditor.js +541 -89
  39. package/dist/utils/projectStorage.js +9 -0
  40. package/dist/utils/systemPrompts.d.ts +3 -2
  41. package/dist/utils/systemPrompts.js +29 -5
  42. package/oclif.manifest.json +2 -2
  43. package/package.json +1 -1
@@ -1,20 +1,31 @@
1
1
  import path from 'node:path';
2
2
  import pc from 'picocolors';
3
3
  const EXTENSION_MAP = {
4
- '.ts': 'js', '.tsx': 'js', '.js': 'js', '.jsx': 'js', '.mjs': 'js', '.cjs': 'js',
5
- '.py': 'python', '.pyw': 'python',
4
+ '.ts': 'js',
5
+ '.tsx': 'js',
6
+ '.js': 'js',
7
+ '.jsx': 'js',
8
+ '.mjs': 'js',
9
+ '.cjs': 'js',
10
+ '.py': 'python',
11
+ '.pyw': 'python',
6
12
  '.go': 'go',
7
13
  '.rs': 'rust',
8
14
  '.php': 'php',
15
+ '.cs': 'csharp',
16
+ '.java': 'java',
17
+ '.cpp': 'cpp',
18
+ '.hpp': 'cpp',
19
+ '.cc': 'cpp',
20
+ '.cxx': 'cpp',
21
+ '.c': 'cpp',
22
+ '.h': 'cpp',
23
+ '.rb': 'ruby',
9
24
  };
10
25
  function detectLanguage(filePath) {
11
26
  return EXTENSION_MAP[path.extname(filePath).toLowerCase()] ?? 'unknown';
12
27
  }
13
28
  // ─── Comment & String Stripping ──────────────────────────────────────
14
- //
15
- // Before running regex-based detection, we strip string literals and
16
- // comments from the source to prevent false positives on patterns
17
- // appearing inside strings or documentation.
18
29
  /**
19
30
  * Replaces the content of string literals and comments with whitespace,
20
31
  * preserving line count so that line-number tracking remains accurate.
@@ -27,32 +38,77 @@ function stripStringsAndComments(source, lang) {
27
38
  while (i < source.length) {
28
39
  const ch = source[i];
29
40
  const next = source[i + 1];
30
- // ── JS/TS/Go/Rust/PHP: line comments (//) ─────────────────────
31
- if ((lang === 'js' || lang === 'go' || lang === 'rust' || lang === 'php') && ch === '/' && next === '/') {
41
+ // ── C-family: line comments (//) ──────────────────────────────
42
+ if (['js', 'go', 'rust', 'php', 'csharp', 'java', 'cpp'].includes(lang) && ch === '/' && next === '/') {
32
43
  const eol = source.indexOf('\n', i);
33
44
  const end = eol === -1 ? source.length : eol;
34
45
  result += ' '.repeat(end - i);
35
46
  i = end;
36
47
  continue;
37
48
  }
38
- // ── JS/TS/Go/Rust/PHP: block comments (/* ... */) ─────────────
39
- if ((lang === 'js' || lang === 'go' || lang === 'rust' || lang === 'php') && ch === '/' && next === '*') {
49
+ // ── C-family: block comments (/* ... */) ─────────────────────
50
+ if (['js', 'go', 'rust', 'php', 'csharp', 'java', 'cpp'].includes(lang) && ch === '/' && next === '*') {
40
51
  const end = source.indexOf('*/', i + 2);
41
52
  const closePos = end === -1 ? source.length : end + 2;
42
- // Preserve newlines for line counting
43
53
  const segment = source.substring(i, closePos);
44
54
  result += segment.replace(/[^\n]/g, ' ');
45
55
  i = closePos;
46
56
  continue;
47
57
  }
48
- // ── Python/PHP: line comments (#) ─────────────────────────────
49
- if ((lang === 'python' || lang === 'php') && ch === '#') {
58
+ // ── Hash comment languages: (#) ──────────────────────────────
59
+ if (['python', 'php', 'ruby'].includes(lang) && ch === '#') {
50
60
  const eol = source.indexOf('\n', i);
51
61
  const end = eol === -1 ? source.length : eol;
52
62
  result += ' '.repeat(end - i);
53
63
  i = end;
54
64
  continue;
55
65
  }
66
+ // ── Ruby: block comments (=begin ... =end) ────────────────────
67
+ if (lang === 'ruby' && (i === 0 || source[i - 1] === '\n') && source.substring(i, i + 6) === '=begin') {
68
+ const end = source.indexOf('\n=end', i + 6);
69
+ const closePos = end === -1 ? source.length : end + 5;
70
+ const segment = source.substring(i, closePos);
71
+ result += segment.replace(/[^\n]/g, ' ');
72
+ i = closePos;
73
+ continue;
74
+ }
75
+ // ── C#: Raw string literals ("""...""") ────────────────────────
76
+ if (lang === 'csharp' && source.substring(i, i + 3) === '"""') {
77
+ const end = source.indexOf('"""', i + 3);
78
+ const closePos = end === -1 ? source.length : end + 3;
79
+ const segment = source.substring(i, closePos);
80
+ result += segment.replace(/[^\n]/g, ' ');
81
+ i = closePos;
82
+ continue;
83
+ }
84
+ // ── C#: Verbatim string literals (@"...") ─────────────────────
85
+ if (lang === 'csharp' && ch === '@' && next === '"') {
86
+ let j = i + 2;
87
+ while (j < source.length) {
88
+ if (source[j] === '"' && source[j + 1] === '"') {
89
+ j += 2;
90
+ continue;
91
+ }
92
+ if (source[j] === '"') {
93
+ j++;
94
+ break;
95
+ }
96
+ j++;
97
+ }
98
+ const segment = source.substring(i, j);
99
+ result += segment.replace(/[^\n]/g, ' ');
100
+ i = j;
101
+ continue;
102
+ }
103
+ // ── C++: Raw string literals R"(...)" ─────────────────────────
104
+ if (lang === 'cpp' && ch === 'R' && next === '"') {
105
+ const end = source.indexOf(')"', i + 2);
106
+ const closePos = end === -1 ? source.length : end + 2;
107
+ const segment = source.substring(i, closePos);
108
+ result += segment.replace(/[^\n]/g, ' ');
109
+ i = closePos;
110
+ continue;
111
+ }
56
112
  // ── Python: triple-quoted strings ─────────────────────────
57
113
  if (lang === 'python' && (source.substring(i, i + 3) === '"""' || source.substring(i, i + 3) === "'''")) {
58
114
  const quote = source.substring(i, i + 3);
@@ -67,7 +123,6 @@ function stripStringsAndComments(source, lang) {
67
123
  if (lang === 'php' && ch === '<' && next === '<' && source[i + 2] === '<') {
68
124
  const eol = source.indexOf('\n', i);
69
125
  if (eol !== -1) {
70
- // e.g. <<<EOF or <<<'EOF'
71
126
  const idMatch = source.substring(i + 3, eol).match(/[A-Za-z0-9_]+/);
72
127
  if (idMatch) {
73
128
  const identifier = idMatch[0];
@@ -83,26 +138,24 @@ function stripStringsAndComments(source, lang) {
83
138
  }
84
139
  }
85
140
  }
86
- // ── All languages: string literals ────────────────────────
141
+ // ── All languages: generic string literals ────────────────────
87
142
  if (ch === '"' || ch === "'" || (lang === 'js' && ch === '`')) {
88
143
  const quote = ch;
89
144
  let j = i + 1;
90
145
  while (j < source.length) {
91
146
  if (source[j] === '\\') {
92
- j += 2; // skip escaped character
147
+ j += 2;
93
148
  continue;
94
149
  }
95
150
  if (source[j] === quote) {
96
151
  j++;
97
152
  break;
98
153
  }
99
- // Template literals can span multiple lines
100
154
  if (quote === '`' && source[j] === '\n') {
101
155
  result += ' '.repeat(j - i);
102
156
  result += '\n';
103
157
  i = j + 1;
104
158
  j = i;
105
- // Reset — we'll pick up again from the new line
106
159
  continue;
107
160
  }
108
161
  j++;
@@ -117,10 +170,83 @@ function stripStringsAndComments(source, lang) {
117
170
  }
118
171
  return result;
119
172
  }
120
- // ─── Utility: Line Number Lookup ─────────────────────────────────────
173
+ // ─── Directives Handling (perf-ignore) ────────────────────────────────
121
174
  /**
122
- * Given a character offset in the source, returns the 1-indexed line number.
175
+ * Filters out findings that are suppressed via comment directives in raw source.
176
+ * Supported directives:
177
+ * // perf-ignore
178
+ * // perf-ignore: PERF-001, PERF-009
179
+ * // perf-ignore-file
180
+ * // perf-ignore-file: PERF-003
181
+ * Works with //, #, /* ... * /, -- comment styles across languages.
123
182
  */
183
+ function applyDirectiveSuppression(findings, rawSource) {
184
+ if (findings.length === 0)
185
+ return findings;
186
+ const lines = rawSource.split('\n');
187
+ const fileIgnoredCodes = new Set();
188
+ let ignoreAllFile = false;
189
+ const lineIgnoredCodesMap = new Map();
190
+ const directiveRegex = /(?:\/\/|#|\/\*|--)\s*perf-ignore(-file)?(?:[:\s]+([A-Za-z0-9_,\s-]+))?/i;
191
+ for (let idx = 0; idx < lines.length; idx++) {
192
+ const lineNum = idx + 1;
193
+ const lineStr = lines[idx];
194
+ const match = lineStr.match(directiveRegex);
195
+ if (!match)
196
+ continue;
197
+ const isFileLevel = Boolean(match[1]);
198
+ const codesRaw = match[2]?.trim();
199
+ const parsedCodes = codesRaw
200
+ ? codesRaw
201
+ .split(/[\s,]+/)
202
+ .map((c) => c.trim().toUpperCase())
203
+ .filter(Boolean)
204
+ : [];
205
+ if (isFileLevel) {
206
+ if (parsedCodes.length === 0) {
207
+ ignoreAllFile = true;
208
+ }
209
+ else {
210
+ for (const code of parsedCodes) {
211
+ fileIgnoredCodes.add(code);
212
+ }
213
+ }
214
+ }
215
+ else {
216
+ // Line-level directive suppresses findings on the directive line AND the following line
217
+ const targetLines = [lineNum, lineNum + 1];
218
+ for (const targetLine of targetLines) {
219
+ let lineCodes = lineIgnoredCodesMap.get(targetLine);
220
+ if (!lineCodes) {
221
+ lineCodes = new Set();
222
+ lineIgnoredCodesMap.set(targetLine, lineCodes);
223
+ }
224
+ if (parsedCodes.length === 0) {
225
+ lineCodes.add('*');
226
+ }
227
+ else {
228
+ for (const code of parsedCodes) {
229
+ lineCodes.add(code);
230
+ }
231
+ }
232
+ }
233
+ }
234
+ }
235
+ if (ignoreAllFile)
236
+ return [];
237
+ return findings.filter((finding) => {
238
+ const code = finding.code.toUpperCase();
239
+ if (fileIgnoredCodes.has(code))
240
+ return false;
241
+ const lineCodes = lineIgnoredCodesMap.get(finding.line);
242
+ if (lineCodes) {
243
+ if (lineCodes.has('*') || lineCodes.has(code))
244
+ return false;
245
+ }
246
+ return true;
247
+ });
248
+ }
249
+ // ─── Utility: Line Number Lookup ─────────────────────────────────────
124
250
  function getLineNumber(source, offset) {
125
251
  let line = 1;
126
252
  for (let i = 0; i < offset && i < source.length; i++) {
@@ -130,16 +256,15 @@ function getLineNumber(source, offset) {
130
256
  return line;
131
257
  }
132
258
  // ── PERF-001: Nested Loop Detection (O(n²) risk) ────────────────────
133
- //
134
- // Strategy: Track brace-depth to identify loop constructs that contain
135
- // other loop constructs. Works across all C-family languages + Python.
136
259
  const detectNestedLoops = (source, stripped, lang, findings) => {
137
- // Python uses indentation, not braces — use a different strategy
138
260
  if (lang === 'python') {
139
261
  detectNestedLoopsPython(source, stripped, findings);
140
262
  return;
141
263
  }
142
- // C-family: scan for loop keywords and track brace depth
264
+ if (lang === 'ruby') {
265
+ detectNestedLoopsRuby(source, stripped, findings);
266
+ return;
267
+ }
143
268
  const loopPattern = /\b(for|foreach|while|do)\s*[\s(]|\.(forEach|map|filter|reduce|flatMap)\s*\(/g;
144
269
  const lines = stripped.split('\n');
145
270
  for (let i = 0; i < lines.length; i++) {
@@ -149,7 +274,6 @@ const detectNestedLoops = (source, stripped, lang, findings) => {
149
274
  continue;
150
275
  }
151
276
  loopPattern.lastIndex = 0;
152
- // Found a loop — scan forward tracking brace depth to find nested loops
153
277
  let depth = 0;
154
278
  let foundOpening = false;
155
279
  for (let j = i; j < lines.length; j++) {
@@ -163,20 +287,24 @@ const detectNestedLoops = (source, stripped, lang, findings) => {
163
287
  depth--;
164
288
  }
165
289
  }
166
- // Check lines inside the outer loop body for nested loops
167
290
  if (j > i && foundOpening && depth > 0) {
291
+ // Context-aware heuristic: Ignore nested loops over small constant literals/bounds (e.g. i < 2, ['a', 'b'])
168
292
  if (/\b(for|foreach|while|do)\s*[\s(]|\.(forEach|map|filter|reduce|flatMap)\s*\(/.test(innerLine)) {
293
+ if (/for\s*\([^;]*;\s*[\w$]+\s*<=\s*([0-9]|10)\b/.test(innerLine) ||
294
+ /for\s*\([^;]*;\s*[\w$]+\s*<\s*([0-9]|10)\b/.test(innerLine) ||
295
+ /\[\s*(?:\d+|'[^']+'|"[^"]+")\s*,\s*(?:\d+|'[^']+'|"[^"]+")\s*\]/.test(innerLine)) {
296
+ continue;
297
+ }
169
298
  findings.push({
170
299
  severity: 'WARNING',
171
300
  code: 'PERF-001',
172
301
  line: j + 1,
173
302
  message: `Nested loop detected (outer loop at line ${i + 1}) — O(n²) or worse complexity.`,
174
- suggestion: 'Consider using a Map/Set for O(1) lookups, or restructure to avoid nested iteration.',
303
+ suggestion: 'Consider using a Map/Set/Hash for O(1) lookups, or restructure to avoid nested iteration.',
175
304
  });
176
- break; // One warning per outer loop is enough
305
+ break;
177
306
  }
178
307
  }
179
- // Outer loop body has closed
180
308
  if (foundOpening && depth <= 0)
181
309
  break;
182
310
  }
@@ -192,22 +320,52 @@ function detectNestedLoopsPython(_source, stripped, findings) {
192
320
  const outerIndent = lines[i].search(/\S/);
193
321
  if (outerIndent < 0)
194
322
  continue;
195
- // Scan forward for a nested loop with deeper indentation
196
323
  for (let j = i + 1; j < lines.length; j++) {
197
324
  const innerLine = lines[j];
198
325
  if (innerLine.trim() === '')
199
326
  continue;
200
327
  const innerIndent = innerLine.search(/\S/);
201
- // If we've returned to same or lesser indent, the outer loop body is over
202
328
  if (innerIndent <= outerIndent)
203
329
  break;
204
330
  if (loopPattern.test(innerLine)) {
331
+ if (/\brange\s*\(\s*([0-9]|10)\s*\)/.test(innerLine))
332
+ continue;
333
+ findings.push({
334
+ severity: 'WARNING',
335
+ code: 'PERF-001',
336
+ line: j + 1,
337
+ message: `Nested loop detected (outer loop at line ${i + 1}) — O(n²) or worse complexity.`,
338
+ suggestion: 'Consider using a dictionary/set for O(1) lookups, or restructure to avoid nested iteration.',
339
+ });
340
+ break;
341
+ }
342
+ }
343
+ }
344
+ }
345
+ function detectNestedLoopsRuby(_source, stripped, findings) {
346
+ const lines = stripped.split('\n');
347
+ const loopPattern = /\b(while|until|for)\b|\.(each|times|map|select|loop)\b/;
348
+ for (let i = 0; i < lines.length; i++) {
349
+ const outerLine = lines[i];
350
+ if (!loopPattern.test(outerLine))
351
+ continue;
352
+ const outerIndent = outerLine.search(/\S/);
353
+ if (outerIndent < 0)
354
+ continue;
355
+ for (let j = i + 1; j < lines.length; j++) {
356
+ const innerLine = lines[j];
357
+ if (innerLine.trim() === '')
358
+ continue;
359
+ const innerIndent = innerLine.search(/\S/);
360
+ if (innerIndent <= outerIndent && !/^\s*(end|\}|\))\b/.test(innerLine))
361
+ break;
362
+ if (loopPattern.test(innerLine) && innerIndent > outerIndent) {
205
363
  findings.push({
206
364
  severity: 'WARNING',
207
365
  code: 'PERF-001',
208
366
  line: j + 1,
209
367
  message: `Nested loop detected (outer loop at line ${i + 1}) — O(n²) or worse complexity.`,
210
- suggestion: 'Consider using a dictionary for O(1) lookups, or restructure to avoid nested iteration.',
368
+ suggestion: 'Consider using a Hash for O(1) lookups or flat map operations.',
211
369
  });
212
370
  break;
213
371
  }
@@ -218,10 +376,7 @@ function detectNestedLoopsPython(_source, stripped, findings) {
218
376
  const detectChainedArrayMethods = (_source, stripped, lang, findings) => {
219
377
  if (lang !== 'js')
220
378
  return;
221
- // Match chains like .map(...).filter(...).reduce(...) with 3+ methods
222
- const chainPattern = /\.(map|filter|reduce|flatMap|flat|sort|slice|concat)\s*\([^)]*\)\s*\.(map|filter|reduce|flatMap|flat|sort|slice|concat)\s*\([^)]*\)\s*\.(map|filter|reduce|flatMap|flat|sort|slice|concat)/g;
223
- const lines = stripped.split('\n');
224
- // Search across joined lines since chains can span multiple lines
379
+ const chainPattern = /\.(map|filter|reduce|flatMap|flat|sort|slice|concat)\s*\([\s\S]*?\)\s*\.(map|filter|reduce|flatMap|flat|sort|slice|concat)\s*\([\s\S]*?\)\s*\.(map|filter|reduce|flatMap|flat|sort|slice|concat)/g;
225
380
  const joined = stripped;
226
381
  let match;
227
382
  while ((match = chainPattern.exec(joined)) !== null) {
@@ -240,10 +395,21 @@ const detectSyncIOInAsync = (_source, stripped, lang, findings) => {
240
395
  if (lang !== 'js')
241
396
  return;
242
397
  const syncAPIs = [
243
- 'readFileSync', 'writeFileSync', 'appendFileSync', 'mkdirSync',
244
- 'readdirSync', 'statSync', 'existsSync', 'copyFileSync',
245
- 'renameSync', 'unlinkSync', 'rmdirSync', 'accessSync',
246
- 'execSync', 'spawnSync', 'execFileSync'
398
+ 'readFileSync',
399
+ 'writeFileSync',
400
+ 'appendFileSync',
401
+ 'mkdirSync',
402
+ 'readdirSync',
403
+ 'statSync',
404
+ 'existsSync',
405
+ 'copyFileSync',
406
+ 'renameSync',
407
+ 'unlinkSync',
408
+ 'rmdirSync',
409
+ 'accessSync',
410
+ 'execSync',
411
+ 'spawnSync',
412
+ 'execFileSync',
247
413
  ];
248
414
  const lines = stripped.split('\n');
249
415
  let insideAsync = false;
@@ -251,7 +417,6 @@ const detectSyncIOInAsync = (_source, stripped, lang, findings) => {
251
417
  let braceDepth = 0;
252
418
  for (let i = 0; i < lines.length; i++) {
253
419
  const line = lines[i];
254
- // Detect async function/method/arrow entry
255
420
  if (/\basync\s+(function\b|\w+\s*\(|(\w+)\s*=>|\(.*\)\s*=>)/.test(line) || /\basync\s+\w+\s*\(/.test(line)) {
256
421
  insideAsync = true;
257
422
  asyncBraceDepth = braceDepth;
@@ -309,7 +474,6 @@ const detectSpreadInLoops = (_source, stripped, lang, findings) => {
309
474
  }
310
475
  }
311
476
  if (loopDepth > 0 && /\.\.\.[\w$]/.test(line)) {
312
- // Ignore rest parameters in function definitions: function(...args) or (...args) =>
313
477
  if (!/function\s*\([^)]*\.\.\.[\w$]/.test(line) && !/\([^)]*\.\.\.[\w$][^)]*\)\s*=>/.test(line)) {
314
478
  findings.push({
315
479
  severity: 'WARNING',
@@ -327,21 +491,17 @@ const detectMissingCleanup = (source, stripped, lang, findings) => {
327
491
  if (lang !== 'js' && lang !== 'python')
328
492
  return;
329
493
  if (lang === 'js') {
330
- // Look for createReadStream/createWriteStream/createServer assigned to a variable
331
494
  const pattern = /\b(const|let|var)\s+(\w+)\s*=\s*.*\b(createReadStream|createWriteStream|createServer|createConnection|net\.connect|tls\.connect)\b/g;
332
495
  let match;
333
496
  while ((match = pattern.exec(stripped)) !== null) {
334
497
  const varName = match[2];
335
498
  const creator = match[3];
336
499
  const line = getLineNumber(stripped, match.index);
337
- // Check if .close(), .destroy(), .end(), or .disconnect() is called on this variable
338
- // within a reasonable scope (the rest of the file)
339
500
  const rest = stripped.substring(match.index);
340
501
  const hasCleanup = rest.includes(`${varName}.close()`) ||
341
502
  rest.includes(`${varName}.destroy()`) ||
342
503
  rest.includes(`${varName}.end()`) ||
343
504
  rest.includes(`${varName}.disconnect()`) ||
344
- // Also accept piping (which handles cleanup) or 'using' declarations
345
505
  rest.includes(`${varName}.pipe(`) ||
346
506
  stripped.includes(`using ${varName}`);
347
507
  if (!hasCleanup) {
@@ -356,11 +516,9 @@ const detectMissingCleanup = (source, stripped, lang, findings) => {
356
516
  }
357
517
  }
358
518
  if (lang === 'python') {
359
- // Detect open() not inside a 'with' statement
360
519
  const lines = stripped.split('\n');
361
520
  for (let i = 0; i < lines.length; i++) {
362
521
  const line = lines[i];
363
- // Match: var = open(...) but NOT: with open(...) as var
364
522
  if (/=\s*open\s*\(/.test(line) && !/\bwith\b/.test(line)) {
365
523
  findings.push({
366
524
  severity: 'WARNING',
@@ -379,7 +537,6 @@ const detectDangerousEval = (_source, stripped, lang, findings) => {
379
537
  for (let i = 0; i < lines.length; i++) {
380
538
  const line = lines[i];
381
539
  if (lang === 'js') {
382
- // Match eval(...) but not .addEventListener('...', eval)
383
540
  if (/\beval\s*\(/.test(line)) {
384
541
  findings.push({
385
542
  severity: 'ERROR',
@@ -444,14 +601,10 @@ const detectUnsafeJsonParse = (_source, stripped, lang, findings) => {
444
601
  insideAsync = false;
445
602
  }
446
603
  }
447
- // Detect JSON.parse(req.body) / JSON.parse(body) / JSON.parse(data) etc.
448
- // in what appears to be a request handler context
449
604
  if (/JSON\.parse\s*\(/.test(line)) {
450
- // Check if this is inside a try block — if so, it's handled
451
605
  const prevLines = lines.slice(Math.max(0, i - 5), i).join('\n');
452
606
  if (/\btry\s*\{/.test(prevLines))
453
607
  continue;
454
- // Check if the argument looks like an external input
455
608
  const argMatch = line.match(/JSON\.parse\s*\(\s*(\w+)/);
456
609
  if (argMatch) {
457
610
  const argName = argMatch[1].toLowerCase();
@@ -475,13 +628,9 @@ const detectUnboundedFetch = (_source, stripped, lang, findings) => {
475
628
  for (let i = 0; i < lines.length; i++) {
476
629
  const line = lines[i];
477
630
  if (lang === 'js') {
478
- // Detect .find() / .findMany() / .select() / .query() without .limit() / .take() / .paginate()
479
- // Common in Prisma, Mongoose, Knex, Sequelize. Uses negative lookahead to ignore array methods like .find(x => ...) or .find(function(...) ...)
480
631
  if (/\.(find|findMany|findAll|select|query)\s*\(\s*(?!(\w+|\([^)]*\))\s*=>|function\b)/.test(line)) {
481
- // Look ahead a few lines for a .limit() / .take() / .skip() / .paginate() / .first()
482
632
  const window = lines.slice(i, Math.min(i + 5, lines.length)).join('\n');
483
- if (!/\.(limit|take|first|paginate|skip|offset|top)\s*\(/.test(window) &&
484
- !/\bLIMIT\b/i.test(window)) {
633
+ if (!/\.(limit|take|first|paginate|skip|offset|top)\s*\(/.test(window) && !/\bLIMIT\b/i.test(window)) {
485
634
  findings.push({
486
635
  severity: 'WARNING',
487
636
  code: 'PERF-008',
@@ -493,7 +642,6 @@ const detectUnboundedFetch = (_source, stripped, lang, findings) => {
493
642
  }
494
643
  }
495
644
  if (lang === 'python') {
496
- // Detect .all() / .filter() on Django/SQLAlchemy querysets without [:N] or .limit()
497
645
  if (/\.(all|filter|objects\.filter|objects\.all)\s*\(/.test(line)) {
498
646
  const window = lines.slice(i, Math.min(i + 5, lines.length)).join('\n');
499
647
  if (!/\.(limit|first|paginate|count)\s*\(/.test(window) &&
@@ -512,8 +660,7 @@ const detectUnboundedFetch = (_source, stripped, lang, findings) => {
512
660
  if (lang === 'php') {
513
661
  if (/\.(all|get|findAll|query)\s*\(|::all\s*\(/.test(line)) {
514
662
  const window = lines.slice(i, Math.min(i + 5, lines.length)).join('\n');
515
- if (!/\.(limit|take|paginate|offset)\s*\(/.test(window) &&
516
- !/\bLIMIT\b/i.test(window)) {
663
+ if (!/\.(limit|take|paginate|offset)\s*\(/.test(window) && !/\bLIMIT\b/i.test(window)) {
517
664
  findings.push({
518
665
  severity: 'WARNING',
519
666
  code: 'PERF-008',
@@ -524,10 +671,36 @@ const detectUnboundedFetch = (_source, stripped, lang, findings) => {
524
671
  }
525
672
  }
526
673
  }
527
- // Go / Rust: detect SQL queries without LIMIT
528
- if (lang === 'go' || lang === 'rust') {
674
+ if (lang === 'ruby') {
675
+ if (/\.(all|where)\b/.test(line) && !/\.(limit|find_each|first|take|paginate)\b/.test(line)) {
676
+ const window = lines.slice(i, Math.min(i + 5, lines.length)).join('\n');
677
+ if (!/\.(limit|find_each|first|take|paginate)\b/.test(window) && !/\bLIMIT\b/i.test(window)) {
678
+ findings.push({
679
+ severity: 'WARNING',
680
+ code: 'PERF-008',
681
+ line: i + 1,
682
+ message: 'ActiveRecord query without .limit() or .find_each — may fetch unbounded rows into memory.',
683
+ suggestion: 'Add .limit(N) or use .find_each for batch processing.',
684
+ });
685
+ }
686
+ }
687
+ }
688
+ if (lang === 'csharp' || lang === 'java') {
689
+ if (/\.(ToList|ToArray|findAll|Query)\s*\(/.test(line)) {
690
+ const window = lines.slice(i, Math.min(i + 5, lines.length)).join('\n');
691
+ if (!/\.(Take|Limit|Page|FirstOrDefault)\s*\(/.test(window) && !/\bLIMIT\b/i.test(window)) {
692
+ findings.push({
693
+ severity: 'WARNING',
694
+ code: 'PERF-008',
695
+ line: i + 1,
696
+ message: 'ORM/Collection query materialization without Take/Limit — may load entire dataset into memory.',
697
+ suggestion: 'Apply .Take(N) or pagination before calling materialization methods.',
698
+ });
699
+ }
700
+ }
701
+ }
702
+ if (lang === 'go' || lang === 'rust' || lang === 'cpp') {
529
703
  if (/SELECT\s+/i.test(line) && !/\bLIMIT\b/i.test(line)) {
530
- // Look ahead for LIMIT on subsequent lines
531
704
  const window = lines.slice(i, Math.min(i + 5, lines.length)).join('\n');
532
705
  if (!/\bLIMIT\b/i.test(window)) {
533
706
  findings.push({
@@ -570,7 +743,6 @@ const detectPhpInLoopOperations = (_source, stripped, lang, findings) => {
570
743
  }
571
744
  }
572
745
  if (loopDepth > 0) {
573
- // PERF-PHP1: array_merge
574
746
  if (/\barray_merge\s*\(/.test(line)) {
575
747
  findings.push({
576
748
  severity: 'WARNING',
@@ -580,7 +752,6 @@ const detectPhpInLoopOperations = (_source, stripped, lang, findings) => {
580
752
  suggestion: 'Append to array with $arr[] = $val and merge once outside the loop, or use the splat operator.',
581
753
  });
582
754
  }
583
- // PERF-PHP2: N+1 queries
584
755
  if (/(->get\(|->find\(|->query\(|->findAll\(|::find\(|DB::|->execute\()/.test(line)) {
585
756
  findings.push({
586
757
  severity: 'WARNING',
@@ -590,7 +761,6 @@ const detectPhpInLoopOperations = (_source, stripped, lang, findings) => {
590
761
  suggestion: 'Pre-fetch data outside the loop using eager loading (e.g., with()) or a single IN() query.',
591
762
  });
592
763
  }
593
- // PERF-PHP3: Blocking I/O
594
764
  if (/\b(file_get_contents|curl_exec|fopen)\s*\(/.test(line)) {
595
765
  findings.push({
596
766
  severity: 'WARNING',
@@ -612,12 +782,20 @@ const detectSequentialAwait = (_source, stripped, lang, findings) => {
612
782
  let braceDepth = 0;
613
783
  let inLoop = false;
614
784
  let loopBraceStart = 0;
785
+ let isPaginationOrRetryLoop = false;
615
786
  for (let i = 0; i < lines.length; i++) {
616
787
  const line = lines[i];
617
- if ((/\b(for|while|do)\s*[\s(]/.test(line) || /\.(forEach|map|filter|reduce)\s*\(/.test(line)) && !inLoop) {
788
+ const isLoopHeader = /\b(for|while|do)\s*[\s(]/.test(line) || /\.(forEach|map|filter|reduce)\s*\(/.test(line);
789
+ if (isLoopHeader && !inLoop) {
618
790
  inLoop = true;
619
791
  loopBraceStart = braceDepth;
620
792
  loopDepth++;
793
+ if (/\b(cursor|hasMore|nextPage|hasNext|page|retry|attempt|backoff)\b/i.test(line)) {
794
+ isPaginationOrRetryLoop = true;
795
+ }
796
+ else {
797
+ isPaginationOrRetryLoop = false;
798
+ }
621
799
  }
622
800
  for (const ch of line) {
623
801
  if (ch === '{')
@@ -627,10 +805,27 @@ const detectSequentialAwait = (_source, stripped, lang, findings) => {
627
805
  if (inLoop && braceDepth <= loopBraceStart) {
628
806
  inLoop = false;
629
807
  loopDepth--;
808
+ isPaginationOrRetryLoop = false;
630
809
  }
631
810
  }
632
811
  }
633
812
  if (loopDepth > 0 && /\bawait\s+/.test(line)) {
813
+ // Context-aware heuristic 1: Concurrent execution patterns
814
+ if (/\bawait\s+Promise\.(all|allSettled|race|any)\b/.test(line))
815
+ continue;
816
+ // Context-aware heuristic 2: Delays / sleep / retries
817
+ if (/\bawait\s+(delay|sleep|wait|pause|setTimeout)\b/i.test(line) ||
818
+ /\bawait\s+new\s+Promise\s*\(\s*\(?resolve\)?\s*=>\s*setTimeout/.test(line)) {
819
+ continue;
820
+ }
821
+ // Context-aware heuristic 3: Cursor/pagination or retry loop body
822
+ if (isPaginationOrRetryLoop || /\b(cursor|hasMore|nextPage|hasNext|page|token)\b/i.test(line)) {
823
+ continue;
824
+ }
825
+ // Context-aware heuristic 4: Stream/reader consumption
826
+ if (/\b(reader\.read|stream|readline|rl|iterator\.next|gen\.next)\b/i.test(line)) {
827
+ continue;
828
+ }
634
829
  findings.push({
635
830
  severity: 'WARNING',
636
831
  code: 'PERF-009',
@@ -757,12 +952,12 @@ const detectPythonInLoopOperations = (_source, stripped, lang, findings) => {
757
952
  continue;
758
953
  }
759
954
  if (inLoop && indent > outerIndent) {
760
- if (/^\s*[a-zA-Z_]\w*\s*\+=\s*['"]/.test(line)) {
955
+ if (/\b[a-zA-Z_]\w*\s*\+=/.test(line)) {
761
956
  findings.push({
762
957
  severity: 'WARNING',
763
958
  code: 'PERF-PY1',
764
959
  line: i + 1,
765
- message: 'String concatenation (+=) inside a loop — Python strings are immutable, causing O(n²) memory allocations.',
960
+ message: 'String/collection concatenation (+=) inside a loop — Python strings are immutable, causing O(n²) memory allocations.',
766
961
  suggestion: 'Append to a list and use "".join() outside the loop instead.',
767
962
  });
768
963
  }
@@ -815,7 +1010,7 @@ const detectGoInLoopOperations = (_source, stripped, lang, findings) => {
815
1010
  suggestion: 'Wrap the loop body in an anonymous function (func() { ... }()) or close resources manually.',
816
1011
  });
817
1012
  }
818
- if (/\b[a-zA-Z_]\w*\s*\+=\s*["`]/.test(line)) {
1013
+ if (/\b[a-zA-Z_]\w*\s*\+=/.test(line)) {
819
1014
  findings.push({
820
1015
  severity: 'WARNING',
821
1016
  code: 'PERF-GO2',
@@ -876,6 +1071,268 @@ const detectRustInLoopOperations = (_source, stripped, lang, findings) => {
876
1071
  }
877
1072
  }
878
1073
  };
1074
+ // ── PERF-CS1/2/3: C# Specific Loop Operations ──────────────────────────
1075
+ const detectCsharpInLoopOperations = (source, stripped, lang, findings) => {
1076
+ if (lang !== 'csharp')
1077
+ return;
1078
+ const lines = stripped.split('\n');
1079
+ const rawLines = source.split('\n');
1080
+ let loopDepth = 0;
1081
+ let braceDepth = 0;
1082
+ let inLoop = false;
1083
+ let loopBraceStart = 0;
1084
+ for (let i = 0; i < lines.length; i++) {
1085
+ const line = lines[i];
1086
+ const rawLine = rawLines[i];
1087
+ if (/\b(for|foreach|while|do)\s*[\s(]/.test(line) && !inLoop) {
1088
+ inLoop = true;
1089
+ loopBraceStart = braceDepth;
1090
+ loopDepth++;
1091
+ }
1092
+ for (const ch of line) {
1093
+ if (ch === '{')
1094
+ braceDepth++;
1095
+ else if (ch === '}') {
1096
+ braceDepth--;
1097
+ if (inLoop && braceDepth <= loopBraceStart) {
1098
+ inLoop = false;
1099
+ loopDepth--;
1100
+ }
1101
+ }
1102
+ }
1103
+ if (loopDepth > 0) {
1104
+ // PERF-CS1: LINQ in loop
1105
+ if (/\.(Where|Select|ToList|ToArray|FirstOrDefault|Count)\s*\(/.test(line)) {
1106
+ findings.push({
1107
+ severity: 'WARNING',
1108
+ code: 'PERF-CS1',
1109
+ line: i + 1,
1110
+ message: 'LINQ query method inside loop — allocates delegates and iterators on every iteration.',
1111
+ suggestion: 'Pre-compute or pull LINQ queries outside the loop.',
1112
+ });
1113
+ }
1114
+ // PERF-CS2: String concatenation in loop
1115
+ if (/\b[a-zA-Z_]\w*\s*\+=/.test(line)) {
1116
+ findings.push({
1117
+ severity: 'WARNING',
1118
+ code: 'PERF-CS2',
1119
+ line: i + 1,
1120
+ message: 'String concatenation (+=) inside loop — allocates new string objects repeatedly.',
1121
+ suggestion: 'Use System.Text.StringBuilder for efficient string construction.',
1122
+ });
1123
+ }
1124
+ }
1125
+ // PERF-CS3: Sync-over-async blocking
1126
+ if (/\.(Result|Wait\(\)|GetAwaiter\(\)\.GetResult\(\))/.test(rawLine)) {
1127
+ findings.push({
1128
+ severity: 'ERROR',
1129
+ code: 'PERF-CS3',
1130
+ line: i + 1,
1131
+ message: 'Sync-over-async blocking (.Result / .Wait()) — causes threadpool starvation and potential deadlocks.',
1132
+ suggestion: 'Use await instead of synchronous blocking on Task objects.',
1133
+ });
1134
+ }
1135
+ }
1136
+ };
1137
+ // ── PERF-JV1/2/3: Java Specific Loop Operations ────────────────────────
1138
+ const detectJavaInLoopOperations = (source, stripped, lang, findings) => {
1139
+ if (lang !== 'java')
1140
+ return;
1141
+ const lines = stripped.split('\n');
1142
+ const rawLines = source.split('\n');
1143
+ let loopDepth = 0;
1144
+ let braceDepth = 0;
1145
+ let inLoop = false;
1146
+ let loopBraceStart = 0;
1147
+ for (let i = 0; i < lines.length; i++) {
1148
+ const line = lines[i];
1149
+ const rawLine = rawLines[i];
1150
+ if (/\b(for|while|do)\s*[\s(]/.test(line) && !inLoop) {
1151
+ inLoop = true;
1152
+ loopBraceStart = braceDepth;
1153
+ loopDepth++;
1154
+ }
1155
+ for (const ch of line) {
1156
+ if (ch === '{')
1157
+ braceDepth++;
1158
+ else if (ch === '}') {
1159
+ braceDepth--;
1160
+ if (inLoop && braceDepth <= loopBraceStart) {
1161
+ inLoop = false;
1162
+ loopDepth--;
1163
+ }
1164
+ }
1165
+ }
1166
+ if (loopDepth > 0) {
1167
+ // PERF-JV1: String concat in loop
1168
+ if (/\b[a-zA-Z_]\w*\s*\+=/.test(line)) {
1169
+ findings.push({
1170
+ severity: 'WARNING',
1171
+ code: 'PERF-JV1',
1172
+ line: i + 1,
1173
+ message: 'String concatenation (+=) inside loop — allocates intermediate String and StringBuilder instances.',
1174
+ suggestion: 'Use StringBuilder explicitly outside the loop.',
1175
+ });
1176
+ }
1177
+ // PERF-JV3: Expensive object instantiations in loop
1178
+ if (/\bnew\s+(SimpleDateFormat|DecimalFormat|Pattern|BigInteger|BigDecimal)\b/.test(rawLine) ||
1179
+ /\bPattern\.compile\s*\(/.test(line)) {
1180
+ findings.push({
1181
+ severity: 'WARNING',
1182
+ code: 'PERF-JV3',
1183
+ line: i + 1,
1184
+ message: 'Heavy object instantiation / Pattern compilation inside loop.',
1185
+ suggestion: 'Extract formatter / pattern instances outside the loop or reuse thread-safe / static instances.',
1186
+ });
1187
+ }
1188
+ }
1189
+ // PERF-JV2: System.gc() call
1190
+ if (/\bSystem\.gc\s*\(\s*\)/.test(line)) {
1191
+ findings.push({
1192
+ severity: 'WARNING',
1193
+ code: 'PERF-JV2',
1194
+ line: i + 1,
1195
+ message: 'Explicit System.gc() invocation — triggers full stop-the-world Garbage Collection.',
1196
+ suggestion: 'Avoid explicit GC calls and let the JVM manage memory automatically.',
1197
+ });
1198
+ }
1199
+ }
1200
+ };
1201
+ // ── PERF-CPP1/2/3: C++ Specific Loop Operations ────────────────────────
1202
+ const detectCppInLoopOperations = (source, stripped, lang, findings) => {
1203
+ if (lang !== 'cpp')
1204
+ return;
1205
+ const lines = stripped.split('\n');
1206
+ const rawLines = source.split('\n');
1207
+ let loopDepth = 0;
1208
+ let braceDepth = 0;
1209
+ let inLoop = false;
1210
+ let loopBraceStart = 0;
1211
+ for (let i = 0; i < lines.length; i++) {
1212
+ const line = lines[i];
1213
+ const rawLine = rawLines[i];
1214
+ if (/\bfor\s*\(\s*(auto|[a-zA-Z_]\w*(?:::[a-zA-Z_]\w*)*)\s+([a-zA-Z_]\w*)\s*:\s*/.test(line)) {
1215
+ const match = line.match(/\bfor\s*\(\s*(const\s+)?(auto|[a-zA-Z_]\w*(?:::[a-zA-Z_]\w*)*)\s*(&)?\s*([a-zA-Z_]\w*)\s*:\s*/);
1216
+ if (match && !match[3]) {
1217
+ const typeName = match[2];
1218
+ const primitiveTypes = [
1219
+ 'int',
1220
+ 'char',
1221
+ 'bool',
1222
+ 'float',
1223
+ 'double',
1224
+ 'size_t',
1225
+ 'long',
1226
+ 'short',
1227
+ 'uint32_t',
1228
+ 'int32_t',
1229
+ ];
1230
+ if (!primitiveTypes.includes(typeName)) {
1231
+ findings.push({
1232
+ severity: 'WARNING',
1233
+ code: 'PERF-CPP1',
1234
+ line: i + 1,
1235
+ message: 'Range-based for loop element copied by value — triggers copy constructor for non-primitive types.',
1236
+ suggestion: 'Use const auto& or auto& to pass loop elements by reference.',
1237
+ });
1238
+ }
1239
+ }
1240
+ }
1241
+ if (/\b(for|while|do)\s*[\s(]/.test(line) && !inLoop) {
1242
+ inLoop = true;
1243
+ loopBraceStart = braceDepth;
1244
+ loopDepth++;
1245
+ }
1246
+ for (const ch of line) {
1247
+ if (ch === '{')
1248
+ braceDepth++;
1249
+ else if (ch === '}') {
1250
+ braceDepth--;
1251
+ if (inLoop && braceDepth <= loopBraceStart) {
1252
+ inLoop = false;
1253
+ loopDepth--;
1254
+ }
1255
+ }
1256
+ }
1257
+ if (loopDepth > 0) {
1258
+ // PERF-CPP2: std::endl inside loop
1259
+ if (/\bstd::endl\b/.test(rawLine)) {
1260
+ findings.push({
1261
+ severity: 'WARNING',
1262
+ code: 'PERF-CPP2',
1263
+ line: i + 1,
1264
+ message: 'std::endl used inside loop — forces stream buffer flushing on every iteration.',
1265
+ suggestion: "Use '\\n' instead of std::endl to avoid frequent I/O flushing.",
1266
+ });
1267
+ }
1268
+ // PERF-CPP3: std::regex inside loop
1269
+ if (/\bstd::regex\b/.test(line)) {
1270
+ findings.push({
1271
+ severity: 'WARNING',
1272
+ code: 'PERF-CPP3',
1273
+ line: i + 1,
1274
+ message: 'std::regex compilation inside loop — C++ regex construction is extremely heavy.',
1275
+ suggestion: 'Construct std::regex objects once outside the loop.',
1276
+ });
1277
+ }
1278
+ }
1279
+ }
1280
+ };
1281
+ // ── PERF-RB1/2/3: Ruby Specific Loop Operations ────────────────────────
1282
+ const detectRubyInLoopOperations = (source, stripped, lang, findings) => {
1283
+ if (lang !== 'ruby')
1284
+ return;
1285
+ const lines = stripped.split('\n');
1286
+ let inLoop = false;
1287
+ let outerIndent = -1;
1288
+ const loopPattern = /\b(while|until|for)\b|\.(each|times|map|select|loop)\b/;
1289
+ for (let i = 0; i < lines.length; i++) {
1290
+ const line = lines[i];
1291
+ if (line.trim() === '')
1292
+ continue;
1293
+ const indent = line.search(/\S/);
1294
+ if (inLoop && indent <= outerIndent && /^\s*(end|\}|\))\b/.test(line)) {
1295
+ inLoop = false;
1296
+ }
1297
+ // PERF-RB3: Chained Enumerable methods
1298
+ if (/\.(map|select|reject|compact)\b[^.\n]*\.(map|select|reject|compact)\b/.test(line)) {
1299
+ findings.push({
1300
+ severity: 'WARNING',
1301
+ code: 'PERF-RB3',
1302
+ line: i + 1,
1303
+ message: 'Chained Enumerable transformations — creates intermediate array allocations.',
1304
+ suggestion: 'Use .lazy or combine transformations into a single pass.',
1305
+ });
1306
+ }
1307
+ if (!inLoop && loopPattern.test(line)) {
1308
+ inLoop = true;
1309
+ outerIndent = indent;
1310
+ continue;
1311
+ }
1312
+ if (inLoop && indent > outerIndent) {
1313
+ // PERF-RB1: String concat += in loop
1314
+ if (/\b[a-zA-Z_]\w*\s*\+=/.test(line)) {
1315
+ findings.push({
1316
+ severity: 'WARNING',
1317
+ code: 'PERF-RB1',
1318
+ line: i + 1,
1319
+ message: 'String concatenation (+=) inside loop — creates intermediate string allocations.',
1320
+ suggestion: 'Use shovel operator << or string interpolation instead.',
1321
+ });
1322
+ }
1323
+ // PERF-RB2: ActiveRecord N+1 query in loop
1324
+ if (/\.(find|where|first|find_by)\b/.test(line)) {
1325
+ findings.push({
1326
+ severity: 'WARNING',
1327
+ code: 'PERF-RB2',
1328
+ line: i + 1,
1329
+ message: 'Database query execution inside loop (N+1 query risk).',
1330
+ suggestion: 'Pre-fetch records outside loop using .includes() or bulk query.',
1331
+ });
1332
+ }
1333
+ }
1334
+ }
1335
+ };
879
1336
  // ─── Rule Registry ───────────────────────────────────────────────────
880
1337
  const ALL_RULES = [
881
1338
  detectNestedLoops,
@@ -894,6 +1351,10 @@ const ALL_RULES = [
894
1351
  detectPythonInLoopOperations,
895
1352
  detectGoInLoopOperations,
896
1353
  detectRustInLoopOperations,
1354
+ detectCsharpInLoopOperations,
1355
+ detectJavaInLoopOperations,
1356
+ detectCppInLoopOperations,
1357
+ detectRubyInLoopOperations,
897
1358
  ];
898
1359
  // ─── Supported Extensions for Auditing ───────────────────────────────
899
1360
  const AUDITABLE_EXTENSIONS = new Set(Object.keys(EXTENSION_MAP));
@@ -907,10 +1368,7 @@ export function isAuditableFile(filePath) {
907
1368
  /**
908
1369
  * Scans source code for performance anti-patterns using language-aware
909
1370
  * regex-based heuristics. Returns structured findings grouped by severity.
910
- *
911
- * This function is designed to be called inline during the verification
912
- * phase — it's synchronous, zero-dependency, and executes in <50ms on
913
- * files up to 10,000 lines.
1371
+ * Supports comment suppression directives (perf-ignore).
914
1372
  *
915
1373
  * @param content - The raw source code string to audit.
916
1374
  * @param filePath - The file path (used for language detection via extension).
@@ -918,15 +1376,17 @@ export function isAuditableFile(filePath) {
918
1376
  */
919
1377
  export function auditFilePerformance(content, filePath) {
920
1378
  const lang = detectLanguage(filePath);
921
- const findings = [];
922
1379
  if (lang === 'unknown') {
923
1380
  return { filePath, findings: [], errors: [], warnings: [], infos: [] };
924
1381
  }
925
1382
  // Strip strings and comments to avoid false positives
926
1383
  const stripped = stripStringsAndComments(content, lang);
1384
+ let findings = [];
927
1385
  for (const rule of ALL_RULES) {
928
1386
  rule(content, stripped, lang, findings);
929
1387
  }
1388
+ // Filter out findings suppressed by perf-ignore comment directives
1389
+ findings = applyDirectiveSuppression(findings, content);
930
1390
  // Sort by severity (ERROR first), then by line number
931
1391
  const severityOrder = { ERROR: 0, WARNING: 1, INFO: 2 };
932
1392
  findings.sort((a, b) => {
@@ -948,21 +1408,17 @@ const SEVERITY_ICONS = {
948
1408
  INFO: '🔵',
949
1409
  };
950
1410
  /**
951
- * Formats audit findings for beautiful terminal display using picocolors.
952
- * Used for non-blocking warnings shown to the user after verification.
1411
+ * Formats audit findings for terminal display using picocolors.
953
1412
  */
954
1413
  export function formatAuditForTerminal(results) {
955
1414
  const allFindings = results.flatMap((r) => r.findings.map((f) => ({ ...f, file: r.filePath })));
956
1415
  if (allFindings.length === 0)
957
1416
  return '';
958
- const lines = [
959
- pc.bold('Performance Audit Results'),
960
- pc.dim('─'.repeat(50)),
961
- ];
1417
+ const lines = [pc.bold('Performance Audit Results'), pc.dim('─'.repeat(50))];
962
1418
  for (const f of allFindings) {
963
1419
  const icon = SEVERITY_ICONS[f.severity];
964
1420
  const sevColor = f.severity === 'ERROR' ? pc.red : f.severity === 'WARNING' ? pc.yellow : pc.blue;
965
- lines.push(`${icon} ${sevColor(f.severity)} ${pc.dim(`[${f.code}]`)} ${pc.cyan(f.file)}${pc.dim(`:${f.line}`)}`);
1421
+ lines.push(`${icon} ${sevColor(f.severity)} ${pc.dim(`[${f.code}]`)} ${pc.cyan(`${f.file}:${f.line}`)}`);
966
1422
  lines.push(` ${f.message}`);
967
1423
  lines.push(` ${pc.dim('→')} ${pc.dim(f.suggestion)}`);
968
1424
  }
@@ -977,16 +1433,12 @@ export function formatAuditForTerminal(results) {
977
1433
  }
978
1434
  /**
979
1435
  * Formats audit findings into a structured prompt for the AI auto-correction loop.
980
- * Only includes ERROR-severity findings (warnings and info are non-blocking).
981
1436
  */
982
1437
  export function formatAuditForModel(results) {
983
1438
  const errors = results.flatMap((r) => r.errors.map((f) => ({ ...f, file: r.filePath })));
984
1439
  if (errors.length === 0)
985
1440
  return '';
986
- const lines = [
987
- 'The following performance anti-patterns were detected in your generated code:',
988
- '',
989
- ];
1441
+ const lines = ['The following performance anti-patterns were detected in your generated code:', ''];
990
1442
  for (const e of errors) {
991
1443
  lines.push(`[${e.code}] ${e.file}:${e.line} — ${e.message}`);
992
1444
  lines.push(` Fix: ${e.suggestion}`);