minovative-mind-cli 1.2.3 → 1.3.1

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.
@@ -0,0 +1,615 @@
1
+ import path from 'node:path';
2
+ import pc from 'picocolors';
3
+ const EXTENSION_MAP = {
4
+ '.ts': 'js', '.tsx': 'js', '.js': 'js', '.jsx': 'js', '.mjs': 'js', '.cjs': 'js',
5
+ '.py': 'python', '.pyw': 'python',
6
+ '.go': 'go',
7
+ '.rs': 'rust',
8
+ };
9
+ function detectLanguage(filePath) {
10
+ return EXTENSION_MAP[path.extname(filePath).toLowerCase()] ?? 'unknown';
11
+ }
12
+ // ─── Comment & String Stripping ──────────────────────────────────────
13
+ //
14
+ // Before running regex-based detection, we strip string literals and
15
+ // comments from the source to prevent false positives on patterns
16
+ // appearing inside strings or documentation.
17
+ /**
18
+ * Replaces the content of string literals and comments with whitespace,
19
+ * preserving line count so that line-number tracking remains accurate.
20
+ */
21
+ function stripStringsAndComments(source, lang) {
22
+ if (lang === 'unknown')
23
+ return source;
24
+ let result = '';
25
+ let i = 0;
26
+ while (i < source.length) {
27
+ const ch = source[i];
28
+ const next = source[i + 1];
29
+ // ── JS/TS/Go/Rust: line comments (//) ─────────────────────
30
+ if ((lang === 'js' || lang === 'go' || lang === 'rust') && ch === '/' && next === '/') {
31
+ const eol = source.indexOf('\n', i);
32
+ const end = eol === -1 ? source.length : eol;
33
+ result += ' '.repeat(end - i);
34
+ i = end;
35
+ continue;
36
+ }
37
+ // ── JS/TS/Go/Rust: block comments (/* ... */) ─────────────
38
+ if ((lang === 'js' || lang === 'go' || lang === 'rust') && ch === '/' && next === '*') {
39
+ const end = source.indexOf('*/', i + 2);
40
+ const closePos = end === -1 ? source.length : end + 2;
41
+ // Preserve newlines for line counting
42
+ const segment = source.substring(i, closePos);
43
+ result += segment.replace(/[^\n]/g, ' ');
44
+ i = closePos;
45
+ continue;
46
+ }
47
+ // ── Python: line comments (#) ─────────────────────────────
48
+ if (lang === 'python' && ch === '#') {
49
+ const eol = source.indexOf('\n', i);
50
+ const end = eol === -1 ? source.length : eol;
51
+ result += ' '.repeat(end - i);
52
+ i = end;
53
+ continue;
54
+ }
55
+ // ── Python: triple-quoted strings ─────────────────────────
56
+ if (lang === 'python' && (source.substring(i, i + 3) === '"""' || source.substring(i, i + 3) === "'''")) {
57
+ const quote = source.substring(i, i + 3);
58
+ const end = source.indexOf(quote, i + 3);
59
+ const closePos = end === -1 ? source.length : end + 3;
60
+ const segment = source.substring(i, closePos);
61
+ result += segment.replace(/[^\n]/g, ' ');
62
+ i = closePos;
63
+ continue;
64
+ }
65
+ // ── All languages: string literals ────────────────────────
66
+ if (ch === '"' || ch === "'" || (lang === 'js' && ch === '`')) {
67
+ const quote = ch;
68
+ let j = i + 1;
69
+ while (j < source.length) {
70
+ if (source[j] === '\\') {
71
+ j += 2; // skip escaped character
72
+ continue;
73
+ }
74
+ if (source[j] === quote) {
75
+ j++;
76
+ break;
77
+ }
78
+ // Template literals can span multiple lines
79
+ if (quote === '`' && source[j] === '\n') {
80
+ result += ' '.repeat(j - i);
81
+ result += '\n';
82
+ i = j + 1;
83
+ j = i;
84
+ // Reset — we'll pick up again from the new line
85
+ continue;
86
+ }
87
+ j++;
88
+ }
89
+ const segment = source.substring(i, j);
90
+ result += segment.replace(/[^\n]/g, ' ');
91
+ i = j;
92
+ continue;
93
+ }
94
+ result += ch;
95
+ i++;
96
+ }
97
+ return result;
98
+ }
99
+ // ─── Utility: Line Number Lookup ─────────────────────────────────────
100
+ /**
101
+ * Given a character offset in the source, returns the 1-indexed line number.
102
+ */
103
+ function getLineNumber(source, offset) {
104
+ let line = 1;
105
+ for (let i = 0; i < offset && i < source.length; i++) {
106
+ if (source[i] === '\n')
107
+ line++;
108
+ }
109
+ return line;
110
+ }
111
+ // ── PERF-001: Nested Loop Detection (O(n²) risk) ────────────────────
112
+ //
113
+ // Strategy: Track brace-depth to identify loop constructs that contain
114
+ // other loop constructs. Works across all C-family languages + Python.
115
+ const detectNestedLoops = (source, stripped, lang, findings) => {
116
+ // Python uses indentation, not braces — use a different strategy
117
+ if (lang === 'python') {
118
+ detectNestedLoopsPython(source, stripped, findings);
119
+ return;
120
+ }
121
+ // C-family: scan for loop keywords and track brace depth
122
+ const loopPattern = /\b(for|while|do)\s*[\s(]/g;
123
+ const lines = stripped.split('\n');
124
+ for (let i = 0; i < lines.length; i++) {
125
+ const line = lines[i];
126
+ if (!loopPattern.test(line)) {
127
+ loopPattern.lastIndex = 0;
128
+ continue;
129
+ }
130
+ loopPattern.lastIndex = 0;
131
+ // Found a loop — scan forward tracking brace depth to find nested loops
132
+ let depth = 0;
133
+ let foundOpening = false;
134
+ for (let j = i; j < lines.length; j++) {
135
+ const innerLine = lines[j];
136
+ for (const ch of innerLine) {
137
+ if (ch === '{') {
138
+ depth++;
139
+ foundOpening = true;
140
+ }
141
+ else if (ch === '}') {
142
+ depth--;
143
+ }
144
+ }
145
+ // Check lines inside the outer loop body for nested loops
146
+ if (j > i && foundOpening && depth > 0) {
147
+ if (/\b(for|while|do)\s*[\s(]/.test(innerLine)) {
148
+ findings.push({
149
+ severity: 'WARNING',
150
+ code: 'PERF-001',
151
+ line: j + 1,
152
+ message: `Nested loop detected (outer loop at line ${i + 1}) — O(n²) or worse complexity.`,
153
+ suggestion: 'Consider using a Map/Set for O(1) lookups, or restructure to avoid nested iteration.',
154
+ });
155
+ break; // One warning per outer loop is enough
156
+ }
157
+ }
158
+ // Outer loop body has closed
159
+ if (foundOpening && depth <= 0)
160
+ break;
161
+ }
162
+ }
163
+ };
164
+ function detectNestedLoopsPython(_source, stripped, findings) {
165
+ const lines = stripped.split('\n');
166
+ const loopPattern = /^\s*(for\s+.+\s+in\s+|while\s+)/;
167
+ for (let i = 0; i < lines.length; i++) {
168
+ const outerMatch = lines[i].match(loopPattern);
169
+ if (!outerMatch)
170
+ continue;
171
+ const outerIndent = lines[i].search(/\S/);
172
+ if (outerIndent < 0)
173
+ continue;
174
+ // Scan forward for a nested loop with deeper indentation
175
+ for (let j = i + 1; j < lines.length; j++) {
176
+ const innerLine = lines[j];
177
+ if (innerLine.trim() === '')
178
+ continue;
179
+ const innerIndent = innerLine.search(/\S/);
180
+ // If we've returned to same or lesser indent, the outer loop body is over
181
+ if (innerIndent <= outerIndent)
182
+ break;
183
+ if (loopPattern.test(innerLine)) {
184
+ findings.push({
185
+ severity: 'WARNING',
186
+ code: 'PERF-001',
187
+ line: j + 1,
188
+ message: `Nested loop detected (outer loop at line ${i + 1}) — O(n²) or worse complexity.`,
189
+ suggestion: 'Consider using a dictionary for O(1) lookups, or restructure to avoid nested iteration.',
190
+ });
191
+ break;
192
+ }
193
+ }
194
+ }
195
+ }
196
+ // ── PERF-002: Chained Array Methods (multiple intermediate allocations) ──
197
+ const detectChainedArrayMethods = (_source, stripped, lang, findings) => {
198
+ if (lang !== 'js')
199
+ return;
200
+ // Match chains like .map(...).filter(...).reduce(...) with 3+ methods
201
+ 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;
202
+ const lines = stripped.split('\n');
203
+ // Search across joined lines since chains can span multiple lines
204
+ const joined = stripped;
205
+ let match;
206
+ while ((match = chainPattern.exec(joined)) !== null) {
207
+ const line = getLineNumber(stripped, match.index);
208
+ findings.push({
209
+ severity: 'WARNING',
210
+ code: 'PERF-002',
211
+ line,
212
+ message: `Chained array methods (.${match[1]}().${match[2]}().${match[3]}()) — creates 3+ intermediate arrays.`,
213
+ suggestion: 'Combine into a single .reduce() or use a for-loop to avoid intermediate allocations.',
214
+ });
215
+ }
216
+ };
217
+ // ── PERF-003: Synchronous I/O in Async Functions ─────────────────────
218
+ const detectSyncIOInAsync = (_source, stripped, lang, findings) => {
219
+ if (lang !== 'js')
220
+ return;
221
+ const syncAPIs = [
222
+ 'readFileSync', 'writeFileSync', 'appendFileSync', 'mkdirSync',
223
+ 'readdirSync', 'statSync', 'existsSync', 'copyFileSync',
224
+ 'renameSync', 'unlinkSync', 'rmdirSync', 'accessSync',
225
+ ];
226
+ const lines = stripped.split('\n');
227
+ let insideAsync = false;
228
+ let asyncBraceDepth = 0;
229
+ let braceDepth = 0;
230
+ for (let i = 0; i < lines.length; i++) {
231
+ const line = lines[i];
232
+ // Detect async function/method/arrow entry
233
+ if (/\basync\s+(function\b|\w+\s*\(|(\w+)\s*=>|\(.*\)\s*=>)/.test(line) || /\basync\s+\w+\s*\(/.test(line)) {
234
+ insideAsync = true;
235
+ asyncBraceDepth = braceDepth;
236
+ }
237
+ for (const ch of line) {
238
+ if (ch === '{')
239
+ braceDepth++;
240
+ else if (ch === '}') {
241
+ braceDepth--;
242
+ if (insideAsync && braceDepth <= asyncBraceDepth) {
243
+ insideAsync = false;
244
+ }
245
+ }
246
+ }
247
+ if (insideAsync) {
248
+ for (const api of syncAPIs) {
249
+ if (line.includes(api)) {
250
+ findings.push({
251
+ severity: 'ERROR',
252
+ code: 'PERF-003',
253
+ line: i + 1,
254
+ message: `Synchronous I/O (${api}) inside async function — blocks the event loop.`,
255
+ suggestion: 'Use the async equivalent (e.g., fs.promises.readFile or fs/promises) instead.',
256
+ });
257
+ }
258
+ }
259
+ }
260
+ }
261
+ };
262
+ // ── PERF-004: Object/Array Spread Inside Loops ───────────────────────
263
+ const detectSpreadInLoops = (_source, stripped, lang, findings) => {
264
+ if (lang !== 'js')
265
+ return;
266
+ const lines = stripped.split('\n');
267
+ let loopDepth = 0;
268
+ let braceDepth = 0;
269
+ let inLoop = false;
270
+ let loopBraceStart = 0;
271
+ for (let i = 0; i < lines.length; i++) {
272
+ const line = lines[i];
273
+ if (/\b(for|while|do)\s*[\s(]/.test(line) && !inLoop) {
274
+ inLoop = true;
275
+ loopBraceStart = braceDepth;
276
+ loopDepth++;
277
+ }
278
+ for (const ch of line) {
279
+ if (ch === '{')
280
+ braceDepth++;
281
+ else if (ch === '}') {
282
+ braceDepth--;
283
+ if (inLoop && braceDepth <= loopBraceStart) {
284
+ inLoop = false;
285
+ loopDepth--;
286
+ }
287
+ }
288
+ }
289
+ if (loopDepth > 0 && /\.\.\.[\w$]/.test(line)) {
290
+ findings.push({
291
+ severity: 'WARNING',
292
+ code: 'PERF-004',
293
+ line: i + 1,
294
+ message: 'Object/Array spread inside a loop — creates a full shallow copy on every iteration.',
295
+ suggestion: 'Mutate the object directly or accumulate into a pre-allocated structure.',
296
+ });
297
+ }
298
+ }
299
+ };
300
+ // ── PERF-005: Missing Resource Cleanup ───────────────────────────────
301
+ const detectMissingCleanup = (source, stripped, lang, findings) => {
302
+ if (lang !== 'js' && lang !== 'python')
303
+ return;
304
+ if (lang === 'js') {
305
+ // Look for createReadStream/createWriteStream/createServer assigned to a variable
306
+ const pattern = /\b(const|let|var)\s+(\w+)\s*=\s*.*\b(createReadStream|createWriteStream|createServer|createConnection|net\.connect|tls\.connect)\b/g;
307
+ let match;
308
+ while ((match = pattern.exec(stripped)) !== null) {
309
+ const varName = match[2];
310
+ const creator = match[3];
311
+ const line = getLineNumber(stripped, match.index);
312
+ // Check if .close(), .destroy(), .end(), or .disconnect() is called on this variable
313
+ // within a reasonable scope (the rest of the file)
314
+ const rest = stripped.substring(match.index);
315
+ const hasCleanup = rest.includes(`${varName}.close()`) ||
316
+ rest.includes(`${varName}.destroy()`) ||
317
+ rest.includes(`${varName}.end()`) ||
318
+ rest.includes(`${varName}.disconnect()`) ||
319
+ // Also accept piping (which handles cleanup) or 'using' declarations
320
+ rest.includes(`${varName}.pipe(`) ||
321
+ stripped.includes(`using ${varName}`);
322
+ if (!hasCleanup) {
323
+ findings.push({
324
+ severity: 'WARNING',
325
+ code: 'PERF-005',
326
+ line,
327
+ message: `Resource created via ${creator}() assigned to '${varName}' but no .close()/.destroy()/.end() found.`,
328
+ suggestion: "Use a try/finally block or 'using' declaration to ensure cleanup.",
329
+ });
330
+ }
331
+ }
332
+ }
333
+ if (lang === 'python') {
334
+ // Detect open() not inside a 'with' statement
335
+ const lines = stripped.split('\n');
336
+ for (let i = 0; i < lines.length; i++) {
337
+ const line = lines[i];
338
+ // Match: var = open(...) but NOT: with open(...) as var
339
+ if (/=\s*open\s*\(/.test(line) && !/\bwith\b/.test(line)) {
340
+ findings.push({
341
+ severity: 'WARNING',
342
+ code: 'PERF-005',
343
+ line: i + 1,
344
+ message: "File opened with open() without a 'with' statement — resource may not be properly closed.",
345
+ suggestion: "Use 'with open(...) as f:' to ensure the file handle is automatically closed.",
346
+ });
347
+ }
348
+ }
349
+ }
350
+ };
351
+ // ── PERF-006: eval() / new Function() / exec() ──────────────────────
352
+ const detectDangerousEval = (_source, stripped, lang, findings) => {
353
+ const lines = stripped.split('\n');
354
+ for (let i = 0; i < lines.length; i++) {
355
+ const line = lines[i];
356
+ if (lang === 'js') {
357
+ // Match eval(...) but not .addEventListener('...', eval)
358
+ if (/\beval\s*\(/.test(line)) {
359
+ findings.push({
360
+ severity: 'ERROR',
361
+ code: 'PERF-006',
362
+ line: i + 1,
363
+ message: 'eval() usage — prevents V8 optimizations and poses security risks.',
364
+ suggestion: 'Use JSON.parse(), a lookup table, or refactor the logic to avoid dynamic code evaluation.',
365
+ });
366
+ }
367
+ if (/\bnew\s+Function\s*\(/.test(line)) {
368
+ findings.push({
369
+ severity: 'ERROR',
370
+ code: 'PERF-006',
371
+ line: i + 1,
372
+ message: 'new Function() — dynamically compiled code bypasses V8 optimization pipeline.',
373
+ suggestion: 'Pre-compile or use a safer alternative (e.g., a lookup table or map).',
374
+ });
375
+ }
376
+ }
377
+ if (lang === 'python') {
378
+ if (/\beval\s*\(/.test(line)) {
379
+ findings.push({
380
+ severity: 'ERROR',
381
+ code: 'PERF-006',
382
+ line: i + 1,
383
+ message: 'eval() usage — arbitrary code execution risk and performance penalty.',
384
+ suggestion: 'Use ast.literal_eval() for safe evaluation, or refactor to avoid dynamic code.',
385
+ });
386
+ }
387
+ if (/\bexec\s*\(/.test(line)) {
388
+ findings.push({
389
+ severity: 'ERROR',
390
+ code: 'PERF-006',
391
+ line: i + 1,
392
+ message: 'exec() usage — arbitrary code execution risk.',
393
+ suggestion: 'Refactor to avoid dynamic code execution. Use importlib or a dispatch table.',
394
+ });
395
+ }
396
+ }
397
+ }
398
+ };
399
+ // ── PERF-007: JSON.parse on Unvalidated Input ────────────────────────
400
+ const detectUnsafeJsonParse = (_source, stripped, lang, findings) => {
401
+ if (lang !== 'js')
402
+ return;
403
+ const lines = stripped.split('\n');
404
+ let insideAsync = false;
405
+ let asyncBraceDepth = 0;
406
+ let braceDepth = 0;
407
+ for (let i = 0; i < lines.length; i++) {
408
+ const line = lines[i];
409
+ if (/\basync\s+/.test(line)) {
410
+ insideAsync = true;
411
+ asyncBraceDepth = braceDepth;
412
+ }
413
+ for (const ch of line) {
414
+ if (ch === '{')
415
+ braceDepth++;
416
+ else if (ch === '}') {
417
+ braceDepth--;
418
+ if (insideAsync && braceDepth <= asyncBraceDepth)
419
+ insideAsync = false;
420
+ }
421
+ }
422
+ // Detect JSON.parse(req.body) / JSON.parse(body) / JSON.parse(data) etc.
423
+ // in what appears to be a request handler context
424
+ if (/JSON\.parse\s*\(/.test(line)) {
425
+ // Check if this is inside a try block — if so, it's handled
426
+ const prevLines = lines.slice(Math.max(0, i - 5), i).join('\n');
427
+ if (/\btry\s*\{/.test(prevLines))
428
+ continue;
429
+ // Check if the argument looks like an external input
430
+ const argMatch = line.match(/JSON\.parse\s*\(\s*(\w+)/);
431
+ if (argMatch) {
432
+ const argName = argMatch[1].toLowerCase();
433
+ const externalHints = ['body', 'data', 'payload', 'input', 'raw', 'text', 'chunk', 'buffer', 'message'];
434
+ if (externalHints.some((hint) => argName.includes(hint))) {
435
+ findings.push({
436
+ severity: 'INFO',
437
+ code: 'PERF-007',
438
+ line: i + 1,
439
+ message: `JSON.parse() on '${argMatch[1]}' without try/catch — may throw on malformed input or cause OOM on large payloads.`,
440
+ suggestion: 'Wrap in try/catch and consider validating payload size before parsing.',
441
+ });
442
+ }
443
+ }
444
+ }
445
+ }
446
+ };
447
+ // ── PERF-008: Unbounded Database/API Fetch ───────────────────────────
448
+ const detectUnboundedFetch = (_source, stripped, lang, findings) => {
449
+ const lines = stripped.split('\n');
450
+ for (let i = 0; i < lines.length; i++) {
451
+ const line = lines[i];
452
+ if (lang === 'js') {
453
+ // Detect .find() / .findMany() / .select() / .query() without .limit() / .take() / .paginate()
454
+ // Common in Prisma, Mongoose, Knex, Sequelize
455
+ if (/\.(find|findMany|findAll|select|query)\s*\(/.test(line)) {
456
+ // Look ahead a few lines for a .limit() / .take() / .skip() / .paginate() / .first()
457
+ const window = lines.slice(i, Math.min(i + 5, lines.length)).join('\n');
458
+ if (!/\.(limit|take|first|paginate|skip|offset|top)\s*\(/.test(window) &&
459
+ !/\bLIMIT\b/i.test(window)) {
460
+ findings.push({
461
+ severity: 'WARNING',
462
+ code: 'PERF-008',
463
+ line: i + 1,
464
+ message: 'Database/API query without .limit() or pagination — may fetch unbounded rows into memory.',
465
+ suggestion: 'Add .limit(), .take(), or cursor-based pagination to bound the result set.',
466
+ });
467
+ }
468
+ }
469
+ }
470
+ if (lang === 'python') {
471
+ // Detect .all() / .filter() on Django/SQLAlchemy querysets without [:N] or .limit()
472
+ if (/\.(all|filter|objects\.filter|objects\.all)\s*\(/.test(line)) {
473
+ const window = lines.slice(i, Math.min(i + 5, lines.length)).join('\n');
474
+ if (!/\.(limit|first|paginate|count)\s*\(/.test(window) &&
475
+ !/\[:?\d+\]/.test(window) &&
476
+ !/\bLIMIT\b/i.test(window)) {
477
+ findings.push({
478
+ severity: 'WARNING',
479
+ code: 'PERF-008',
480
+ line: i + 1,
481
+ message: 'Query without .limit() or slicing — may fetch unbounded rows into memory.',
482
+ suggestion: 'Add .limit(), slicing [:N], or cursor-based pagination to bound the result set.',
483
+ });
484
+ }
485
+ }
486
+ }
487
+ // Go / Rust: detect SQL queries without LIMIT
488
+ if (lang === 'go' || lang === 'rust') {
489
+ if (/SELECT\s+/i.test(line) && !/\bLIMIT\b/i.test(line)) {
490
+ // Look ahead for LIMIT on subsequent lines
491
+ const window = lines.slice(i, Math.min(i + 5, lines.length)).join('\n');
492
+ if (!/\bLIMIT\b/i.test(window)) {
493
+ findings.push({
494
+ severity: 'WARNING',
495
+ code: 'PERF-008',
496
+ line: i + 1,
497
+ message: 'SQL SELECT without LIMIT clause — may fetch unbounded rows into memory.',
498
+ suggestion: 'Add a LIMIT clause or use cursor-based pagination.',
499
+ });
500
+ }
501
+ }
502
+ }
503
+ }
504
+ };
505
+ // ─── Rule Registry ───────────────────────────────────────────────────
506
+ const ALL_RULES = [
507
+ detectNestedLoops,
508
+ detectChainedArrayMethods,
509
+ detectSyncIOInAsync,
510
+ detectSpreadInLoops,
511
+ detectMissingCleanup,
512
+ detectDangerousEval,
513
+ detectUnsafeJsonParse,
514
+ detectUnboundedFetch,
515
+ ];
516
+ // ─── Supported Extensions for Auditing ───────────────────────────────
517
+ const AUDITABLE_EXTENSIONS = new Set(Object.keys(EXTENSION_MAP));
518
+ /**
519
+ * Returns true if the file extension is supported by the performance auditor.
520
+ */
521
+ export function isAuditableFile(filePath) {
522
+ return AUDITABLE_EXTENSIONS.has(path.extname(filePath).toLowerCase());
523
+ }
524
+ // ─── Main Entry Point ────────────────────────────────────────────────
525
+ /**
526
+ * Scans source code for performance anti-patterns using language-aware
527
+ * regex-based heuristics. Returns structured findings grouped by severity.
528
+ *
529
+ * This function is designed to be called inline during the verification
530
+ * phase — it's synchronous, zero-dependency, and executes in <50ms on
531
+ * files up to 10,000 lines.
532
+ *
533
+ * @param content - The raw source code string to audit.
534
+ * @param filePath - The file path (used for language detection via extension).
535
+ * @returns A structured audit result with findings separated by severity.
536
+ */
537
+ export function auditFilePerformance(content, filePath) {
538
+ const lang = detectLanguage(filePath);
539
+ const findings = [];
540
+ if (lang === 'unknown') {
541
+ return { filePath, findings: [], errors: [], warnings: [], infos: [] };
542
+ }
543
+ // Strip strings and comments to avoid false positives
544
+ const stripped = stripStringsAndComments(content, lang);
545
+ for (const rule of ALL_RULES) {
546
+ rule(content, stripped, lang, findings);
547
+ }
548
+ // Sort by severity (ERROR first), then by line number
549
+ const severityOrder = { ERROR: 0, WARNING: 1, INFO: 2 };
550
+ findings.sort((a, b) => {
551
+ const sevDiff = severityOrder[a.severity] - severityOrder[b.severity];
552
+ return sevDiff !== 0 ? sevDiff : a.line - b.line;
553
+ });
554
+ return {
555
+ filePath,
556
+ findings,
557
+ errors: findings.filter((f) => f.severity === 'ERROR'),
558
+ warnings: findings.filter((f) => f.severity === 'WARNING'),
559
+ infos: findings.filter((f) => f.severity === 'INFO'),
560
+ };
561
+ }
562
+ // ─── Output Formatters ───────────────────────────────────────────────
563
+ const SEVERITY_ICONS = {
564
+ ERROR: '🔴',
565
+ WARNING: '🟡',
566
+ INFO: '🔵',
567
+ };
568
+ /**
569
+ * Formats audit findings for beautiful terminal display using picocolors.
570
+ * Used for non-blocking warnings shown to the user after verification.
571
+ */
572
+ export function formatAuditForTerminal(results) {
573
+ const allFindings = results.flatMap((r) => r.findings.map((f) => ({ ...f, file: r.filePath })));
574
+ if (allFindings.length === 0)
575
+ return '';
576
+ const lines = [
577
+ pc.bold('Performance Audit Results'),
578
+ pc.dim('─'.repeat(50)),
579
+ ];
580
+ for (const f of allFindings) {
581
+ const icon = SEVERITY_ICONS[f.severity];
582
+ const sevColor = f.severity === 'ERROR' ? pc.red : f.severity === 'WARNING' ? pc.yellow : pc.blue;
583
+ lines.push(`${icon} ${sevColor(f.severity)} ${pc.dim(`[${f.code}]`)} ${pc.cyan(f.file)}${pc.dim(`:${f.line}`)}`);
584
+ lines.push(` ${f.message}`);
585
+ lines.push(` ${pc.dim('→')} ${pc.dim(f.suggestion)}`);
586
+ }
587
+ const errorCount = allFindings.filter((f) => f.severity === 'ERROR').length;
588
+ const warnCount = allFindings.filter((f) => f.severity === 'WARNING').length;
589
+ const infoCount = allFindings.filter((f) => f.severity === 'INFO').length;
590
+ lines.push(pc.dim('─'.repeat(50)));
591
+ lines.push(`${errorCount > 0 ? pc.red(`${errorCount} error(s)`) : '0 errors'}, ` +
592
+ `${warnCount > 0 ? pc.yellow(`${warnCount} warning(s)`) : '0 warnings'}, ` +
593
+ `${infoCount > 0 ? pc.blue(`${infoCount} info`) : '0 info'}`);
594
+ return lines.join('\n');
595
+ }
596
+ /**
597
+ * Formats audit findings into a structured prompt for the AI auto-correction loop.
598
+ * Only includes ERROR-severity findings (warnings and info are non-blocking).
599
+ */
600
+ export function formatAuditForModel(results) {
601
+ const errors = results.flatMap((r) => r.errors.map((f) => ({ ...f, file: r.filePath })));
602
+ if (errors.length === 0)
603
+ return '';
604
+ const lines = [
605
+ 'The following performance anti-patterns were detected in your generated code:',
606
+ '',
607
+ ];
608
+ for (const e of errors) {
609
+ lines.push(`[${e.code}] ${e.file}:${e.line} — ${e.message}`);
610
+ lines.push(` Fix: ${e.suggestion}`);
611
+ lines.push('');
612
+ }
613
+ lines.push('Please fix these performance issues using the modify_file tool.');
614
+ return lines.join('\n');
615
+ }