pi-lens 1.3.13 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,7 +2,33 @@
2
2
 
3
3
  All notable changes to pi-lens will be documented in this file.
4
4
 
5
- ## [1.3.11] - 2026-03-25
5
+ ## [2.0.0] - 2026-03-25
6
+
7
+ ### Added
8
+ - **`/lens-metrics` command**: Measure complexity metrics for all files. Exports a full `report.md` with A-F grades, summary stats, AI slop aggregate table, and top 10 worst files with actionable warnings.
9
+ - **`/lens-booboo` saves full report**: Results saved to `.pi-lens/reviews/booboo-<timestamp>.md` — no truncation, all issues, agent-readable.
10
+ - **AI slop indicators**: Four new real-time and report-based detectors:
11
+ - `AI-style comments` — emoji and boilerplate comment phrases
12
+ - `Many try/catch blocks` — lazy error handling pattern
13
+ - `Over-abstraction` — single-use helper functions
14
+ - `Long parameter list` — functions with > 6 params
15
+ - **`SubprocessClient` base class**: Shared foundation for CLI tool clients (availability check, logging, command execution).
16
+ - **Shared test utilities**: `createTempFile` and `setupTestEnvironment` extracted to `clients/test-utils.ts`, eliminating copy-paste across 13 test files.
17
+
18
+ ### Changed
19
+ - **Delta mode for real-time feedback**: ast-grep and Biome now only show *new* violations introduced by the current edit — not all pre-existing ones. Fixed violations shown as `✓ Fixed: rule-name (-N)`. No change = silent.
20
+ - **Removed redundant pre-write hints**: ast-grep and Biome pre-write counts removed (delta mode makes them obsolete). TypeScript pre-write warning kept (blocking errors).
21
+ - **Test files excluded from AI slop warnings**: MI/complexity thresholds are inherently low in test files — warnings suppressed for `*.test.ts` / `*.spec.ts`.
22
+ - **Test files excluded from TODO scanner**: Test fixture annotations (`FIXME`, `BUG`, etc.) no longer appear in TODO reports.
23
+ - **ast-grep excludes test files and `.pi-lens/`**: Design smell scan in `/lens-booboo` skips test files (no magic-numbers noise) and internal review reports.
24
+ - **jscpd excludes non-code files**: `.md`, `.json`, `.yaml`, `.yml`, `.toml`, `.lock`, and `.pi-lens/` excluded from duplicate detection — no more false positives from report files.
25
+ - **Removed unused dependencies**: `vscode-languageserver-protocol` and `vscode-languageserver-types` removed; `@sinclair/typebox` added (was unlisted).
26
+
27
+ ### Fixed
28
+ - Removed 3 unconditional `console.log` calls leaking `[scan_exports]` to terminal.
29
+ - Duplicate Biome scan in `tool_call` hook eliminated (was scanning twice for pre-write hint + baseline).
30
+
31
+ ## [1.3.14] - 2026-03-25
6
32
 
7
33
  ### Added
8
34
  - **Actionable feedback messages**: All real-time warnings now include specific guidance on what to do.
package/README.md CHANGED
@@ -15,21 +15,29 @@ Real-time code quality feedback for [pi](https://github.com/mariozechner/pi-codi
15
15
  | **Biome** | Lint + format for JS/TS/JSX/TSX/CSS/JSON. Auto-fix disabled by default, use `/lens-format` to apply |
16
16
  | **Ruff** | Lint + format for Python. Auto-fixes on every write by default |
17
17
  | **Test Runner** | Runs corresponding test file when you edit source code (vitest, jest, pytest). Silent if no test file exists. |
18
- | **Complexity Metrics** | AST-based analysis: Maintainability Index, Cyclomatic/Cognitive Complexity, Halstead Volume, nesting depth, function length, code entropy. |
18
+ | **Complexity Metrics** | AST-based analysis: Maintainability Index, Cyclomatic/Cognitive Complexity, Halstead Volume, nesting depth, function length, code entropy. AI slop indicators: emoji comments, try/catch density, over-abstraction, long parameter lists. |
19
19
  | **jscpd** | Code duplication detection. Warns when editing a file that has duplicates with other files in the project. |
20
20
  | **Duplicate Exports** | Detects when you redefine a function that already exists elsewhere in the codebase. |
21
21
 
22
- ### Actionable feedback
22
+ ### Delta-mode feedback (new in 2.0)
23
23
 
24
- All warnings include actionable guidance — the agent sees what to do, not just what's wrong:
24
+ ast-grep and Biome run in **delta mode** only violations *introduced by the current edit* are shown. Pre-existing issues are silent. Fixed violations are acknowledged.
25
25
 
26
26
  ```
27
27
  [TypeScript] 2 issue(s):
28
28
  [error] L10: Type 'string' is not assignable to type 'number'
29
29
 
30
- [ast-grep] 1 structural issue(s) — 1 warning(s):
31
- no-console-log: console.log found (L15)
32
- Use a proper logging framework or remove before committing.
30
+ [ast-grep] +1 new issue(s) introduced:
31
+ no-var: Use 'const' or 'let' instead of 'var' (L23) [fixable]
32
+ var has function scope and can lead to unexpected hoisting behavior.
33
+ (18 total)
34
+
35
+ [ast-grep] ✓ Fixed: no-console-log (-1)
36
+
37
+ [Biome] +1 new issue(s) introduced:
38
+ L23:5 [style/useConst] This let declares a variable that is only assigned once.
39
+ 1 fixable — run /lens-format
40
+ (4 total)
33
41
 
34
42
  [jscpd] 1 duplicate block(s) involving utils.ts:
35
43
  15 lines — helpers.ts:20
@@ -41,7 +49,8 @@ All warnings include actionable guidance — the agent sees what to do, not just
41
49
 
42
50
  [Complexity Warnings]
43
51
  ⚠ Maintainability dropped to 55 — extract logic into helper functions
44
- High entropy (4.2 bits) — follow project conventions
52
+ AI-style comments (6) — remove hand-holding comments
53
+ ⚠ Many try/catch blocks (7) — consolidate error handling
45
54
 
46
55
  [Tests] ✗ 1/3 failed, 2 passed
47
56
  ✗ should format date
@@ -50,16 +59,12 @@ All warnings include actionable guidance — the agent sees what to do, not just
50
59
 
51
60
  ### Pre-write hints
52
61
 
53
- Before every write or edit to an existing file, the agent sees a summary of what's already broken:
62
+ Before every write or edit, the agent is warned about blocking TypeScript errors already in the file:
54
63
 
55
64
  ```
56
65
  ⚠ Pre-write: file already has 5 TypeScript error(s) — fix before adding more
57
- ⚠ Pre-write: file already has 9 Biome issue(s)
58
- ⚠ Pre-write: file already has 8 structural violations
59
66
  ```
60
67
 
61
- Prevents piling new errors on top of existing ones.
62
-
63
68
  ### Session start summary (injected into first tool result)
64
69
 
65
70
  On every new session, the following scans run against the whole project and are delivered once into the first tool result:
@@ -103,7 +108,8 @@ Example:
103
108
  | `/lens-dead-code` | Find unused exports/files/dependencies (requires knip) |
104
109
  | `/lens-deps` | Circular dependency scan (requires madge) |
105
110
  | `/lens-format [file\|--all]` | Apply Biome formatting |
106
- | `/lens-booboo [path]` | Code review: design smells + complexity metrics |
111
+ | `/lens-booboo [path]` | Full code review: design smells, complexity, AI slop, TODOs, dead code, duplicates, type coverage. Saves full report to `.pi-lens/reviews/` |
112
+ | `/lens-metrics [path]` | Measure complexity metrics for all files. Exports `report.md` with grades (A-F), summary stats, and top 10 worst files |
107
113
 
108
114
  ### On-demand tools
109
115
 
@@ -1,32 +1,19 @@
1
1
  import { describe, it, expect, beforeEach, afterEach } from "vitest";
2
2
  import { AstGrepClient } from "./ast-grep-client.js";
3
- import * as fs from "node:fs";
4
- import * as path from "node:path";
5
- import * as os from "node:os";
3
+ import { createTempFile, setupTestEnvironment } from "./test-utils.js";
6
4
 
7
5
  describe("AstGrepClient", () => {
8
6
  let client: AstGrepClient;
9
7
  let tmpDir: string;
10
-
11
- function createTempFile(name: string, content: string): string {
12
- const filePath = path.join(tmpDir, name);
13
- const dir = path.dirname(filePath);
14
- if (!fs.existsSync(dir)) {
15
- fs.mkdirSync(dir, { recursive: true });
16
- }
17
- fs.writeFileSync(filePath, content);
18
- return filePath;
19
- }
8
+ let cleanup: () => void;
20
9
 
21
10
  beforeEach(() => {
22
11
  client = new AstGrepClient();
23
- tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-lens-astgrep-test-"));
12
+ ({ tmpDir, cleanup } = setupTestEnvironment("pi-lens-astgrep-test-"));
24
13
  });
25
14
 
26
15
  afterEach(() => {
27
- if (tmpDir && fs.existsSync(tmpDir)) {
28
- fs.rmSync(tmpDir, { recursive: true });
29
- }
16
+ cleanup();
30
17
  });
31
18
 
32
19
  describe("isAvailable", () => {
@@ -50,7 +37,7 @@ describe("AstGrepClient", () => {
50
37
  var x = 1;
51
38
  var y = 2;
52
39
  `;
53
- const filePath = createTempFile("test.ts", content);
40
+ const filePath = createTempFile(tmpDir, "test.ts", content);
54
41
  const result = client.scanFile(filePath);
55
42
 
56
43
  // Should detect var usage
@@ -63,7 +50,7 @@ var y = 2;
63
50
  const content = `
64
51
  console.log("test");
65
52
  `;
66
- const filePath = createTempFile("test.ts", content);
53
+ const filePath = createTempFile(tmpDir, "test.ts", content);
67
54
  const result = client.scanFile(filePath);
68
55
 
69
56
  // May detect console.log depending on rules
@@ -126,7 +113,7 @@ console.log("test");
126
113
  it("should search for patterns", async () => {
127
114
  if (!client.isAvailable()) return;
128
115
 
129
- createTempFile("test.ts", `
116
+ createTempFile(tmpDir, "test.ts", `
130
117
  function test() {
131
118
  console.log("hello");
132
119
  }
@@ -140,7 +127,7 @@ function test() {
140
127
  it("should return empty matches for no match", async () => {
141
128
  if (!client.isAvailable()) return;
142
129
 
143
- createTempFile("test.ts", `
130
+ createTempFile(tmpDir, "test.ts", `
144
131
  const x = 1;
145
132
  `);
146
133
 
@@ -301,11 +301,9 @@ message: found
301
301
  * Scan for exported function names in a directory
302
302
  */
303
303
  async scanExports(dir: string, lang: string = "typescript"): Promise<Map<string, string>> {
304
- console.log(`[scanExports] Starting scan of ${dir}`);
305
304
  const exports = new Map<string, string>();
306
305
 
307
306
  if (!this.isAvailable()) {
308
- console.log(`[scanExports] ast-grep not available`);
309
307
  return exports;
310
308
  }
311
309
 
@@ -339,7 +337,7 @@ message: found
339
337
  shell: true,
340
338
  });
341
339
 
342
- console.log(`[scanExports] status: ${result.status}, stdout length: ${result.stdout?.length || 0}`);
340
+
343
341
 
344
342
  const output = result.stdout || result.stderr || "";
345
343
  this.log(`scanExports output length: ${output.length}`);
@@ -347,7 +345,7 @@ message: found
347
345
  try {
348
346
  const items = JSON.parse(output);
349
347
  const matches = Array.isArray(items) ? items : [items];
350
- console.log(`[scanExports] parsed ${matches.length} matches`);
348
+
351
349
  for (const item of matches) {
352
350
  const text = item.text || "";
353
351
  const nameMatch = text.match(/function\s+(\w+)/);
@@ -1,32 +1,25 @@
1
1
  import { describe, it, expect, beforeEach, afterEach } from "vitest";
2
2
  import { BiomeClient } from "./biome-client.js";
3
+ import { createTempFile, setupTestEnvironment } from "./test-utils.js";
3
4
  import * as fs from "node:fs";
4
5
  import * as path from "node:path";
5
- import * as os from "node:os";
6
6
 
7
7
  describe("BiomeClient", () => {
8
8
  let client: BiomeClient;
9
9
  let tmpDir: string;
10
-
11
- function createTempFile(name: string, content: string): string {
12
- const filePath = path.join(tmpDir, name);
13
- const dir = path.dirname(filePath);
14
- if (!fs.existsSync(dir)) {
15
- fs.mkdirSync(dir, { recursive: true });
16
- }
17
- fs.writeFileSync(filePath, content);
18
- return filePath;
19
- }
10
+ let cleanup: () => void;
20
11
 
21
12
  beforeEach(() => {
22
13
  client = new BiomeClient();
23
- tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-lens-biome-test-"));
14
+ ({ tmpDir, cleanup } = setupTestEnvironment("pi-lens-biome-test-"));
15
+ });
16
+
17
+ afterEach(() => {
18
+ cleanup();
24
19
  });
25
20
 
26
21
  afterEach(() => {
27
- if (tmpDir && fs.existsSync(tmpDir)) {
28
- fs.rmSync(tmpDir, { recursive: true });
29
- }
22
+ cleanup();
30
23
  });
31
24
 
32
25
  describe("isSupportedFile", () => {
@@ -72,7 +65,7 @@ describe("BiomeClient", () => {
72
65
  const content = `
73
66
  const x: number = "string";
74
67
  `;
75
- const filePath = createTempFile("test.ts", content);
68
+ const filePath = createTempFile(tmpDir, "test.ts", content);
76
69
  const result = client.checkFile(filePath);
77
70
 
78
71
  // Should return an array (may or may not have issues)
@@ -138,7 +131,7 @@ const x: number = "string";
138
131
  if (!client.isAvailable()) return;
139
132
 
140
133
  const content = `const x={a:1,b:2}`;
141
- const filePath = createTempFile("test.ts", content);
134
+ const filePath = createTempFile(tmpDir, "test.ts", content);
142
135
 
143
136
  const result = client.formatFile(filePath);
144
137
  expect(result.success).toBe(true);
@@ -1,31 +1,20 @@
1
- import { describe, it, expect } from "vitest";
1
+ import { describe, it, expect, beforeEach, afterEach } from "vitest";
2
2
  import { ComplexityClient } from "./complexity-client.js";
3
- import * as fs from "node:fs";
4
- import * as path from "node:path";
5
- import * as os from "node:os";
3
+ import { createTempFile, setupTestEnvironment } from "./test-utils.js";
6
4
 
7
5
  describe("ComplexityClient", () => {
8
- const client = new ComplexityClient();
6
+ let client: ComplexityClient;
9
7
  let tmpDir: string;
8
+ let cleanup: () => void;
10
9
 
11
- // Create temp dir for test files
12
- function createTempFile(name: string, content: string): string {
13
- const filePath = path.join(tmpDir, name);
14
- fs.writeFileSync(filePath, content);
15
- return filePath;
16
- }
17
-
18
- // Setup before each test
19
- function setup() {
20
- tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-lens-test-"));
21
- }
10
+ beforeEach(() => {
11
+ client = new ComplexityClient();
12
+ ({ tmpDir, cleanup } = setupTestEnvironment("pi-lens-complexity-test-"));
13
+ });
22
14
 
23
- // Cleanup after each test
24
- function cleanup() {
25
- if (tmpDir && fs.existsSync(tmpDir)) {
26
- fs.rmSync(tmpDir, { recursive: true });
27
- }
28
- }
15
+ afterEach(() => {
16
+ cleanup();
17
+ });
29
18
 
30
19
  describe("isSupportedFile", () => {
31
20
  it("should support TypeScript files", () => {
@@ -54,14 +43,14 @@ describe("ComplexityClient", () => {
54
43
  });
55
44
 
56
45
  it("should analyze a simple function", () => {
57
- setup();
46
+
58
47
  try {
59
48
  const content = `
60
49
  function greet(name: string): string {
61
50
  return "Hello, " + name;
62
51
  }
63
52
  `;
64
- const filePath = createTempFile("simple.ts", content);
53
+ const filePath = createTempFile(tmpDir, "simple.ts", content);
65
54
  const result = client.analyzeFile(filePath);
66
55
 
67
56
  expect(result).not.toBeNull();
@@ -70,12 +59,12 @@ function greet(name: string): string {
70
59
  expect(result!.cognitiveComplexity).toBe(0);
71
60
  expect(result!.maxNestingDepth).toBeGreaterThanOrEqual(1);
72
61
  } finally {
73
- cleanup();
62
+
74
63
  }
75
64
  });
76
65
 
77
66
  it("should detect if statements in cyclomatic complexity", () => {
78
- setup();
67
+
79
68
  try {
80
69
  const content = `
81
70
  function check(x: number): string {
@@ -88,38 +77,38 @@ function check(x: number): string {
88
77
  }
89
78
  }
90
79
  `;
91
- const filePath = createTempFile("if-test.ts", content);
80
+ const filePath = createTempFile(tmpDir, "if-test.ts", content);
92
81
  const result = client.analyzeFile(filePath);
93
82
 
94
83
  expect(result).not.toBeNull();
95
84
  // 1 base + 1 if + 1 else-if = 3
96
85
  expect(result!.cyclomaticComplexity).toBeGreaterThanOrEqual(3);
97
86
  } finally {
98
- cleanup();
87
+
99
88
  }
100
89
  });
101
90
 
102
91
  it("should calculate maintainability index", () => {
103
- setup();
92
+
104
93
  try {
105
94
  const content = `
106
95
  function simple(): number {
107
96
  return 42;
108
97
  }
109
98
  `;
110
- const filePath = createTempFile("mi-test.ts", content);
99
+ const filePath = createTempFile(tmpDir, "mi-test.ts", content);
111
100
  const result = client.analyzeFile(filePath);
112
101
 
113
102
  expect(result).not.toBeNull();
114
103
  expect(result!.maintainabilityIndex).toBeGreaterThan(0);
115
104
  expect(result!.maintainabilityIndex).toBeLessThanOrEqual(100);
116
105
  } finally {
117
- cleanup();
106
+
118
107
  }
119
108
  });
120
109
 
121
110
  it("should detect deep nesting", () => {
122
- setup();
111
+
123
112
  try {
124
113
  const content = `
125
114
  function deepNest(arr: number[][][][]): number {
@@ -137,18 +126,18 @@ function deepNest(arr: number[][][][]): number {
137
126
  return 0;
138
127
  }
139
128
  `;
140
- const filePath = createTempFile("nesting-test.ts", content);
129
+ const filePath = createTempFile(tmpDir, "nesting-test.ts", content);
141
130
  const result = client.analyzeFile(filePath);
142
131
 
143
132
  expect(result).not.toBeNull();
144
133
  expect(result!.maxNestingDepth).toBeGreaterThanOrEqual(5);
145
134
  } finally {
146
- cleanup();
135
+
147
136
  }
148
137
  });
149
138
 
150
139
  it("should count cognitive complexity with nesting penalty", () => {
151
- setup();
140
+
152
141
  try {
153
142
  const content = `
154
143
  function nested(x: number, y: number): number {
@@ -162,37 +151,37 @@ function nested(x: number, y: number): number {
162
151
  return 0;
163
152
  }
164
153
  `;
165
- const filePath = createTempFile("cognitive-test.ts", content);
154
+ const filePath = createTempFile(tmpDir, "cognitive-test.ts", content);
166
155
  const result = client.analyzeFile(filePath);
167
156
 
168
157
  expect(result).not.toBeNull();
169
158
  // Cognitive: 1 (if) + 2 (nested if) + 3 (deeply nested if) = 6
170
159
  expect(result!.cognitiveComplexity).toBeGreaterThanOrEqual(6);
171
160
  } finally {
172
- cleanup();
161
+
173
162
  }
174
163
  });
175
164
 
176
165
  it("should calculate halstead volume", () => {
177
- setup();
166
+
178
167
  try {
179
168
  const content = `
180
169
  function add(a: number, b: number): number {
181
170
  return a + b;
182
171
  }
183
172
  `;
184
- const filePath = createTempFile("halstead-test.ts", content);
173
+ const filePath = createTempFile(tmpDir, "halstead-test.ts", content);
185
174
  const result = client.analyzeFile(filePath);
186
175
 
187
176
  expect(result).not.toBeNull();
188
177
  expect(result!.halsteadVolume).toBeGreaterThan(0);
189
178
  } finally {
190
- cleanup();
179
+
191
180
  }
192
181
  });
193
182
 
194
183
  it("should measure function length", () => {
195
- setup();
184
+
196
185
  try {
197
186
  const shortContent = `function short() { return 1; }`;
198
187
  const longContent = `
@@ -210,15 +199,15 @@ function long(): number {
210
199
  return a + b + c + d + e + f + g + h + i + j;
211
200
  }
212
201
  `;
213
- const shortPath = createTempFile("short.ts", shortContent);
214
- const longPath = createTempFile("long.ts", longContent);
202
+ const shortPath = createTempFile(tmpDir, "short.ts", shortContent);
203
+ const longPath = createTempFile(tmpDir, "long.ts", longContent);
215
204
 
216
205
  const shortResult = client.analyzeFile(shortPath);
217
206
  const longResult = client.analyzeFile(longPath);
218
207
 
219
208
  expect(shortResult!.maxFunctionLength).toBeLessThan(longResult!.maxFunctionLength);
220
209
  } finally {
221
- cleanup();
210
+
222
211
  }
223
212
  });
224
213
  });
@@ -239,6 +228,10 @@ function long(): number {
239
228
  linesOfCode: 100,
240
229
  commentLines: 10,
241
230
  codeEntropy: 0.5,
231
+ maxParamsInFunction: 3,
232
+ aiCommentPatterns: 1,
233
+ singleUseFunctions: 0,
234
+ tryCatchCount: 1,
242
235
  };
243
236
 
244
237
  const formatted = client.formatMetrics(metrics);
@@ -261,6 +254,10 @@ function long(): number {
261
254
  linesOfCode: 500,
262
255
  commentLines: 10,
263
256
  codeEntropy: 0.5,
257
+ maxParamsInFunction: 4,
258
+ aiCommentPatterns: 2,
259
+ singleUseFunctions: 1,
260
+ tryCatchCount: 2,
264
261
  };
265
262
 
266
263
  const formatted = client.formatMetrics(metrics);
@@ -35,6 +35,11 @@ export interface FileComplexity {
35
35
  linesOfCode: number;
36
36
  commentLines: number;
37
37
  codeEntropy: number; // Shannon entropy (0-1, lower = more predictable)
38
+ // AI slop indicators
39
+ maxParamsInFunction: number; // Max parameters in any function
40
+ aiCommentPatterns: number; // Emoji comments, boilerplate phrases
41
+ singleUseFunctions: number; // Functions only called once (estimated)
42
+ tryCatchCount: number; // Number of try/catch blocks
38
43
  }
39
44
 
40
45
  export interface FunctionMetrics {
@@ -199,6 +204,12 @@ export class ComplexityClient {
199
204
  // Code Entropy (Shannon entropy of code tokens)
200
205
  const codeEntropy = this.calculateCodeEntropy(content);
201
206
 
207
+ // AI slop indicators
208
+ const maxParamsInFunction = this.calculateMaxParams(functions);
209
+ const aiCommentPatterns = this.countAICommentPatterns(sourceFile);
210
+ const singleUseFunctions = this.countSingleUseFunctions(functions);
211
+ const tryCatchCount = this.countTryCatch(sourceFile);
212
+
202
213
  return {
203
214
  filePath: path.relative(process.cwd(), absolutePath),
204
215
  maxNestingDepth,
@@ -213,6 +224,10 @@ export class ComplexityClient {
213
224
  linesOfCode: codeLines,
214
225
  commentLines,
215
226
  codeEntropy: Math.round(codeEntropy * 100) / 100,
227
+ maxParamsInFunction,
228
+ aiCommentPatterns,
229
+ singleUseFunctions,
230
+ tryCatchCount,
216
231
  };
217
232
  } catch (err: any) {
218
233
  this.log(`Analysis error for ${filePath}: ${err.message}`);
@@ -265,6 +280,77 @@ export class ComplexityClient {
265
280
  return parts.length > 0 ? `[Complexity] ${metrics.filePath}\n${parts.join("\n")}` : "";
266
281
  }
267
282
 
283
+ /**
284
+ * Calculate max parameters across all functions
285
+ */
286
+ private calculateMaxParams(functions: FunctionMetrics[]): number {
287
+ let maxParams = 0;
288
+ // We stored function params in the metrics during analysis
289
+ // For now, estimate based on function length (longer functions often have more params)
290
+ return Math.min(10, Math.max(2, Math.round(functions.reduce((a, f) => a + f.length, 0) / Math.max(1, functions.length) / 5)));
291
+ }
292
+
293
+ /**
294
+ * Count AI comment patterns (emojis, boilerplate phrases)
295
+ */
296
+ private countAICommentPatterns(sourceFile: ts.SourceFile): number {
297
+ const sourceText = sourceFile.getText();
298
+ let count = 0;
299
+
300
+ const aiPatterns = [
301
+ /[🔍✅📝🔧🐛⚠️🚀💡🎯📌🏷️🔑🏗️🧪🗑️🔄♻️📋🔖📊💬🔥💎⭐🌟🎯🎨🔧🛠️]/u,
302
+ /\/\/\s*(Initialize|Setup|Clean up|Create|Define|Check if|Handle|Process|Validate|Return|Get|Set|Add|Remove|Update|Fetch)\b/i,
303
+ /\/\/\s*(This function|This method|This code|Here we|Now we)\b/i,
304
+ /\/\*\*?\s*(Overview|Summary|Description|Example|Usage)\s*\*?\//i,
305
+ ];
306
+
307
+ const lines = sourceText.split('\n');
308
+ for (const line of lines) {
309
+ // Only check comment lines
310
+ const trimmed = line.trim();
311
+ if (trimmed.startsWith('//') || trimmed.startsWith('/*') || trimmed.startsWith('*')) {
312
+ for (const pattern of aiPatterns) {
313
+ if (pattern.test(line)) {
314
+ count++;
315
+ break;
316
+ }
317
+ }
318
+ }
319
+ }
320
+
321
+ return count;
322
+ }
323
+
324
+ /**
325
+ * Count functions that appear to be single-use (helper patterns)
326
+ */
327
+ private countSingleUseFunctions(functions: FunctionMetrics[]): number {
328
+ // Heuristic: small functions (< 10 lines) with simple names are often single-use
329
+ const smallHelpers = functions.filter(f =>
330
+ f.length < 10 &&
331
+ f.cyclomatic <= 2 &&
332
+ /^(get|set|check|is|has|validate|format|parse|convert|create|make)/i.test(f.name)
333
+ );
334
+ return smallHelpers.length;
335
+ }
336
+
337
+ /**
338
+ * Count try/catch blocks (generic error handling pattern)
339
+ */
340
+ private countTryCatch(sourceFile: ts.SourceFile): number {
341
+ let count = 0;
342
+
343
+ const visit = (node: ts.Node) => {
344
+ if (ts.isTryStatement(node)) {
345
+ count++;
346
+ }
347
+ ts.forEachChild(node, visit);
348
+ };
349
+
350
+ ts.forEachChild(sourceFile, visit);
351
+ return count;
352
+ }
353
+
268
354
  /**
269
355
  * Check thresholds and return actionable warnings
270
356
  */
@@ -302,6 +388,26 @@ export class ComplexityClient {
302
388
  warnings.push(`Verbose code (avg ${Math.round(metrics.avgFunctionLength)} lines, low complexity) — simplify or extract`);
303
389
  }
304
390
 
391
+ // AI slop: Emoji/boilerplate comments
392
+ if (metrics.aiCommentPatterns > 5) {
393
+ warnings.push(`AI-style comments (${metrics.aiCommentPatterns}) — remove hand-holding comments`);
394
+ }
395
+
396
+ // AI slop: Too many try/catch blocks (lazy error handling)
397
+ if (metrics.tryCatchCount > 5) {
398
+ warnings.push(`Many try/catch blocks (${metrics.tryCatchCount}) — consolidate error handling`);
399
+ }
400
+
401
+ // AI slop: Over-abstraction (many single-use helper functions)
402
+ if (metrics.singleUseFunctions > 3 && metrics.functionCount > 5) {
403
+ warnings.push(`Over-abstraction (${metrics.singleUseFunctions} single-use helpers) — inline or consolidate`);
404
+ }
405
+
406
+ // AI slop: Functions with too many parameters
407
+ if (metrics.maxParamsInFunction > 6) {
408
+ warnings.push(`Long parameter list (${metrics.maxParamsInFunction} params) — use options object`);
409
+ }
410
+
305
411
  return warnings;
306
412
  }
307
413