pi-lens 2.0.37 → 2.0.39

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 (46) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/README.md +1 -1
  3. package/clients/architect-client.js +50 -20
  4. package/clients/architect-client.ts +61 -22
  5. package/clients/ast-grep-client.js +32 -127
  6. package/clients/ast-grep-client.test.js +2 -14
  7. package/clients/ast-grep-client.test.ts +2 -16
  8. package/clients/ast-grep-client.ts +34 -150
  9. package/clients/auto-loop.js +117 -0
  10. package/clients/auto-loop.ts +171 -0
  11. package/clients/biome-client.js +25 -22
  12. package/clients/biome-client.ts +28 -25
  13. package/clients/complexity-client.js +111 -74
  14. package/clients/complexity-client.ts +149 -105
  15. package/clients/dependency-checker.js +16 -30
  16. package/clients/dependency-checker.ts +15 -35
  17. package/clients/fix-scanners.js +195 -0
  18. package/clients/fix-scanners.ts +297 -0
  19. package/clients/interviewer-templates.js +75 -0
  20. package/clients/interviewer-templates.ts +90 -0
  21. package/clients/interviewer.js +73 -101
  22. package/clients/interviewer.ts +195 -140
  23. package/clients/knip-client.js +12 -4
  24. package/clients/knip-client.ts +21 -16
  25. package/clients/metrics-history.js +215 -0
  26. package/clients/metrics-history.ts +300 -0
  27. package/clients/scan-architectural-debt.js +62 -72
  28. package/clients/scan-architectural-debt.ts +79 -64
  29. package/clients/scan-utils.js +98 -0
  30. package/clients/scan-utils.ts +112 -0
  31. package/clients/sg-runner.js +138 -0
  32. package/clients/sg-runner.ts +168 -0
  33. package/clients/subprocess-client.js +0 -37
  34. package/clients/subprocess-client.ts +0 -60
  35. package/clients/ts-service.ts +1 -7
  36. package/clients/type-safety-client.js +3 -8
  37. package/clients/type-safety-client.ts +10 -14
  38. package/clients/typescript-client.js +55 -56
  39. package/clients/typescript-client.ts +94 -52
  40. package/default-architect.yaml +87 -0
  41. package/index.ts +143 -1165
  42. package/package.json +2 -1
  43. package/rules/ast-grep-rules/rules/large-class.yml +6 -2
  44. package/rules/ast-grep-rules/rules/long-method.yml +1 -1
  45. package/rules/ast-grep-rules/rules/no-single-char-var.yml +2 -2
  46. package/rules/ast-grep-rules/rules/switch-without-default.yml +0 -12
@@ -92,14 +92,26 @@ export class BiomeClient {
92
92
  ].includes(ext);
93
93
  }
94
94
 
95
+ // --- Internal helpers ---
96
+
95
97
  /**
96
- * Run biome check (format + lint) without fixing returns diagnostics
98
+ * Validate path and availability returns path or null on failure
97
99
  */
98
- checkFile(filePath: string): BiomeDiagnostic[] {
99
- if (!this.isAvailable()) return [];
100
+ private withValidatedPath(filePath: string): string | null {
101
+ if (!this.isAvailable()) return null;
100
102
 
101
103
  const absolutePath = path.resolve(filePath);
102
- if (!fs.existsSync(absolutePath)) return [];
104
+ if (!fs.existsSync(absolutePath)) return null;
105
+
106
+ return absolutePath;
107
+ }
108
+
109
+ /**
110
+ * Run biome check (format + lint) without fixing — returns diagnostics
111
+ */
112
+ checkFile(filePath: string): BiomeDiagnostic[] {
113
+ const absolutePath = this.withValidatedPath(filePath);
114
+ if (!absolutePath) return [];
103
115
 
104
116
  try {
105
117
  const result = spawnSync(
@@ -137,12 +149,13 @@ export class BiomeClient {
137
149
  changed: boolean;
138
150
  error?: string;
139
151
  } {
140
- if (!this.isAvailable())
141
- return { success: false, changed: false, error: "Biome not available" };
142
-
143
- const absolutePath = path.resolve(filePath);
144
- if (!fs.existsSync(absolutePath))
145
- return { success: false, changed: false, error: "File not found" };
152
+ const absolutePath = this.withValidatedPath(filePath);
153
+ if (!absolutePath)
154
+ return {
155
+ success: false,
156
+ changed: false,
157
+ error: this.isAvailable() ? "File not found" : "Biome not available",
158
+ };
146
159
 
147
160
  const content = fs.readFileSync(absolutePath, "utf-8");
148
161
 
@@ -184,21 +197,13 @@ export class BiomeClient {
184
197
  fixed: number;
185
198
  error?: string;
186
199
  } {
187
- if (!this.isAvailable())
188
- return {
189
- success: false,
190
- changed: false,
191
- fixed: 0,
192
- error: "Biome not available",
193
- };
194
-
195
- const absolutePath = path.resolve(filePath);
196
- if (!fs.existsSync(absolutePath))
200
+ const absolutePath = this.withValidatedPath(filePath);
201
+ if (!absolutePath)
197
202
  return {
198
203
  success: false,
199
204
  changed: false,
200
205
  fixed: 0,
201
- error: "File not found",
206
+ error: this.isAvailable() ? "File not found" : "Biome not available",
202
207
  };
203
208
 
204
209
  const content = fs.readFileSync(absolutePath, "utf-8");
@@ -288,10 +293,8 @@ export class BiomeClient {
288
293
  * Generate a diff-like summary of formatting changes
289
294
  */
290
295
  getFormatDiff(filePath: string): string {
291
- if (!this.isAvailable()) return "";
292
-
293
- const absolutePath = path.resolve(filePath);
294
- if (!fs.existsSync(absolutePath)) return "";
296
+ const absolutePath = this.withValidatedPath(filePath);
297
+ if (!absolutePath) return "";
295
298
 
296
299
  const content = fs.readFileSync(absolutePath, "utf-8");
297
300
 
@@ -126,70 +126,88 @@ export class ComplexityClient {
126
126
  * Analyze complexity metrics for a file
127
127
  */
128
128
  analyzeFile(filePath) {
129
- const absolutePath = path.resolve(filePath);
130
- if (!fs.existsSync(absolutePath))
129
+ const parsed = this.readAndParse(filePath);
130
+ if (!parsed)
131
131
  return null;
132
132
  try {
133
- const content = fs.readFileSync(absolutePath, "utf-8");
134
- const lines = content.split("\n");
135
- const sourceFile = ts.createSourceFile(filePath, content, ts.ScriptTarget.Latest, true);
136
- // Count lines of code (non-empty, non-comment)
137
- const { codeLines, commentLines } = this.countLines(sourceFile, lines);
138
- // Collect function metrics
139
- const functions = [];
140
- this.collectFunctionMetrics(sourceFile, sourceFile, functions, 0);
141
- // Calculate file-level metrics
142
- const maxNestingDepth = this.calculateMaxNesting(sourceFile, 0);
143
- const _cyclomatic = this.calculateCyclomaticComplexity(sourceFile);
144
- const cognitive = this.calculateCognitiveComplexity(sourceFile);
145
- const halstead = this.calculateHalsteadVolume(sourceFile);
146
- // Function length stats
147
- const funcLengths = functions.map((f) => f.length);
148
- const avgFunctionLength = funcLengths.length > 0
149
- ? Math.round(funcLengths.reduce((a, b) => a + b, 0) / funcLengths.length)
150
- : 0;
151
- const maxFunctionLength = funcLengths.length > 0 ? Math.max(...funcLengths) : 0;
152
- // Function cyclomatic stats
153
- const cyclomatics = functions.map((f) => f.cyclomatic);
154
- const avgCyclomatic = cyclomatics.length > 0
155
- ? Math.round(cyclomatics.reduce((a, b) => a + b, 0) / cyclomatics.length)
156
- : 1;
157
- const maxCyclomatic = cyclomatics.length > 0 ? Math.max(...cyclomatics) : 1;
158
- // Maintainability Index (simplified Microsoft formula)
159
- // MI = max(0, (171 - 5.2 * ln(Halstead) - 0.23 * Cyclomatic - 16.2 * ln(LOC)) * 100 / 171)
160
- const maintainabilityIndex = this.calculateMaintainabilityIndex(halstead, avgCyclomatic, codeLines, commentLines);
161
- // Code Entropy (Shannon entropy of code tokens)
162
- const codeEntropy = this.calculateCodeEntropy(content);
163
- // AI slop indicators
164
- const maxParamsInFunction = this.calculateMaxParams(functions);
165
- const aiCommentPatterns = this.countAICommentPatterns(sourceFile);
166
- const singleUseFunctions = this.countSingleUseFunctions(functions);
167
- const tryCatchCount = this.countTryCatch(sourceFile);
168
- return {
169
- filePath: path.relative(process.cwd(), absolutePath),
170
- maxNestingDepth,
171
- avgFunctionLength,
172
- maxFunctionLength,
173
- functionCount: functions.length,
174
- cyclomaticComplexity: avgCyclomatic,
175
- maxCyclomaticComplexity: maxCyclomatic,
176
- cognitiveComplexity: cognitive,
177
- halsteadVolume: Math.round(halstead * 10) / 10,
178
- maintainabilityIndex: Math.round(maintainabilityIndex * 10) / 10,
179
- linesOfCode: codeLines,
180
- commentLines,
181
- codeEntropy: Math.round(codeEntropy * 100) / 100,
182
- maxParamsInFunction,
183
- aiCommentPatterns,
184
- singleUseFunctions,
185
- tryCatchCount,
186
- };
133
+ return this.computeMetrics(parsed);
187
134
  }
188
135
  catch (err) {
189
136
  this.log(`Analysis error for ${filePath}: ${err.message}`);
190
137
  return null;
191
138
  }
192
139
  }
140
+ /**
141
+ * Read file and parse to TypeScript AST
142
+ */
143
+ readAndParse(filePath) {
144
+ const absolutePath = path.resolve(filePath);
145
+ if (!fs.existsSync(absolutePath))
146
+ return null;
147
+ const content = fs.readFileSync(absolutePath, "utf-8");
148
+ const sourceFile = ts.createSourceFile(filePath, content, ts.ScriptTarget.Latest, true);
149
+ return { absolutePath, content, sourceFile };
150
+ }
151
+ /**
152
+ * Compute all metrics from parsed source
153
+ */
154
+ computeMetrics(parsed) {
155
+ const { absolutePath, content, sourceFile } = parsed;
156
+ const lines = content.split("\n");
157
+ // Line counts and function collection
158
+ const { codeLines, commentLines } = this.countLines(sourceFile, lines);
159
+ const functions = this.collectFunctionMetrics(sourceFile);
160
+ // File-level complexity metrics
161
+ const maxNestingDepth = this.calculateMaxNesting(sourceFile, 0);
162
+ const cognitive = this.calculateCognitiveComplexity(sourceFile);
163
+ const halstead = this.calculateHalsteadVolume(sourceFile);
164
+ // Aggregate function statistics
165
+ const funcStats = this.aggregateFunctionStats(functions);
166
+ // Derived metrics
167
+ const maintainabilityIndex = this.calculateMaintainabilityIndex(halstead, funcStats.avgCyclomatic, codeLines, commentLines);
168
+ const codeEntropy = this.calculateCodeEntropy(content);
169
+ // AI slop indicators
170
+ const maxParamsInFunction = this.calculateMaxParams(functions);
171
+ const aiCommentPatterns = this.countAICommentPatterns(sourceFile);
172
+ const singleUseFunctions = this.countSingleUseFunctions(functions);
173
+ const tryCatchCount = this.countTryCatch(sourceFile);
174
+ return {
175
+ filePath: path.relative(process.cwd(), absolutePath),
176
+ maxNestingDepth,
177
+ avgFunctionLength: funcStats.avgLength,
178
+ maxFunctionLength: funcStats.maxLength,
179
+ functionCount: functions.length,
180
+ cyclomaticComplexity: funcStats.avgCyclomatic,
181
+ maxCyclomaticComplexity: funcStats.maxCyclomatic,
182
+ cognitiveComplexity: cognitive,
183
+ halsteadVolume: Math.round(halstead * 10) / 10,
184
+ maintainabilityIndex: Math.round(maintainabilityIndex * 10) / 10,
185
+ linesOfCode: codeLines,
186
+ commentLines,
187
+ codeEntropy: Math.round(codeEntropy * 100) / 100,
188
+ maxParamsInFunction,
189
+ aiCommentPatterns,
190
+ singleUseFunctions,
191
+ tryCatchCount,
192
+ };
193
+ }
194
+ /**
195
+ * Aggregate function metrics into summary statistics
196
+ */
197
+ aggregateFunctionStats(functions) {
198
+ if (functions.length === 0) {
199
+ return { avgLength: 0, maxLength: 0, avgCyclomatic: 1, maxCyclomatic: 1 };
200
+ }
201
+ const lengths = functions.map((f) => f.length);
202
+ const cyclomatics = functions.map((f) => f.cyclomatic);
203
+ const sum = (arr) => arr.reduce((a, b) => a + b, 0);
204
+ return {
205
+ avgLength: Math.round(sum(lengths) / lengths.length),
206
+ maxLength: Math.max(...lengths),
207
+ avgCyclomatic: Math.max(1, Math.round(sum(cyclomatics) / cyclomatics.length)),
208
+ maxCyclomatic: Math.max(1, Math.max(...cyclomatics)),
209
+ };
210
+ }
193
211
  /**
194
212
  * Format metrics for display
195
213
  */
@@ -316,9 +334,9 @@ export class ComplexityClient {
316
334
  if (metrics.codeEntropy > 3.5) {
317
335
  warnings.push(`High entropy (${metrics.codeEntropy.toFixed(1)} bits) — follow project conventions`);
318
336
  }
319
- // Comments ratio (>30% = excessive comments, AI slop signal)
337
+ // Comments ratio (>40% = excessive comments, AI slop signal)
320
338
  const totalLines = metrics.linesOfCode + metrics.commentLines;
321
- if (totalLines > 10 && metrics.commentLines / totalLines > 0.3) {
339
+ if (totalLines > 10 && metrics.commentLines / totalLines > 0.4) {
322
340
  warnings.push(`Excessive comments (${Math.round((metrics.commentLines / totalLines) * 100)}%) — remove obvious comments`);
323
341
  }
324
342
  // Verbose code (long functions with low complexity = overly verbose)
@@ -330,7 +348,7 @@ export class ComplexityClient {
330
348
  warnings.push(`AI-style comments (${metrics.aiCommentPatterns}) — remove hand-holding comments`);
331
349
  }
332
350
  // AI slop: Too many try/catch blocks (lazy error handling)
333
- if (metrics.tryCatchCount > 5) {
351
+ if (metrics.tryCatchCount > 15) {
334
352
  warnings.push(`Many try/catch blocks (${metrics.tryCatchCount}) — consolidate error handling`);
335
353
  }
336
354
  // AI slop: Over-abstraction (many single-use helper functions)
@@ -385,12 +403,31 @@ export class ComplexityClient {
385
403
  commentLines = commentPositions.size;
386
404
  const codeLines = lines.filter((line, i) => {
387
405
  const trimmed = line.trim();
388
- return trimmed.length > 0 && !commentPositions.has(i);
406
+ if (trimmed.length === 0)
407
+ return false;
408
+ // If the line is not in commentPositions, it definitely has code
409
+ if (!commentPositions.has(i))
410
+ return true;
411
+ // If it IS in commentPositions, it might still have code (trailing comment)
412
+ // Remove the comment part and check if anything remains
413
+ const lineWithoutComments = line
414
+ .replace(/\/\/.*$/, "")
415
+ .replace(/\/\*[\s\S]*?\*\//g, "")
416
+ .trim();
417
+ return lineWithoutComments.length > 0;
389
418
  }).length;
390
419
  return { codeLines, commentLines };
391
420
  }
392
421
  // --- Private: Function Metrics Collection ---
393
- collectFunctionMetrics(node, sourceFile, functions, nestingLevel) {
422
+ /**
423
+ * Collect metrics for all functions in the source file
424
+ */
425
+ collectFunctionMetrics(sourceFile) {
426
+ const functions = [];
427
+ this.visitFunctionMetrics(sourceFile, sourceFile, functions, 0);
428
+ return functions;
429
+ }
430
+ visitFunctionMetrics(node, sourceFile, functions, nestingLevel) {
394
431
  if (FUNCTION_LIKE_NODES.has(node.kind)) {
395
432
  const funcNode = node;
396
433
  const startLine = sourceFile.getLineAndCharacterOfPosition(node.getStart()).line;
@@ -416,7 +453,7 @@ export class ComplexityClient {
416
453
  ? nestingLevel + 1
417
454
  : nestingLevel;
418
455
  ts.forEachChild(node, (child) => {
419
- this.collectFunctionMetrics(child, sourceFile, functions, newNesting);
456
+ this.visitFunctionMetrics(child, sourceFile, functions, newNesting);
420
457
  });
421
458
  }
422
459
  // --- Private: Max Nesting Depth ---
@@ -436,18 +473,22 @@ export class ComplexityClient {
436
473
  calculateCyclomaticComplexity(node) {
437
474
  return this.nodeCyclomaticComplexity(node, 0);
438
475
  }
476
+ isLogicalOperator(node) {
477
+ if (node.kind === ts.SyntaxKind.BinaryExpression) {
478
+ const binary = node;
479
+ return (binary.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken ||
480
+ binary.operatorToken.kind === ts.SyntaxKind.BarBarToken);
481
+ }
482
+ return false;
483
+ }
439
484
  nodeCyclomaticComplexity(node, complexity) {
440
485
  // Base increment for branching nodes
441
486
  if (CYCLOMAL_NODES.has(node.kind)) {
442
487
  complexity++;
443
488
  }
444
489
  // Binary && and || add complexity
445
- if (node.kind === ts.SyntaxKind.BinaryExpression) {
446
- const binary = node;
447
- if (binary.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken ||
448
- binary.operatorToken.kind === ts.SyntaxKind.BarBarToken) {
449
- complexity++;
450
- }
490
+ if (this.isLogicalOperator(node)) {
491
+ complexity++;
451
492
  }
452
493
  ts.forEachChild(node, (child) => {
453
494
  complexity = this.nodeCyclomaticComplexity(child, complexity);
@@ -475,12 +516,8 @@ export class ComplexityClient {
475
516
  }
476
517
  }
477
518
  // Binary && and || contribute to complexity
478
- if (node.kind === ts.SyntaxKind.BinaryExpression) {
479
- const binary = node;
480
- if (binary.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken ||
481
- binary.operatorToken.kind === ts.SyntaxKind.BarBarToken) {
482
- complexity += 1;
483
- }
519
+ if (this.isLogicalOperator(node)) {
520
+ complexity += 1;
484
521
  }
485
522
  // Calculate nesting for children
486
523
  const increasesNesting = NESTING_NODES.has(node.kind);
@@ -171,97 +171,122 @@ export class ComplexityClient {
171
171
  * Analyze complexity metrics for a file
172
172
  */
173
173
  analyzeFile(filePath: string): FileComplexity | null {
174
- const absolutePath = path.resolve(filePath);
175
- if (!fs.existsSync(absolutePath)) return null;
174
+ const parsed = this.readAndParse(filePath);
175
+ if (!parsed) return null;
176
176
 
177
177
  try {
178
- const content = fs.readFileSync(absolutePath, "utf-8");
179
- const lines = content.split("\n");
180
- const sourceFile = ts.createSourceFile(
181
- filePath,
182
- content,
183
- ts.ScriptTarget.Latest,
184
- true,
185
- );
186
-
187
- // Count lines of code (non-empty, non-comment)
188
- const { codeLines, commentLines } = this.countLines(sourceFile, lines);
189
-
190
- // Collect function metrics
191
- const functions: FunctionMetrics[] = [];
192
- this.collectFunctionMetrics(sourceFile, sourceFile, functions, 0);
193
-
194
- // Calculate file-level metrics
195
- const maxNestingDepth = this.calculateMaxNesting(sourceFile, 0);
196
- const _cyclomatic = this.calculateCyclomaticComplexity(sourceFile);
197
- const cognitive = this.calculateCognitiveComplexity(sourceFile);
198
- const halstead = this.calculateHalsteadVolume(sourceFile);
199
-
200
- // Function length stats
201
- const funcLengths = functions.map((f) => f.length);
202
- const avgFunctionLength =
203
- funcLengths.length > 0
204
- ? Math.round(
205
- funcLengths.reduce((a, b) => a + b, 0) / funcLengths.length,
206
- )
207
- : 0;
208
- const maxFunctionLength =
209
- funcLengths.length > 0 ? Math.max(...funcLengths) : 0;
210
-
211
- // Function cyclomatic stats
212
- const cyclomatics = functions.map((f) => f.cyclomatic);
213
- const avgCyclomatic =
214
- cyclomatics.length > 0
215
- ? Math.round(
216
- cyclomatics.reduce((a, b) => a + b, 0) / cyclomatics.length,
217
- )
218
- : 1;
219
- const maxCyclomatic =
220
- cyclomatics.length > 0 ? Math.max(...cyclomatics) : 1;
221
-
222
- // Maintainability Index (simplified Microsoft formula)
223
- // MI = max(0, (171 - 5.2 * ln(Halstead) - 0.23 * Cyclomatic - 16.2 * ln(LOC)) * 100 / 171)
224
- const maintainabilityIndex = this.calculateMaintainabilityIndex(
225
- halstead,
226
- avgCyclomatic,
227
- codeLines,
228
- commentLines,
229
- );
230
-
231
- // Code Entropy (Shannon entropy of code tokens)
232
- const codeEntropy = this.calculateCodeEntropy(content);
233
-
234
- // AI slop indicators
235
- const maxParamsInFunction = this.calculateMaxParams(functions);
236
- const aiCommentPatterns = this.countAICommentPatterns(sourceFile);
237
- const singleUseFunctions = this.countSingleUseFunctions(functions);
238
- const tryCatchCount = this.countTryCatch(sourceFile);
239
-
240
- return {
241
- filePath: path.relative(process.cwd(), absolutePath),
242
- maxNestingDepth,
243
- avgFunctionLength,
244
- maxFunctionLength,
245
- functionCount: functions.length,
246
- cyclomaticComplexity: avgCyclomatic,
247
- maxCyclomaticComplexity: maxCyclomatic,
248
- cognitiveComplexity: cognitive,
249
- halsteadVolume: Math.round(halstead * 10) / 10,
250
- maintainabilityIndex: Math.round(maintainabilityIndex * 10) / 10,
251
- linesOfCode: codeLines,
252
- commentLines,
253
- codeEntropy: Math.round(codeEntropy * 100) / 100,
254
- maxParamsInFunction,
255
- aiCommentPatterns,
256
- singleUseFunctions,
257
- tryCatchCount,
258
- };
178
+ return this.computeMetrics(parsed);
259
179
  } catch (err: any) {
260
180
  this.log(`Analysis error for ${filePath}: ${err.message}`);
261
181
  return null;
262
182
  }
263
183
  }
264
184
 
185
+ /**
186
+ * Read file and parse to TypeScript AST
187
+ */
188
+ private readAndParse(
189
+ filePath: string,
190
+ ): { absolutePath: string; content: string; sourceFile: ts.SourceFile } | null {
191
+ const absolutePath = path.resolve(filePath);
192
+ if (!fs.existsSync(absolutePath)) return null;
193
+
194
+ const content = fs.readFileSync(absolutePath, "utf-8");
195
+ const sourceFile = ts.createSourceFile(
196
+ filePath,
197
+ content,
198
+ ts.ScriptTarget.Latest,
199
+ true,
200
+ );
201
+
202
+ return { absolutePath, content, sourceFile };
203
+ }
204
+
205
+ /**
206
+ * Compute all metrics from parsed source
207
+ */
208
+ private computeMetrics(parsed: {
209
+ absolutePath: string;
210
+ content: string;
211
+ sourceFile: ts.SourceFile;
212
+ }): FileComplexity {
213
+ const { absolutePath, content, sourceFile } = parsed;
214
+ const lines = content.split("\n");
215
+
216
+ // Line counts and function collection
217
+ const { codeLines, commentLines } = this.countLines(sourceFile, lines);
218
+ const functions = this.collectFunctionMetrics(sourceFile);
219
+
220
+ // File-level complexity metrics
221
+ const maxNestingDepth = this.calculateMaxNesting(sourceFile, 0);
222
+ const cognitive = this.calculateCognitiveComplexity(sourceFile);
223
+ const halstead = this.calculateHalsteadVolume(sourceFile);
224
+
225
+ // Aggregate function statistics
226
+ const funcStats = this.aggregateFunctionStats(functions);
227
+
228
+ // Derived metrics
229
+ const maintainabilityIndex = this.calculateMaintainabilityIndex(
230
+ halstead,
231
+ funcStats.avgCyclomatic,
232
+ codeLines,
233
+ commentLines,
234
+ );
235
+ const codeEntropy = this.calculateCodeEntropy(content);
236
+
237
+ // AI slop indicators
238
+ const maxParamsInFunction = this.calculateMaxParams(functions);
239
+ const aiCommentPatterns = this.countAICommentPatterns(sourceFile);
240
+ const singleUseFunctions = this.countSingleUseFunctions(functions);
241
+ const tryCatchCount = this.countTryCatch(sourceFile);
242
+
243
+ return {
244
+ filePath: path.relative(process.cwd(), absolutePath),
245
+ maxNestingDepth,
246
+ avgFunctionLength: funcStats.avgLength,
247
+ maxFunctionLength: funcStats.maxLength,
248
+ functionCount: functions.length,
249
+ cyclomaticComplexity: funcStats.avgCyclomatic,
250
+ maxCyclomaticComplexity: funcStats.maxCyclomatic,
251
+ cognitiveComplexity: cognitive,
252
+ halsteadVolume: Math.round(halstead * 10) / 10,
253
+ maintainabilityIndex: Math.round(maintainabilityIndex * 10) / 10,
254
+ linesOfCode: codeLines,
255
+ commentLines,
256
+ codeEntropy: Math.round(codeEntropy * 100) / 100,
257
+ maxParamsInFunction,
258
+ aiCommentPatterns,
259
+ singleUseFunctions,
260
+ tryCatchCount,
261
+ };
262
+ }
263
+
264
+ /**
265
+ * Aggregate function metrics into summary statistics
266
+ */
267
+ private aggregateFunctionStats(functions: FunctionMetrics[]): {
268
+ avgLength: number;
269
+ maxLength: number;
270
+ avgCyclomatic: number;
271
+ maxCyclomatic: number;
272
+ } {
273
+ if (functions.length === 0) {
274
+ return { avgLength: 0, maxLength: 0, avgCyclomatic: 1, maxCyclomatic: 1 };
275
+ }
276
+
277
+ const lengths = functions.map((f) => f.length);
278
+ const cyclomatics = functions.map((f) => f.cyclomatic);
279
+
280
+ const sum = (arr: number[]) => arr.reduce((a, b) => a + b, 0);
281
+
282
+ return {
283
+ avgLength: Math.round(sum(lengths) / lengths.length),
284
+ maxLength: Math.max(...lengths),
285
+ avgCyclomatic: Math.max(1, Math.round(sum(cyclomatics) / cyclomatics.length)),
286
+ maxCyclomatic: Math.max(1, Math.max(...cyclomatics)),
287
+ };
288
+ }
289
+
265
290
  /**
266
291
  * Format metrics for display
267
292
  */
@@ -451,9 +476,9 @@ export class ComplexityClient {
451
476
  );
452
477
  }
453
478
 
454
- // Comments ratio (>30% = excessive comments, AI slop signal)
479
+ // Comments ratio (>40% = excessive comments, AI slop signal)
455
480
  const totalLines = metrics.linesOfCode + metrics.commentLines;
456
- if (totalLines > 10 && metrics.commentLines / totalLines > 0.3) {
481
+ if (totalLines > 10 && metrics.commentLines / totalLines > 0.4) {
457
482
  warnings.push(
458
483
  `Excessive comments (${Math.round((metrics.commentLines / totalLines) * 100)}%) — remove obvious comments`,
459
484
  );
@@ -474,7 +499,7 @@ export class ComplexityClient {
474
499
  }
475
500
 
476
501
  // AI slop: Too many try/catch blocks (lazy error handling)
477
- if (metrics.tryCatchCount > 5) {
502
+ if (metrics.tryCatchCount > 15) {
478
503
  warnings.push(
479
504
  `Many try/catch blocks (${metrics.tryCatchCount}) — consolidate error handling`,
480
505
  );
@@ -556,7 +581,18 @@ export class ComplexityClient {
556
581
  commentLines = commentPositions.size;
557
582
  const codeLines = lines.filter((line, i) => {
558
583
  const trimmed = line.trim();
559
- return trimmed.length > 0 && !commentPositions.has(i);
584
+ if (trimmed.length === 0) return false;
585
+
586
+ // If the line is not in commentPositions, it definitely has code
587
+ if (!commentPositions.has(i)) return true;
588
+
589
+ // If it IS in commentPositions, it might still have code (trailing comment)
590
+ // Remove the comment part and check if anything remains
591
+ const lineWithoutComments = line
592
+ .replace(/\/\/.*$/, "")
593
+ .replace(/\/\*[\s\S]*?\*\//g, "")
594
+ .trim();
595
+ return lineWithoutComments.length > 0;
560
596
  }).length;
561
597
 
562
598
  return { codeLines, commentLines };
@@ -564,7 +600,16 @@ export class ComplexityClient {
564
600
 
565
601
  // --- Private: Function Metrics Collection ---
566
602
 
567
- private collectFunctionMetrics(
603
+ /**
604
+ * Collect metrics for all functions in the source file
605
+ */
606
+ private collectFunctionMetrics(sourceFile: ts.SourceFile): FunctionMetrics[] {
607
+ const functions: FunctionMetrics[] = [];
608
+ this.visitFunctionMetrics(sourceFile, sourceFile, functions, 0);
609
+ return functions;
610
+ }
611
+
612
+ private visitFunctionMetrics(
568
613
  node: ts.Node,
569
614
  sourceFile: ts.SourceFile,
570
615
  functions: FunctionMetrics[],
@@ -603,7 +648,7 @@ export class ComplexityClient {
603
648
  ? nestingLevel + 1
604
649
  : nestingLevel;
605
650
  ts.forEachChild(node, (child) => {
606
- this.collectFunctionMetrics(child, sourceFile, functions, newNesting);
651
+ this.visitFunctionMetrics(child, sourceFile, functions, newNesting);
607
652
  });
608
653
  }
609
654
 
@@ -631,6 +676,17 @@ export class ComplexityClient {
631
676
  return this.nodeCyclomaticComplexity(node, 0);
632
677
  }
633
678
 
679
+ private isLogicalOperator(node: ts.Node): boolean {
680
+ if (node.kind === ts.SyntaxKind.BinaryExpression) {
681
+ const binary = node as ts.BinaryExpression;
682
+ return (
683
+ binary.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken ||
684
+ binary.operatorToken.kind === ts.SyntaxKind.BarBarToken
685
+ );
686
+ }
687
+ return false;
688
+ }
689
+
634
690
  private nodeCyclomaticComplexity(node: ts.Node, complexity: number): number {
635
691
  // Base increment for branching nodes
636
692
  if (CYCLOMAL_NODES.has(node.kind)) {
@@ -638,14 +694,8 @@ export class ComplexityClient {
638
694
  }
639
695
 
640
696
  // Binary && and || add complexity
641
- if (node.kind === ts.SyntaxKind.BinaryExpression) {
642
- const binary = node as ts.BinaryExpression;
643
- if (
644
- binary.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken ||
645
- binary.operatorToken.kind === ts.SyntaxKind.BarBarToken
646
- ) {
647
- complexity++;
648
- }
697
+ if (this.isLogicalOperator(node)) {
698
+ complexity++;
649
699
  }
650
700
 
651
701
  ts.forEachChild(node, (child) => {
@@ -681,14 +731,8 @@ export class ComplexityClient {
681
731
  }
682
732
 
683
733
  // Binary && and || contribute to complexity
684
- if (node.kind === ts.SyntaxKind.BinaryExpression) {
685
- const binary = node as ts.BinaryExpression;
686
- if (
687
- binary.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken ||
688
- binary.operatorToken.kind === ts.SyntaxKind.BarBarToken
689
- ) {
690
- complexity += 1;
691
- }
734
+ if (this.isLogicalOperator(node)) {
735
+ complexity += 1;
692
736
  }
693
737
 
694
738
  // Calculate nesting for children