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.
- package/CHANGELOG.md +16 -0
- package/README.md +1 -1
- package/clients/architect-client.js +50 -20
- package/clients/architect-client.ts +61 -22
- package/clients/ast-grep-client.js +32 -127
- package/clients/ast-grep-client.test.js +2 -14
- package/clients/ast-grep-client.test.ts +2 -16
- package/clients/ast-grep-client.ts +34 -150
- package/clients/auto-loop.js +117 -0
- package/clients/auto-loop.ts +171 -0
- package/clients/biome-client.js +25 -22
- package/clients/biome-client.ts +28 -25
- package/clients/complexity-client.js +111 -74
- package/clients/complexity-client.ts +149 -105
- package/clients/dependency-checker.js +16 -30
- package/clients/dependency-checker.ts +15 -35
- package/clients/fix-scanners.js +195 -0
- package/clients/fix-scanners.ts +297 -0
- package/clients/interviewer-templates.js +75 -0
- package/clients/interviewer-templates.ts +90 -0
- package/clients/interviewer.js +73 -101
- package/clients/interviewer.ts +195 -140
- package/clients/knip-client.js +12 -4
- package/clients/knip-client.ts +21 -16
- package/clients/metrics-history.js +215 -0
- package/clients/metrics-history.ts +300 -0
- package/clients/scan-architectural-debt.js +62 -72
- package/clients/scan-architectural-debt.ts +79 -64
- package/clients/scan-utils.js +98 -0
- package/clients/scan-utils.ts +112 -0
- package/clients/sg-runner.js +138 -0
- package/clients/sg-runner.ts +168 -0
- package/clients/subprocess-client.js +0 -37
- package/clients/subprocess-client.ts +0 -60
- package/clients/ts-service.ts +1 -7
- package/clients/type-safety-client.js +3 -8
- package/clients/type-safety-client.ts +10 -14
- package/clients/typescript-client.js +55 -56
- package/clients/typescript-client.ts +94 -52
- package/default-architect.yaml +87 -0
- package/index.ts +143 -1165
- package/package.json +2 -1
- package/rules/ast-grep-rules/rules/large-class.yml +6 -2
- package/rules/ast-grep-rules/rules/long-method.yml +1 -1
- package/rules/ast-grep-rules/rules/no-single-char-var.yml +2 -2
- package/rules/ast-grep-rules/rules/switch-without-default.yml +0 -12
package/index.ts
CHANGED
|
@@ -1,54 +1,39 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* pi-lens - Real-time code feedback for pi
|
|
3
|
-
*
|
|
4
|
-
* Provides real-time diagnostics on every write/edit:
|
|
5
|
-
* - TypeScript/JavaScript: Biome (lint+format) + TypeScript LSP (type checking)
|
|
6
|
-
* - Python: Ruff (lint+format)
|
|
7
|
-
* - All languages: ast-grep (63 structural rules)
|
|
8
|
-
* - JavaScript/TypeScript: Dependency checker (circular deps)
|
|
9
|
-
*
|
|
10
|
-
* Proactive hints before write/edit:
|
|
11
|
-
* - Warns when target file already has existing violations
|
|
12
|
-
*
|
|
13
|
-
* Auto-fix on write (enable with --autofix-ruff flag, Biome auto-fix disabled by default):
|
|
14
|
-
* - Biome: feedback only by default, use /lens-format to apply fixes
|
|
15
|
-
* - Ruff: applies --fix + format (lint + format fixes)
|
|
16
|
-
*
|
|
17
|
-
* On-demand commands:
|
|
18
|
-
* - /lens-format - Apply Biome formatting
|
|
19
|
-
*
|
|
20
|
-
* External dependencies:
|
|
21
|
-
* - npm: @biomejs/biome, @ast-grep/cli, knip, madge
|
|
22
|
-
* - pip: ruff
|
|
23
|
-
*/
|
|
24
|
-
|
|
25
1
|
import * as nodeFs from "node:fs";
|
|
26
2
|
import * as os from "node:os";
|
|
27
3
|
import * as path from "node:path";
|
|
28
4
|
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
29
5
|
import { isToolCallEventType } from "@mariozechner/pi-coding-agent";
|
|
30
6
|
import { Type } from "@sinclair/typebox";
|
|
7
|
+
import { ArchitectClient } from "./clients/architect-client.js";
|
|
31
8
|
import { AstGrepClient } from "./clients/ast-grep-client.js";
|
|
32
9
|
import { BiomeClient } from "./clients/biome-client.js";
|
|
33
10
|
import { ComplexityClient } from "./clients/complexity-client.js";
|
|
34
|
-
import {
|
|
35
|
-
TypeSafetyClient,
|
|
36
|
-
getTypeSafetyClient,
|
|
37
|
-
} from "./clients/type-safety-client.js";
|
|
38
|
-
import { ArchitectClient } from "./clients/architect-client.js";
|
|
39
11
|
import { DependencyChecker } from "./clients/dependency-checker.js";
|
|
40
12
|
import { GoClient } from "./clients/go-client.js";
|
|
13
|
+
import { buildInterviewer } from "./clients/interviewer.js";
|
|
41
14
|
import { JscpdClient } from "./clients/jscpd-client.js";
|
|
42
15
|
import { KnipClient } from "./clients/knip-client.js";
|
|
43
16
|
import { MetricsClient } from "./clients/metrics-client.js";
|
|
17
|
+
import { captureSnapshots, getTrendSummary, formatTrendCell } from "./clients/metrics-history.js";
|
|
44
18
|
import { RuffClient } from "./clients/ruff-client.js";
|
|
45
19
|
import { RustClient } from "./clients/rust-client.js";
|
|
20
|
+
import { getSourceFiles } from "./clients/scan-utils.js";
|
|
46
21
|
import { TestRunnerClient } from "./clients/test-runner-client.js";
|
|
47
22
|
import { TodoScanner } from "./clients/todo-scanner.js";
|
|
48
23
|
import { TypeCoverageClient } from "./clients/type-coverage-client.js";
|
|
24
|
+
import { TypeSafetyClient } from "./clients/type-safety-client.js";
|
|
49
25
|
import { TypeScriptClient } from "./clients/typescript-client.js";
|
|
50
|
-
|
|
51
|
-
import {
|
|
26
|
+
|
|
27
|
+
import { handleBooboo } from "./commands/booboo.js";
|
|
28
|
+
import { handleFix } from "./commands/fix.js";
|
|
29
|
+
import { handleRefactor, initRefactorLoop } from "./commands/refactor.js";
|
|
30
|
+
|
|
31
|
+
const _getExtensionDir = () => {
|
|
32
|
+
if (typeof __dirname !== "undefined") {
|
|
33
|
+
return __dirname;
|
|
34
|
+
}
|
|
35
|
+
return ".";
|
|
36
|
+
};
|
|
52
37
|
|
|
53
38
|
const DEBUG_LOG = path.join(os.homedir(), "pi-lens-debug.log");
|
|
54
39
|
function dbg(msg: string) {
|
|
@@ -89,6 +74,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
89
74
|
const goClient = new GoClient();
|
|
90
75
|
const rustClient = new RustClient();
|
|
91
76
|
|
|
77
|
+
// --- Initialize auto-loops (must be early for event handlers) ---
|
|
78
|
+
initRefactorLoop(pi);
|
|
79
|
+
|
|
92
80
|
// --- Flags ---
|
|
93
81
|
|
|
94
82
|
pi.registerFlag("lens-verbose", {
|
|
@@ -163,503 +151,22 @@ export default function (pi: ExtensionAPI) {
|
|
|
163
151
|
pi.registerCommand("lens-booboo", {
|
|
164
152
|
description:
|
|
165
153
|
"Full codebase review: design smells, complexity, AI slop detection, TODOs, dead code, duplicates, type coverage. Results saved to .pi-lens/reviews/. Usage: /lens-booboo [path]",
|
|
166
|
-
handler:
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
"rules",
|
|
183
|
-
"ast-grep-rules",
|
|
184
|
-
".sgconfig.yml",
|
|
185
|
-
);
|
|
186
|
-
|
|
187
|
-
try {
|
|
188
|
-
const result = require("node:child_process").spawnSync(
|
|
189
|
-
"npx",
|
|
190
|
-
[
|
|
191
|
-
"sg",
|
|
192
|
-
"scan",
|
|
193
|
-
"--config",
|
|
194
|
-
configPath,
|
|
195
|
-
"--json",
|
|
196
|
-
"--globs",
|
|
197
|
-
"!**/*.test.ts",
|
|
198
|
-
"--globs",
|
|
199
|
-
"!**/*.spec.ts",
|
|
200
|
-
"--globs",
|
|
201
|
-
"!**/test-utils.ts",
|
|
202
|
-
"--globs",
|
|
203
|
-
"!**/.pi-lens/**",
|
|
204
|
-
targetPath,
|
|
205
|
-
],
|
|
206
|
-
{
|
|
207
|
-
encoding: "utf-8",
|
|
208
|
-
timeout: 30000,
|
|
209
|
-
shell: true,
|
|
210
|
-
maxBuffer: 32 * 1024 * 1024, // 32MB
|
|
211
|
-
},
|
|
212
|
-
);
|
|
213
|
-
|
|
214
|
-
const output = result.stdout || result.stderr || "";
|
|
215
|
-
if (output.trim() && result.status !== undefined) {
|
|
216
|
-
const issues: Array<{
|
|
217
|
-
line: number;
|
|
218
|
-
rule: string;
|
|
219
|
-
message: string;
|
|
220
|
-
}> = [];
|
|
221
|
-
|
|
222
|
-
// ast-grep outputs either a JSON array or NDJSON (one object per line)
|
|
223
|
-
// biome-ignore lint/suspicious/noExplicitAny: ast-grep JSON output is untyped
|
|
224
|
-
const parseItems = (raw: string): Record<string, any>[] => {
|
|
225
|
-
const trimmed = raw.trim();
|
|
226
|
-
if (trimmed.startsWith("[")) {
|
|
227
|
-
try {
|
|
228
|
-
return JSON.parse(trimmed);
|
|
229
|
-
} catch (err) {
|
|
230
|
-
void err;
|
|
231
|
-
return [];
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
return raw.split("\n").flatMap((l: string) => {
|
|
235
|
-
try {
|
|
236
|
-
return [JSON.parse(l)];
|
|
237
|
-
} catch (err) {
|
|
238
|
-
void err;
|
|
239
|
-
return [];
|
|
240
|
-
}
|
|
241
|
-
});
|
|
242
|
-
};
|
|
243
|
-
|
|
244
|
-
for (const item of parseItems(output)) {
|
|
245
|
-
const ruleId =
|
|
246
|
-
item.ruleId || item.rule?.title || item.name || "unknown";
|
|
247
|
-
const ruleDesc = astGrepClient.getRuleDescription?.(ruleId);
|
|
248
|
-
const message = ruleDesc?.message || item.message || ruleId;
|
|
249
|
-
const lineNum =
|
|
250
|
-
item.labels?.[0]?.range?.start?.line ||
|
|
251
|
-
item.spans?.[0]?.range?.start?.line ||
|
|
252
|
-
item.range?.start?.line ||
|
|
253
|
-
0;
|
|
254
|
-
|
|
255
|
-
issues.push({
|
|
256
|
-
line: lineNum + 1,
|
|
257
|
-
rule: ruleId,
|
|
258
|
-
message: message,
|
|
259
|
-
});
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
if (issues.length > 0) {
|
|
263
|
-
// UI summary (truncated)
|
|
264
|
-
let report = `[ast-grep] ${issues.length} issue(s) found:\n`;
|
|
265
|
-
for (const issue of issues.slice(0, 20)) {
|
|
266
|
-
report += ` L${issue.line}: ${issue.rule} ā ${issue.message}\n`;
|
|
267
|
-
}
|
|
268
|
-
if (issues.length > 20) {
|
|
269
|
-
report += ` ... and ${issues.length - 20} more\n`;
|
|
270
|
-
}
|
|
271
|
-
parts.push(report);
|
|
272
|
-
|
|
273
|
-
// Full report for file
|
|
274
|
-
let fullSection = `## ast-grep (Structural Issues)\n\n**${issues.length} issue(s) found**\n\n`;
|
|
275
|
-
fullSection +=
|
|
276
|
-
"| Line | Rule | Message |\n|------|------|--------|\n";
|
|
277
|
-
for (const issue of issues) {
|
|
278
|
-
fullSection += `| ${issue.line} | ${issue.rule} | ${issue.message} |\n`;
|
|
279
|
-
}
|
|
280
|
-
fullReport.push(fullSection);
|
|
281
|
-
}
|
|
282
|
-
}
|
|
283
|
-
} catch (_err: any) {
|
|
284
|
-
dbg(`ast-grep scan failed: ${_err.message}`);
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
// Part 2: Similar functions (advanced duplicate detection)
|
|
289
|
-
if (astGrepClient.isAvailable()) {
|
|
290
|
-
const similarGroups = await astGrepClient.findSimilarFunctions(
|
|
291
|
-
targetPath,
|
|
292
|
-
"typescript",
|
|
293
|
-
);
|
|
294
|
-
if (similarGroups.length > 0) {
|
|
295
|
-
// UI summary (truncated)
|
|
296
|
-
let report = `[Similar Functions] ${similarGroups.length} group(s) of structurally similar functions:\n`;
|
|
297
|
-
for (const group of similarGroups.slice(0, 5)) {
|
|
298
|
-
report += ` Pattern: ${group.functions.map((f) => f.name).join(", ")}\n`;
|
|
299
|
-
for (const fn of group.functions) {
|
|
300
|
-
report += ` ${fn.name} (${path.basename(fn.file)}:${fn.line})\n`;
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
if (similarGroups.length > 5) {
|
|
304
|
-
report += ` ... and ${similarGroups.length - 5} more groups\n`;
|
|
305
|
-
}
|
|
306
|
-
parts.push(report);
|
|
307
|
-
|
|
308
|
-
// Full report for file
|
|
309
|
-
let fullSection = `## Similar Functions\n\n**${similarGroups.length} group(s) of structurally similar functions**\n\n`;
|
|
310
|
-
for (const group of similarGroups) {
|
|
311
|
-
fullSection += `### Pattern: ${group.functions.map((f) => f.name).join(", ")}\n\n`;
|
|
312
|
-
fullSection +=
|
|
313
|
-
"| Function | File | Line |\n|----------|------|------|\n";
|
|
314
|
-
for (const fn of group.functions) {
|
|
315
|
-
fullSection += `| ${fn.name} | ${fn.file} | ${fn.line} |\n`;
|
|
316
|
-
}
|
|
317
|
-
fullSection += "\n";
|
|
318
|
-
}
|
|
319
|
-
fullReport.push(fullSection);
|
|
320
|
-
}
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
// Part 3: Complexity metrics + AI slop detection
|
|
324
|
-
const results: import("./clients/complexity-client.js").FileComplexity[] =
|
|
325
|
-
[];
|
|
326
|
-
const aiSlopIssues: string[] = [];
|
|
327
|
-
|
|
328
|
-
const scanDir = (dir: string) => {
|
|
329
|
-
let entries: nodeFs.Dirent[];
|
|
330
|
-
try {
|
|
331
|
-
entries = nodeFs.readdirSync(dir, { withFileTypes: true });
|
|
332
|
-
} catch {
|
|
333
|
-
return;
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
for (const entry of entries) {
|
|
337
|
-
const fullPath = path.join(dir, entry.name);
|
|
338
|
-
if (
|
|
339
|
-
entry.isDirectory() &&
|
|
340
|
-
![
|
|
341
|
-
"node_modules",
|
|
342
|
-
".git",
|
|
343
|
-
"dist",
|
|
344
|
-
"build",
|
|
345
|
-
".next",
|
|
346
|
-
".pi-lens",
|
|
347
|
-
].includes(entry.name)
|
|
348
|
-
) {
|
|
349
|
-
scanDir(fullPath);
|
|
350
|
-
} else if (complexityClient.isSupportedFile(fullPath)) {
|
|
351
|
-
const metrics = complexityClient.analyzeFile(fullPath);
|
|
352
|
-
if (metrics) {
|
|
353
|
-
results.push(metrics);
|
|
354
|
-
|
|
355
|
-
// Check AI slop indicators ā skip test files (low MI is expected/structural)
|
|
356
|
-
if (!/\.(test|spec)\.[jt]sx?$/.test(entry.name)) {
|
|
357
|
-
const warnings = complexityClient.checkThresholds(metrics);
|
|
358
|
-
if (warnings.length > 0) {
|
|
359
|
-
aiSlopIssues.push(` ${metrics.filePath}:`);
|
|
360
|
-
for (const w of warnings) {
|
|
361
|
-
aiSlopIssues.push(` ā ${w}`);
|
|
362
|
-
}
|
|
363
|
-
}
|
|
364
|
-
}
|
|
365
|
-
}
|
|
366
|
-
}
|
|
367
|
-
}
|
|
368
|
-
};
|
|
369
|
-
|
|
370
|
-
scanDir(targetPath);
|
|
371
|
-
|
|
372
|
-
if (results.length > 0) {
|
|
373
|
-
const avgMI =
|
|
374
|
-
results.reduce((a, b) => a + b.maintainabilityIndex, 0) /
|
|
375
|
-
results.length;
|
|
376
|
-
const avgCognitive =
|
|
377
|
-
results.reduce((a, b) => a + b.cognitiveComplexity, 0) /
|
|
378
|
-
results.length;
|
|
379
|
-
const avgCyclomatic =
|
|
380
|
-
results.reduce((a, b) => a + b.cyclomaticComplexity, 0) /
|
|
381
|
-
results.length;
|
|
382
|
-
const maxNesting = Math.max(...results.map((r) => r.maxNestingDepth));
|
|
383
|
-
const maxCognitive = Math.max(
|
|
384
|
-
...results.map((r) => r.cognitiveComplexity),
|
|
385
|
-
);
|
|
386
|
-
const minMI = Math.min(...results.map((r) => r.maintainabilityIndex));
|
|
387
|
-
|
|
388
|
-
const lowMI = results
|
|
389
|
-
.filter((r) => r.maintainabilityIndex < 60)
|
|
390
|
-
.sort((a, b) => a.maintainabilityIndex - b.maintainabilityIndex);
|
|
391
|
-
const highCognitive = results
|
|
392
|
-
.filter((r) => r.cognitiveComplexity > 20)
|
|
393
|
-
.sort((a, b) => b.cognitiveComplexity - a.cognitiveComplexity);
|
|
394
|
-
|
|
395
|
-
// UI summary (truncated)
|
|
396
|
-
let summary = `[Complexity] ${results.length} file(s) scanned\n`;
|
|
397
|
-
summary += ` Maintainability: ${avgMI.toFixed(1)} avg | Cognitive: ${avgCognitive.toFixed(1)} avg | Max Nesting: ${maxNesting} levels\n`;
|
|
398
|
-
|
|
399
|
-
if (lowMI.length > 0) {
|
|
400
|
-
summary += `\n Low Maintainability (MI < 60):\n`;
|
|
401
|
-
for (const f of lowMI.slice(0, 5)) {
|
|
402
|
-
summary += ` ā ${f.filePath}: MI ${f.maintainabilityIndex.toFixed(1)}\n`;
|
|
403
|
-
}
|
|
404
|
-
if (lowMI.length > 5)
|
|
405
|
-
summary += ` ... and ${lowMI.length - 5} more\n`;
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
if (highCognitive.length > 0) {
|
|
409
|
-
summary += `\n High Cognitive Complexity (> 20):\n`;
|
|
410
|
-
for (const f of highCognitive.slice(0, 5)) {
|
|
411
|
-
summary += ` ā ${f.filePath}: ${f.cognitiveComplexity}\n`;
|
|
412
|
-
}
|
|
413
|
-
if (highCognitive.length > 5)
|
|
414
|
-
summary += ` ... and ${highCognitive.length - 5} more\n`;
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
// Add AI slop issues
|
|
418
|
-
if (aiSlopIssues.length > 0) {
|
|
419
|
-
summary += `\n[AI Slop Indicators]\n${aiSlopIssues.join("\n")}`;
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
parts.push(summary);
|
|
423
|
-
|
|
424
|
-
// Full report for file
|
|
425
|
-
let fullSection = `## Complexity Metrics\n\n**${results.length} file(s) scanned**\n\n`;
|
|
426
|
-
fullSection += `### Summary\n\n`;
|
|
427
|
-
fullSection += `| Metric | Value |\n|--------|-------|\n`;
|
|
428
|
-
fullSection += `| Avg Maintainability Index | ${avgMI.toFixed(1)} |\n`;
|
|
429
|
-
fullSection += `| Min Maintainability Index | ${minMI.toFixed(1)} |\n`;
|
|
430
|
-
fullSection += `| Avg Cognitive Complexity | ${avgCognitive.toFixed(1)} |\n`;
|
|
431
|
-
fullSection += `| Max Cognitive Complexity | ${maxCognitive} |\n`;
|
|
432
|
-
fullSection += `| Avg Cyclomatic Complexity | ${avgCyclomatic.toFixed(1)} |\n`;
|
|
433
|
-
fullSection += `| Max Nesting Depth | ${maxNesting} |\n`;
|
|
434
|
-
fullSection += `| Total Files | ${results.length} |\n\n`;
|
|
435
|
-
|
|
436
|
-
if (lowMI.length > 0) {
|
|
437
|
-
fullSection += `### Low Maintainability (MI < 60)\n\n`;
|
|
438
|
-
fullSection += `| File | MI | Cognitive | Cyclomatic | Nesting |\n`;
|
|
439
|
-
fullSection += `|------|-----|-----------|------------|--------|\n`;
|
|
440
|
-
for (const f of lowMI) {
|
|
441
|
-
fullSection += `| ${f.filePath} | ${f.maintainabilityIndex.toFixed(1)} | ${f.cognitiveComplexity} | ${f.cyclomaticComplexity} | ${f.maxNestingDepth} |\n`;
|
|
442
|
-
}
|
|
443
|
-
fullSection += "\n";
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
if (highCognitive.length > 0) {
|
|
447
|
-
fullSection += `### High Cognitive Complexity (> 20)\n\n`;
|
|
448
|
-
fullSection += `| File | Cognitive | MI | Cyclomatic | Nesting |\n`;
|
|
449
|
-
fullSection += `|------|-----------|-----|------------|--------|\n`;
|
|
450
|
-
for (const f of highCognitive) {
|
|
451
|
-
fullSection += `| ${f.filePath} | ${f.cognitiveComplexity} | ${f.maintainabilityIndex.toFixed(1)} | ${f.cyclomaticComplexity} | ${f.maxNestingDepth} |\n`;
|
|
452
|
-
}
|
|
453
|
-
fullSection += "\n";
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
// All files table
|
|
457
|
-
fullSection += `### All Files\n\n`;
|
|
458
|
-
fullSection += `| File | MI | Cognitive | Cyclomatic | Nesting | Entropy |\n`;
|
|
459
|
-
fullSection += `|------|-----|-----------|------------|---------|--------|\n`;
|
|
460
|
-
for (const f of results.sort(
|
|
461
|
-
(a, b) => a.maintainabilityIndex - b.maintainabilityIndex,
|
|
462
|
-
)) {
|
|
463
|
-
fullSection += `| ${f.filePath} | ${f.maintainabilityIndex.toFixed(1)} | ${f.cognitiveComplexity} | ${f.cyclomaticComplexity} | ${f.maxNestingDepth} | ${f.codeEntropy.toFixed(2)} |\n`;
|
|
464
|
-
}
|
|
465
|
-
fullSection += "\n";
|
|
466
|
-
|
|
467
|
-
// AI slop indicators
|
|
468
|
-
if (aiSlopIssues.length > 0) {
|
|
469
|
-
fullSection += `### AI Slop Indicators\n\n`;
|
|
470
|
-
for (const issue of aiSlopIssues) {
|
|
471
|
-
fullSection += `${issue}\n`;
|
|
472
|
-
}
|
|
473
|
-
fullSection += "\n";
|
|
474
|
-
}
|
|
475
|
-
|
|
476
|
-
fullReport.push(fullSection);
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
// Part 4: TODOs scan
|
|
480
|
-
const todoResult = todoScanner.scanDirectory(targetPath);
|
|
481
|
-
const todoReport = todoScanner.formatResult(todoResult);
|
|
482
|
-
if (todoReport) {
|
|
483
|
-
parts.push(todoReport);
|
|
484
|
-
// Full TODO report
|
|
485
|
-
let fullSection = `## TODOs / Annotations\n\n`;
|
|
486
|
-
if (todoResult.items.length > 0) {
|
|
487
|
-
fullSection += `**${todoResult.items.length} annotation(s) found**\n\n`;
|
|
488
|
-
fullSection += `| Type | File | Line | Text |\n`;
|
|
489
|
-
fullSection += `|------|------|------|------|\n`;
|
|
490
|
-
for (const item of todoResult.items) {
|
|
491
|
-
fullSection += `| ${item.type} | ${item.file} | ${item.line} | ${item.message} |\n`;
|
|
492
|
-
}
|
|
493
|
-
} else {
|
|
494
|
-
fullSection += `No annotations found.\n`;
|
|
495
|
-
}
|
|
496
|
-
fullSection += "\n";
|
|
497
|
-
fullReport.push(fullSection);
|
|
498
|
-
}
|
|
499
|
-
|
|
500
|
-
// Part 5: Dead code (knip)
|
|
501
|
-
if (knipClient.isAvailable()) {
|
|
502
|
-
const knipResult = knipClient.analyze(targetPath);
|
|
503
|
-
const knipReport = knipClient.formatResult(knipResult);
|
|
504
|
-
if (knipReport) {
|
|
505
|
-
parts.push(knipReport);
|
|
506
|
-
// Full knip report
|
|
507
|
-
let fullSection = `## Dead Code (Knip)\n\n`;
|
|
508
|
-
if (knipResult.issues.length > 0) {
|
|
509
|
-
fullSection += `**${knipResult.issues.length} issue(s) found**\n\n`;
|
|
510
|
-
fullSection += `| Type | Name | File |\n`;
|
|
511
|
-
fullSection += `|------|------|------|\n`;
|
|
512
|
-
for (const issue of knipResult.issues) {
|
|
513
|
-
fullSection += `| ${issue.type} | ${issue.name} | ${issue.file ?? ""} |\n`;
|
|
514
|
-
}
|
|
515
|
-
} else {
|
|
516
|
-
fullSection += `No dead code issues found.\n`;
|
|
517
|
-
}
|
|
518
|
-
fullSection += "\n";
|
|
519
|
-
fullReport.push(fullSection);
|
|
520
|
-
}
|
|
521
|
-
}
|
|
522
|
-
|
|
523
|
-
// Part 6: Code duplication
|
|
524
|
-
if (jscpdClient.isAvailable()) {
|
|
525
|
-
const jscpdResult = jscpdClient.scan(targetPath);
|
|
526
|
-
const jscpdReport = jscpdClient.formatResult(jscpdResult);
|
|
527
|
-
if (jscpdReport) {
|
|
528
|
-
parts.push(jscpdReport);
|
|
529
|
-
// Full jscpd report
|
|
530
|
-
let fullSection = `## Code Duplication (jscpd)\n\n`;
|
|
531
|
-
if (jscpdResult.clones.length > 0) {
|
|
532
|
-
fullSection += `**${jscpdResult.clones.length} duplicate block(s) found** (${jscpdResult.duplicatedLines}/${jscpdResult.totalLines} lines, ${jscpdResult.percentage.toFixed(1)}%)\n\n`;
|
|
533
|
-
fullSection += `| File A | Line A | File B | Line B | Lines | Tokens |\n`;
|
|
534
|
-
fullSection += `|--------|--------|--------|--------|-------|--------|\n`;
|
|
535
|
-
for (const dup of jscpdResult.clones) {
|
|
536
|
-
fullSection += `| ${dup.fileA} | ${dup.startA} | ${dup.fileB} | ${dup.startB} | ${dup.lines} | ${dup.tokens} |\n`;
|
|
537
|
-
}
|
|
538
|
-
} else {
|
|
539
|
-
fullSection += `No duplicate code found.\n`;
|
|
540
|
-
}
|
|
541
|
-
fullSection += "\n";
|
|
542
|
-
fullReport.push(fullSection);
|
|
543
|
-
}
|
|
544
|
-
}
|
|
545
|
-
|
|
546
|
-
// Part 7: Type coverage
|
|
547
|
-
if (typeCoverageClient.isAvailable()) {
|
|
548
|
-
const tcResult = typeCoverageClient.scan(targetPath);
|
|
549
|
-
const tcReport = typeCoverageClient.formatResult(tcResult);
|
|
550
|
-
if (tcReport) {
|
|
551
|
-
parts.push(tcReport);
|
|
552
|
-
// Full type coverage report
|
|
553
|
-
let fullSection = `## Type Coverage\n\n`;
|
|
554
|
-
fullSection += `**${tcResult.percentage.toFixed(1)}% typed** (${tcResult.typed}/${tcResult.total} identifiers)\n\n`;
|
|
555
|
-
if (tcResult.untypedLocations.length > 0) {
|
|
556
|
-
fullSection += `### Untyped Identifiers\n\n`;
|
|
557
|
-
fullSection += `| File | Line | Column | Name |\n`;
|
|
558
|
-
fullSection += `|------|------|--------|------|\n`;
|
|
559
|
-
for (const u of tcResult.untypedLocations) {
|
|
560
|
-
fullSection += `| ${u.file} | ${u.line} | ${u.column} | ${u.name} |\n`;
|
|
561
|
-
}
|
|
562
|
-
}
|
|
563
|
-
fullSection += "\n";
|
|
564
|
-
fullReport.push(fullSection);
|
|
565
|
-
}
|
|
566
|
-
}
|
|
567
|
-
|
|
568
|
-
// Part 8: Circular dependencies
|
|
569
|
-
if (!pi.getFlag("no-madge") && depChecker.isAvailable()) {
|
|
570
|
-
const { circular } = depChecker.scanProject(targetPath);
|
|
571
|
-
const depReport = depChecker.formatScanResult(circular);
|
|
572
|
-
if (depReport) {
|
|
573
|
-
parts.push(depReport);
|
|
574
|
-
let fullSection = `## Circular Dependencies (Madge)\n\n`;
|
|
575
|
-
fullSection += `**${circular.length} circular chain(s) found**\n\n`;
|
|
576
|
-
for (const dep of circular) {
|
|
577
|
-
fullSection += `- ${dep.path.join(" ā ")}\n`;
|
|
578
|
-
}
|
|
579
|
-
fullSection += "\n";
|
|
580
|
-
fullReport.push(fullSection);
|
|
581
|
-
}
|
|
582
|
-
}
|
|
583
|
-
|
|
584
|
-
// Part 9: Architectural Rules
|
|
585
|
-
// Reload config in case it was added after session start
|
|
586
|
-
if (!architectClient.hasConfig()) {
|
|
587
|
-
architectClient.loadConfig(projectRoot);
|
|
588
|
-
}
|
|
589
|
-
if (architectClient.hasConfig()) {
|
|
590
|
-
const archViolations: Array<{ file: string; message: string }> = [];
|
|
591
|
-
const archScanDir = (dir: string) => {
|
|
592
|
-
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
593
|
-
const full = path.join(dir, entry.name);
|
|
594
|
-
if (entry.isDirectory()) {
|
|
595
|
-
if (
|
|
596
|
-
["node_modules", ".git", "dist", "build", ".next", ".pi-lens"].includes(
|
|
597
|
-
entry.name,
|
|
598
|
-
)
|
|
599
|
-
)
|
|
600
|
-
continue;
|
|
601
|
-
archScanDir(full);
|
|
602
|
-
} else if (/\.(ts|tsx|js|jsx|py|go|rs)$/.test(entry.name)) {
|
|
603
|
-
const relPath = path.relative(targetPath, full).replace(/\\/g, "/");
|
|
604
|
-
const content = fs.readFileSync(full, "utf-8");
|
|
605
|
-
const lineCount = content.split("\n").length;
|
|
606
|
-
|
|
607
|
-
// Check pattern violations
|
|
608
|
-
const violations = architectClient.checkFile(relPath, content);
|
|
609
|
-
for (const v of violations) {
|
|
610
|
-
archViolations.push({ file: relPath, message: v.message });
|
|
611
|
-
}
|
|
612
|
-
|
|
613
|
-
// Check file size
|
|
614
|
-
const sizeV = architectClient.checkFileSize(relPath, lineCount);
|
|
615
|
-
if (sizeV) {
|
|
616
|
-
archViolations.push({ file: relPath, message: sizeV.message });
|
|
617
|
-
}
|
|
618
|
-
}
|
|
619
|
-
}
|
|
620
|
-
};
|
|
621
|
-
archScanDir(targetPath);
|
|
622
|
-
|
|
623
|
-
if (archViolations.length > 0) {
|
|
624
|
-
parts.push(
|
|
625
|
-
`š“ ${archViolations.length} architectural violation(s) ā fix before adding new code`,
|
|
626
|
-
);
|
|
627
|
-
let fullSection = `## Architectural Rules\n\n`;
|
|
628
|
-
fullSection += `**${archViolations.length} violation(s) found**\n\n`;
|
|
629
|
-
for (const v of archViolations) {
|
|
630
|
-
fullSection += `- **${v.file}**: ${v.message}\n`;
|
|
631
|
-
}
|
|
632
|
-
fullSection += "\n";
|
|
633
|
-
fullReport.push(fullSection);
|
|
634
|
-
}
|
|
635
|
-
}
|
|
636
|
-
|
|
637
|
-
// Build and save full markdown report
|
|
638
|
-
const fs = require("node:fs");
|
|
639
|
-
fs.mkdirSync(reviewDir, { recursive: true });
|
|
640
|
-
|
|
641
|
-
const projectName = path.basename(process.cwd());
|
|
642
|
-
let mdReport = `# Code Review: ${projectName}\n\n`;
|
|
643
|
-
mdReport += `**Scanned:** ${new Date().toISOString()}\n\n`;
|
|
644
|
-
mdReport += `**Path:** \`${targetPath}\`\n\n`;
|
|
645
|
-
mdReport += `---\n\n`;
|
|
646
|
-
mdReport += fullReport.join("\n");
|
|
647
|
-
|
|
648
|
-
const reportPath = path.join(reviewDir, `booboo-${timestamp}.md`);
|
|
649
|
-
fs.writeFileSync(reportPath, mdReport, "utf-8");
|
|
650
|
-
|
|
651
|
-
if (parts.length === 0) {
|
|
652
|
-
ctx.ui.notify(
|
|
653
|
-
"ā Code review clean ā saved to .pi-lens/reviews/",
|
|
654
|
-
"info",
|
|
655
|
-
);
|
|
656
|
-
} else {
|
|
657
|
-
ctx.ui.notify(
|
|
658
|
-
`${parts.join("\n\n")}\n\nš Full report: ${reportPath}`,
|
|
659
|
-
"info",
|
|
660
|
-
);
|
|
661
|
-
}
|
|
662
|
-
},
|
|
154
|
+
handler: (args, ctx) =>
|
|
155
|
+
handleBooboo(
|
|
156
|
+
args,
|
|
157
|
+
ctx,
|
|
158
|
+
{
|
|
159
|
+
astGrep: astGrepClient,
|
|
160
|
+
complexity: complexityClient,
|
|
161
|
+
todo: todoScanner,
|
|
162
|
+
knip: knipClient,
|
|
163
|
+
jscpd: jscpdClient,
|
|
164
|
+
typeCoverage: typeCoverageClient,
|
|
165
|
+
depChecker: depChecker,
|
|
166
|
+
architect: architectClient,
|
|
167
|
+
},
|
|
168
|
+
pi,
|
|
169
|
+
),
|
|
663
170
|
});
|
|
664
171
|
|
|
665
172
|
// --- Rule action map for lens-booboo-fix ---
|
|
@@ -754,569 +261,41 @@ export default function (pi: ExtensionAPI) {
|
|
|
754
261
|
|
|
755
262
|
pi.registerCommand("lens-booboo-fix", {
|
|
756
263
|
description:
|
|
757
|
-
"Iterative fix loop: auto-fixes Biome/Ruff, then generates a per-issue plan for agent to execute. Run repeatedly until clean. Usage: /lens-booboo-fix [path]",
|
|
758
|
-
handler:
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
const MAX_ITERATIONS = 3;
|
|
776
|
-
|
|
777
|
-
// Detect TypeScript project ā exclude compiled .js output from scans
|
|
778
|
-
const isTsProject = fs.existsSync(path.join(targetPath, "tsconfig.json"));
|
|
779
|
-
dbg(`booboo-fix: isTsProject=${isTsProject}`);
|
|
780
|
-
|
|
781
|
-
// Load session state
|
|
782
|
-
let session: { iteration: number; counts: Record<string, number> } = {
|
|
783
|
-
iteration: 0,
|
|
784
|
-
counts: {},
|
|
785
|
-
};
|
|
786
|
-
try {
|
|
787
|
-
session = JSON.parse(fs.readFileSync(sessionFile, "utf-8"));
|
|
788
|
-
} catch (e) {
|
|
789
|
-
dbg(`fix-session load failed: ${e}`);
|
|
790
|
-
}
|
|
791
|
-
session.iteration++;
|
|
792
|
-
|
|
793
|
-
// Hard stop at max iterations ā auto-reset for next run
|
|
794
|
-
if (session.iteration > MAX_ITERATIONS) {
|
|
795
|
-
try {
|
|
796
|
-
fs.unlinkSync(sessionFile);
|
|
797
|
-
} catch {
|
|
798
|
-
void 0;
|
|
799
|
-
}
|
|
800
|
-
ctx.ui.notify(
|
|
801
|
-
`ā Max iterations (${MAX_ITERATIONS}) reached. Session reset ā run /lens-booboo-fix again for a fresh loop, or /lens-booboo for a full report.`,
|
|
802
|
-
"warning",
|
|
803
|
-
);
|
|
804
|
-
return;
|
|
805
|
-
}
|
|
806
|
-
const prevCounts = { ...session.counts };
|
|
807
|
-
|
|
808
|
-
// --- Step 1: Auto-fix with Biome + Ruff ---
|
|
809
|
-
let biomeRan = false;
|
|
810
|
-
if (!pi.getFlag("no-biome") && biomeClient.isAvailable()) {
|
|
811
|
-
require("node:child_process").spawnSync(
|
|
812
|
-
"npx",
|
|
813
|
-
["@biomejs/biome", "check", "--write", "--unsafe", targetPath],
|
|
814
|
-
{ encoding: "utf-8", timeout: 30000, shell: true },
|
|
815
|
-
);
|
|
816
|
-
biomeRan = true;
|
|
817
|
-
}
|
|
818
|
-
let ruffRan = false;
|
|
819
|
-
if (!pi.getFlag("no-ruff") && ruffClient.isAvailable()) {
|
|
820
|
-
require("node:child_process").spawnSync(
|
|
821
|
-
"ruff",
|
|
822
|
-
["check", "--fix", targetPath],
|
|
823
|
-
{ encoding: "utf-8", timeout: 15000, shell: true },
|
|
824
|
-
);
|
|
825
|
-
require("node:child_process").spawnSync(
|
|
826
|
-
"ruff",
|
|
827
|
-
["format", targetPath],
|
|
828
|
-
{ encoding: "utf-8", timeout: 15000, shell: true },
|
|
829
|
-
);
|
|
830
|
-
ruffRan = true;
|
|
831
|
-
}
|
|
832
|
-
|
|
833
|
-
// --- Step 2: Duplicate code (jscpd) ---
|
|
834
|
-
type JscpdClone = {
|
|
835
|
-
fileA: string;
|
|
836
|
-
fileB: string;
|
|
837
|
-
startA: number;
|
|
838
|
-
startB: number;
|
|
839
|
-
lines: number;
|
|
840
|
-
};
|
|
841
|
-
const dupClones: JscpdClone[] = [];
|
|
842
|
-
if (jscpdClient.isAvailable()) {
|
|
843
|
-
const jscpdResult = jscpdClient.scan(targetPath);
|
|
844
|
-
// Only within-file duplicates are mechanically fixable
|
|
845
|
-
const clones = jscpdResult.clones.filter((c) => {
|
|
846
|
-
if (isTsProject && (c.fileA.endsWith(".js") || c.fileB.endsWith(".js"))) return false;
|
|
847
|
-
return path.resolve(c.fileA) === path.resolve(c.fileB);
|
|
848
|
-
});
|
|
849
|
-
dupClones.push(...clones);
|
|
850
|
-
dbg(
|
|
851
|
-
`booboo-fix jscpd: ${dupClones.length} within-file clone(s) from ${jscpdResult.clones.length} total`,
|
|
852
|
-
);
|
|
853
|
-
}
|
|
854
|
-
|
|
855
|
-
// --- Step 3: Dead code (knip) ---
|
|
856
|
-
type KnipIssue = { type: string; name: string; file?: string };
|
|
857
|
-
const deadCodeIssues: KnipIssue[] = [];
|
|
858
|
-
if (knipClient.isAvailable()) {
|
|
859
|
-
const knipResult = knipClient.analyze(targetPath);
|
|
860
|
-
deadCodeIssues.push(...knipResult.issues);
|
|
861
|
-
dbg(`booboo-fix knip: ${deadCodeIssues.length} issue(s)`);
|
|
862
|
-
}
|
|
863
|
-
|
|
864
|
-
// --- Step 4: ast-grep scan (on surviving code) ---
|
|
865
|
-
type AstIssue = {
|
|
866
|
-
rule: string;
|
|
867
|
-
file: string;
|
|
868
|
-
line: number;
|
|
869
|
-
message: string;
|
|
870
|
-
};
|
|
871
|
-
const astIssues: AstIssue[] = [];
|
|
872
|
-
if (astGrepClient.isAvailable()) {
|
|
873
|
-
const result = require("node:child_process").spawnSync(
|
|
874
|
-
"npx",
|
|
875
|
-
[
|
|
876
|
-
"sg",
|
|
877
|
-
"scan",
|
|
878
|
-
"--config",
|
|
879
|
-
configPath,
|
|
880
|
-
"--json",
|
|
881
|
-
"--globs",
|
|
882
|
-
"!**/*.test.ts",
|
|
883
|
-
"--globs",
|
|
884
|
-
"!**/*.spec.ts",
|
|
885
|
-
"--globs",
|
|
886
|
-
"!**/test-utils.ts",
|
|
887
|
-
"--globs",
|
|
888
|
-
"!**/.pi-lens/**",
|
|
889
|
-
...(isTsProject ? ["--globs", "!**/*.js"] : []),
|
|
890
|
-
targetPath,
|
|
891
|
-
],
|
|
892
|
-
{
|
|
893
|
-
encoding: "utf-8",
|
|
894
|
-
timeout: 30000,
|
|
895
|
-
shell: true,
|
|
896
|
-
maxBuffer: 32 * 1024 * 1024,
|
|
897
|
-
},
|
|
898
|
-
);
|
|
899
|
-
|
|
900
|
-
const raw = result.stdout?.trim() ?? "";
|
|
901
|
-
// biome-ignore lint/suspicious/noExplicitAny: ast-grep JSON output is untyped
|
|
902
|
-
const items: Record<string, any>[] = raw.startsWith("[")
|
|
903
|
-
? (() => {
|
|
904
|
-
try {
|
|
905
|
-
return JSON.parse(raw);
|
|
906
|
-
} catch (e) {
|
|
907
|
-
dbg(`ast-grep parse failed: ${e}`);
|
|
908
|
-
return [];
|
|
909
|
-
}
|
|
910
|
-
})()
|
|
911
|
-
: raw.split("\n").flatMap((l: string) => {
|
|
912
|
-
try {
|
|
913
|
-
return [JSON.parse(l)];
|
|
914
|
-
} catch (err) {
|
|
915
|
-
void err;
|
|
916
|
-
return [];
|
|
917
|
-
}
|
|
918
|
-
});
|
|
919
|
-
|
|
920
|
-
for (const item of items) {
|
|
921
|
-
const rule =
|
|
922
|
-
item.ruleId || item.rule?.title || item.name || "unknown";
|
|
923
|
-
const line =
|
|
924
|
-
(item.labels?.[0]?.range?.start?.line ??
|
|
925
|
-
item.range?.start?.line ??
|
|
926
|
-
0) + 1;
|
|
927
|
-
const relFile = path
|
|
928
|
-
.relative(targetPath, item.file ?? "")
|
|
929
|
-
.replace(/\\/g, "/");
|
|
930
|
-
astIssues.push({
|
|
931
|
-
rule,
|
|
932
|
-
file: relFile,
|
|
933
|
-
line,
|
|
934
|
-
message: item.message ?? rule,
|
|
935
|
-
});
|
|
936
|
-
}
|
|
937
|
-
}
|
|
938
|
-
|
|
939
|
-
// --- Step 5: AI slop from complexity scan ---
|
|
940
|
-
const slopFiles: Array<{ file: string; warnings: string[] }> = [];
|
|
941
|
-
const slopScanDir = (dir: string) => {
|
|
942
|
-
if (!fs.existsSync(dir)) return;
|
|
943
|
-
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
944
|
-
const fullPath = path.join(dir, entry.name);
|
|
945
|
-
if (entry.isDirectory()) {
|
|
946
|
-
if (
|
|
947
|
-
[
|
|
948
|
-
"node_modules",
|
|
949
|
-
".git",
|
|
950
|
-
"dist",
|
|
951
|
-
"build",
|
|
952
|
-
".next",
|
|
953
|
-
".pi-lens",
|
|
954
|
-
].includes(entry.name)
|
|
955
|
-
)
|
|
956
|
-
continue;
|
|
957
|
-
slopScanDir(fullPath);
|
|
958
|
-
} else if (
|
|
959
|
-
complexityClient.isSupportedFile(fullPath) &&
|
|
960
|
-
!/\.(test|spec)\.[jt]sx?$/.test(entry.name) &&
|
|
961
|
-
!(isTsProject && /\.js$/.test(entry.name))
|
|
962
|
-
) {
|
|
963
|
-
const metrics = complexityClient.analyzeFile(fullPath);
|
|
964
|
-
if (metrics) {
|
|
965
|
-
const warnings = complexityClient
|
|
966
|
-
.checkThresholds(metrics)
|
|
967
|
-
.filter(
|
|
968
|
-
(w) =>
|
|
969
|
-
w.includes("AI-style") ||
|
|
970
|
-
w.includes("try/catch") ||
|
|
971
|
-
w.includes("single-use") ||
|
|
972
|
-
w.includes("Excessive comments"),
|
|
973
|
-
);
|
|
974
|
-
// Only flag files with 2+ signals ā single-issue flags are noise
|
|
975
|
-
if (warnings.length >= 2) {
|
|
976
|
-
slopFiles.push({
|
|
977
|
-
file: path.relative(targetPath, fullPath).replace(/\\/g, "/"),
|
|
978
|
-
warnings,
|
|
979
|
-
});
|
|
980
|
-
}
|
|
981
|
-
}
|
|
982
|
-
}
|
|
983
|
-
}
|
|
984
|
-
};
|
|
985
|
-
slopScanDir(targetPath);
|
|
986
|
-
|
|
987
|
-
// --- Step 6: Remaining Biome lint (unfixable by --unsafe) ---
|
|
988
|
-
const remainingBiome: Array<{
|
|
989
|
-
file: string;
|
|
990
|
-
line: number;
|
|
991
|
-
rule: string;
|
|
992
|
-
message: string;
|
|
993
|
-
}> = [];
|
|
994
|
-
if (!pi.getFlag("no-biome") && biomeClient.isAvailable()) {
|
|
995
|
-
const checkResult = require("node:child_process").spawnSync(
|
|
996
|
-
"npx",
|
|
997
|
-
[
|
|
998
|
-
"@biomejs/biome",
|
|
999
|
-
"check",
|
|
1000
|
-
"--reporter=json",
|
|
1001
|
-
"--max-diagnostics=50",
|
|
1002
|
-
targetPath,
|
|
1003
|
-
],
|
|
1004
|
-
{ encoding: "utf-8", timeout: 20000, shell: true },
|
|
1005
|
-
);
|
|
1006
|
-
try {
|
|
1007
|
-
const data = JSON.parse(checkResult.stdout ?? "{}");
|
|
1008
|
-
for (const diag of (data.diagnostics ?? []).slice(0, 20)) {
|
|
1009
|
-
if (!diag.category?.startsWith("lint/")) continue;
|
|
1010
|
-
const filePath = diag.location?.path?.file ?? "";
|
|
1011
|
-
const line = diag.location?.span?.start?.line ?? 0;
|
|
1012
|
-
const rule = diag.category ?? "lint";
|
|
1013
|
-
remainingBiome.push({
|
|
1014
|
-
file: path.relative(targetPath, filePath).replace(/\\/g, "/"),
|
|
1015
|
-
line: line + 1,
|
|
1016
|
-
rule,
|
|
1017
|
-
message: diag.message ?? rule,
|
|
1018
|
-
});
|
|
1019
|
-
}
|
|
1020
|
-
} catch (e) {
|
|
1021
|
-
dbg(`biome lint parse failed: ${e}`);
|
|
1022
|
-
}
|
|
1023
|
-
}
|
|
1024
|
-
|
|
1025
|
-
// --- Categorize ast-grep issues ---
|
|
1026
|
-
const agentTasks: AstIssue[] = [];
|
|
1027
|
-
const skipRules = new Map<string, { note: string; count: number }>();
|
|
1028
|
-
|
|
1029
|
-
const byRule = new Map<string, AstIssue[]>();
|
|
1030
|
-
for (const issue of astIssues) {
|
|
1031
|
-
const list = byRule.get(issue.rule) ?? [];
|
|
1032
|
-
list.push(issue);
|
|
1033
|
-
byRule.set(issue.rule, list);
|
|
1034
|
-
}
|
|
1035
|
-
for (const [rule, issues] of byRule) {
|
|
1036
|
-
const action = RULE_ACTIONS[rule];
|
|
1037
|
-
if (!action || action.type === "agent") {
|
|
1038
|
-
agentTasks.push(...issues);
|
|
1039
|
-
} else if (action.type === "skip") {
|
|
1040
|
-
skipRules.set(rule, { note: action.note, count: issues.length });
|
|
1041
|
-
}
|
|
1042
|
-
}
|
|
1043
|
-
|
|
1044
|
-
// --- Update session counts ---
|
|
1045
|
-
const currentCounts = {
|
|
1046
|
-
duplicates: dupClones.length,
|
|
1047
|
-
dead_code: deadCodeIssues.length,
|
|
1048
|
-
agent_ast: agentTasks.length,
|
|
1049
|
-
biome_lint: remainingBiome.length,
|
|
1050
|
-
slop_files: slopFiles.length,
|
|
1051
|
-
};
|
|
1052
|
-
session.counts = currentCounts;
|
|
1053
|
-
fs.mkdirSync(path.dirname(sessionFile), { recursive: true });
|
|
1054
|
-
fs.writeFileSync(sessionFile, JSON.stringify(session, null, 2), "utf-8");
|
|
1055
|
-
|
|
1056
|
-
// --- Check if done ---
|
|
1057
|
-
const totalFixable =
|
|
1058
|
-
dupClones.length +
|
|
1059
|
-
deadCodeIssues.length +
|
|
1060
|
-
agentTasks.length +
|
|
1061
|
-
remainingBiome.length +
|
|
1062
|
-
slopFiles.length;
|
|
1063
|
-
if (totalFixable === 0) {
|
|
1064
|
-
const msg = `ā
BOOBOO FIX LOOP COMPLETE ā No more fixable issues found after ${session.iteration} iteration(s).\n\nRemaining skipped items are architectural ā see /lens-booboo for full report.`;
|
|
1065
|
-
ctx.ui.notify(msg, "info");
|
|
1066
|
-
try {
|
|
1067
|
-
fs.unlinkSync(sessionFile);
|
|
1068
|
-
} catch {
|
|
1069
|
-
void 0;
|
|
1070
|
-
}
|
|
1071
|
-
return;
|
|
1072
|
-
}
|
|
1073
|
-
|
|
1074
|
-
// --- Build delta line ---
|
|
1075
|
-
let deltaLine = "";
|
|
1076
|
-
if (session.iteration > 1 && Object.keys(prevCounts).length > 0) {
|
|
1077
|
-
const prevTotal = Object.values(prevCounts).reduce((a, b) => a + b, 0);
|
|
1078
|
-
const fixed = prevTotal - totalFixable;
|
|
1079
|
-
deltaLine =
|
|
1080
|
-
fixed > 0
|
|
1081
|
-
? `ā
Fixed ${fixed} issues since last iteration.`
|
|
1082
|
-
: `ā ļø No change since last iteration ā check if fixes were applied.`;
|
|
1083
|
-
}
|
|
1084
|
-
|
|
1085
|
-
// --- Build the fix plan message ---
|
|
1086
|
-
const lines: string[] = [];
|
|
1087
|
-
lines.push(
|
|
1088
|
-
`š BOOBOO FIX PLAN ā Iteration ${session.iteration}/${MAX_ITERATIONS} (${totalFixable} fixable items remaining)`,
|
|
1089
|
-
);
|
|
1090
|
-
if (deltaLine) lines.push(deltaLine);
|
|
1091
|
-
lines.push("");
|
|
1092
|
-
|
|
1093
|
-
if (biomeRan || ruffRan) {
|
|
1094
|
-
lines.push(
|
|
1095
|
-
`ā” Auto-fixed: ${[biomeRan && "Biome --write --unsafe", ruffRan && "Ruff --fix + format"].filter(Boolean).join(", ")} already ran.`,
|
|
1096
|
-
);
|
|
1097
|
-
lines.push("");
|
|
1098
|
-
}
|
|
1099
|
-
|
|
1100
|
-
// Duplicate code ā fix first
|
|
1101
|
-
if (dupClones.length > 0) {
|
|
1102
|
-
lines.push(
|
|
1103
|
-
`## š Duplicate code [${dupClones.length} block(s)] ā fix first`,
|
|
1104
|
-
);
|
|
1105
|
-
lines.push(
|
|
1106
|
-
"ā Extract duplicated blocks into shared utilities before fixing violations in them.",
|
|
1107
|
-
);
|
|
1108
|
-
for (const clone of dupClones.slice(0, 10)) {
|
|
1109
|
-
const relA = path
|
|
1110
|
-
.relative(targetPath, clone.fileA)
|
|
1111
|
-
.replace(/\\/g, "/");
|
|
1112
|
-
const relB = path
|
|
1113
|
-
.relative(targetPath, clone.fileB)
|
|
1114
|
-
.replace(/\\/g, "/");
|
|
1115
|
-
lines.push(
|
|
1116
|
-
` - ${clone.lines} lines: \`${relA}:${clone.startA}\` ā \`${relB}:${clone.startB}\``,
|
|
1117
|
-
);
|
|
1118
|
-
}
|
|
1119
|
-
if (dupClones.length > 10)
|
|
1120
|
-
lines.push(` ... and ${dupClones.length - 10} more`);
|
|
1121
|
-
lines.push("");
|
|
1122
|
-
}
|
|
1123
|
-
|
|
1124
|
-
// Dead code ā delete before fixing violations in it
|
|
1125
|
-
if (deadCodeIssues.length > 0) {
|
|
1126
|
-
lines.push(
|
|
1127
|
-
`## šļø Dead code [${deadCodeIssues.length} item(s)] ā delete before fixing violations`,
|
|
1128
|
-
);
|
|
1129
|
-
lines.push(
|
|
1130
|
-
"ā Remove unused exports/files ā no point fixing violations in code you're about to delete.",
|
|
1131
|
-
);
|
|
1132
|
-
for (const issue of deadCodeIssues.slice(0, 10)) {
|
|
1133
|
-
lines.push(
|
|
1134
|
-
` - [${issue.type}] \`${issue.name}\`${issue.file ? ` in ${issue.file}` : ""}`,
|
|
1135
|
-
);
|
|
1136
|
-
}
|
|
1137
|
-
if (deadCodeIssues.length > 10)
|
|
1138
|
-
lines.push(` ... and ${deadCodeIssues.length - 10} more`);
|
|
1139
|
-
lines.push("");
|
|
1140
|
-
}
|
|
1141
|
-
|
|
1142
|
-
// Agent tasks ā ast-grep violations on surviving code
|
|
1143
|
-
if (agentTasks.length > 0) {
|
|
1144
|
-
lines.push(`## šØ Fix these [${agentTasks.length} items]`);
|
|
1145
|
-
lines.push("");
|
|
1146
|
-
const groupedAgent = new Map<string, AstIssue[]>();
|
|
1147
|
-
for (const t of agentTasks) {
|
|
1148
|
-
const g = groupedAgent.get(t.rule) ?? [];
|
|
1149
|
-
g.push(t);
|
|
1150
|
-
groupedAgent.set(t.rule, g);
|
|
1151
|
-
}
|
|
1152
|
-
for (const [rule, issues] of groupedAgent) {
|
|
1153
|
-
const action = RULE_ACTIONS[rule];
|
|
1154
|
-
const note = action?.note ?? "Fix this violation";
|
|
1155
|
-
lines.push(`### ${rule} (${issues.length})`);
|
|
1156
|
-
lines.push(`ā ${note}`);
|
|
1157
|
-
for (const issue of issues.slice(0, 15)) {
|
|
1158
|
-
lines.push(` - \`${issue.file}:${issue.line}\``);
|
|
1159
|
-
}
|
|
1160
|
-
if (issues.length > 15)
|
|
1161
|
-
lines.push(` ... and ${issues.length - 15} more`);
|
|
1162
|
-
lines.push("");
|
|
1163
|
-
}
|
|
1164
|
-
}
|
|
1165
|
-
|
|
1166
|
-
// Remaining Biome lint ā couldn't be auto-fixed even with --unsafe
|
|
1167
|
-
if (remainingBiome.length > 0) {
|
|
1168
|
-
lines.push(
|
|
1169
|
-
`## š Remaining Biome lint [${remainingBiome.length} items]`,
|
|
1170
|
-
);
|
|
1171
|
-
lines.push(
|
|
1172
|
-
"ā These couldn't be auto-fixed by Biome --unsafe. Fix each one manually:",
|
|
1173
|
-
);
|
|
1174
|
-
for (const d of remainingBiome.slice(0, 10)) {
|
|
1175
|
-
lines.push(` - \`${d.file}:${d.line}\` [${d.rule}] ${d.message}`);
|
|
1176
|
-
}
|
|
1177
|
-
if (remainingBiome.length > 10)
|
|
1178
|
-
lines.push(` ... and ${remainingBiome.length - 10} more`);
|
|
1179
|
-
lines.push("");
|
|
1180
|
-
}
|
|
1181
|
-
|
|
1182
|
-
// AI slop
|
|
1183
|
-
if (slopFiles.length > 0) {
|
|
1184
|
-
lines.push(`## š¤ AI Slop indicators [${slopFiles.length} files]`);
|
|
1185
|
-
for (const { file, warnings } of slopFiles.slice(0, 10)) {
|
|
1186
|
-
lines.push(
|
|
1187
|
-
` - \`${file}\`: ${warnings.map((w) => w.split(" ā ")[0]).join(", ")}`,
|
|
1188
|
-
);
|
|
1189
|
-
}
|
|
1190
|
-
if (slopFiles.length > 10)
|
|
1191
|
-
lines.push(` ... and ${slopFiles.length - 10} more`);
|
|
1192
|
-
lines.push("");
|
|
1193
|
-
}
|
|
1194
|
-
|
|
1195
|
-
// Skips
|
|
1196
|
-
if (skipRules.size > 0) {
|
|
1197
|
-
lines.push(
|
|
1198
|
-
`## āļø Skip [${[...skipRules.values()].reduce((a, b) => a + b.count, 0)} items ā architectural]`,
|
|
1199
|
-
);
|
|
1200
|
-
for (const [rule, { note, count }] of skipRules) {
|
|
1201
|
-
lines.push(` - **${rule}** (${count}): ${note}`);
|
|
1202
|
-
}
|
|
1203
|
-
lines.push("");
|
|
1204
|
-
}
|
|
1205
|
-
|
|
1206
|
-
lines.push("---");
|
|
1207
|
-
lines.push(
|
|
1208
|
-
"Fix the items above in order, then run `/lens-booboo-fix` again for the next iteration.",
|
|
1209
|
-
);
|
|
1210
|
-
lines.push(
|
|
1211
|
-
"If an item is not safe to fix, skip it with one sentence why.",
|
|
1212
|
-
);
|
|
1213
|
-
|
|
1214
|
-
const fixPlan = lines.join("\n");
|
|
1215
|
-
|
|
1216
|
-
// Save plan for reference
|
|
1217
|
-
const planPath = path.join(process.cwd(), ".pi-lens", "fix-plan.md");
|
|
1218
|
-
fs.writeFileSync(
|
|
1219
|
-
planPath,
|
|
1220
|
-
`# Fix Plan ā Iteration ${session.iteration}\n\n${fixPlan}`,
|
|
1221
|
-
"utf-8",
|
|
1222
|
-
);
|
|
1223
|
-
|
|
1224
|
-
// Notify and inject into conversation
|
|
1225
|
-
ctx.ui.notify(`š Fix plan saved: ${planPath}`, "info");
|
|
1226
|
-
// Use steer delivery ā agent is busy processing this tool call
|
|
1227
|
-
// steer interrupts mid-processing with the next fix plan
|
|
1228
|
-
pi.sendUserMessage(fixPlan, { deliverAs: "steer" });
|
|
1229
|
-
},
|
|
264
|
+
"Iterative fix loop: auto-fixes Biome/Ruff, then generates a per-issue plan for agent to execute. Run repeatedly until clean. Usage: /lens-booboo-fix [path] [--reset]",
|
|
265
|
+
handler: (args, ctx) =>
|
|
266
|
+
handleFix(
|
|
267
|
+
args,
|
|
268
|
+
ctx,
|
|
269
|
+
{
|
|
270
|
+
tsClient,
|
|
271
|
+
astGrep: astGrepClient,
|
|
272
|
+
ruff: ruffClient,
|
|
273
|
+
biome: biomeClient,
|
|
274
|
+
knip: knipClient,
|
|
275
|
+
jscpd: jscpdClient,
|
|
276
|
+
complexity: complexityClient,
|
|
277
|
+
},
|
|
278
|
+
pi,
|
|
279
|
+
RULE_ACTIONS,
|
|
280
|
+
),
|
|
1230
281
|
});
|
|
1231
282
|
|
|
1232
283
|
pi.registerCommand("lens-booboo-refactor", {
|
|
1233
284
|
description:
|
|
1234
285
|
"Interactive architectural refactor: scans for worst offender, opens a browser interview with options + recommendation, then steers the agent with your decision. Usage: /lens-booboo-refactor [path]",
|
|
1235
|
-
handler:
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
const metricsByFile = scanComplexityMetrics(complexityClient, targetPath, isTsProject);
|
|
1249
|
-
const architectViolations = architectClient.hasConfig()
|
|
1250
|
-
? scanArchitectViolations(architectClient, targetPath)
|
|
1251
|
-
: new Map<string, string[]>();
|
|
1252
|
-
const scored = scoreFiles(skipByFile, metricsByFile, architectViolations);
|
|
1253
|
-
|
|
1254
|
-
if (scored.length === 0) {
|
|
1255
|
-
ctx.ui.notify("ā
No architectural debt found ā codebase is clean.", "info");
|
|
1256
|
-
return;
|
|
1257
|
-
}
|
|
1258
|
-
|
|
1259
|
-
const { file: worstFile, score } = scored[0];
|
|
1260
|
-
const relFile = path.relative(targetPath, worstFile).replace(/\\/g, "/");
|
|
1261
|
-
const issues = skipByFile.get(worstFile) ?? [];
|
|
1262
|
-
const metrics = metricsByFile.get(worstFile);
|
|
1263
|
-
const archIssues = architectViolations.get(worstFile) ?? [];
|
|
1264
|
-
|
|
1265
|
-
const snippetResult = issues.length > 0 ? extractCodeSnippet(worstFile, issues[0].line) : null;
|
|
1266
|
-
const snippet = snippetResult?.snippet ?? "";
|
|
1267
|
-
const snippetStart = snippetResult?.start ?? 1;
|
|
1268
|
-
const snippetEnd = snippetResult?.end ?? 1;
|
|
1269
|
-
|
|
1270
|
-
// --- Steer agent (agent generates options + calls interviewer tool) ---
|
|
1271
|
-
const ruleGroups = new Map<string, number>();
|
|
1272
|
-
for (const i of issues)
|
|
1273
|
-
ruleGroups.set(i.rule, (ruleGroups.get(i.rule) ?? 0) + 1);
|
|
1274
|
-
|
|
1275
|
-
const issuesSummary = [...ruleGroups.entries()]
|
|
1276
|
-
.map(
|
|
1277
|
-
([r, n]) =>
|
|
1278
|
-
`- \`${r}\` (Ć${n})${RULE_ACTIONS[r] ? ` ā ${RULE_ACTIONS[r].note}` : ""}`,
|
|
1279
|
-
)
|
|
1280
|
-
.join("\n");
|
|
1281
|
-
const archSummary = archIssues.length > 0
|
|
1282
|
-
? archIssues.map((m) => `- ${m}`).join("\n")
|
|
1283
|
-
: "None";
|
|
1284
|
-
const metricsSummary = metrics
|
|
1285
|
-
? `MI: ${metrics.mi.toFixed(1)}, Cognitive: ${metrics.cognitive}, Nesting: ${metrics.nesting}`
|
|
1286
|
-
: "";
|
|
1287
|
-
|
|
1288
|
-
const steer = [
|
|
1289
|
-
`šļø BOOBOO REFACTOR ā worst offender identified`,
|
|
1290
|
-
"",
|
|
1291
|
-
`**File**: \`${relFile}\` (debt score: ${score})`,
|
|
1292
|
-
"",
|
|
1293
|
-
metrics ? `**Complexity**: ${metricsSummary}` : "",
|
|
1294
|
-
"",
|
|
1295
|
-
issues.length > 0 ? `**Violations**:\n${issuesSummary}` : "",
|
|
1296
|
-
archIssues.length > 0 ? `**Architectural rules violated**:\n${archSummary}` : "",
|
|
1297
|
-
"",
|
|
1298
|
-
`**Code** (\`${relFile}\` lines ${snippetStart}ā${snippetEnd}):`,
|
|
1299
|
-
"```typescript",
|
|
1300
|
-
snippet,
|
|
1301
|
-
"```",
|
|
1302
|
-
"",
|
|
1303
|
-
"**Your job**:",
|
|
1304
|
-
"1. Analyze this code ā what's the most impactful refactoring for this file?",
|
|
1305
|
-
"2. Build 3-5 refactoring options. For each, explain *why* it helps and *what* you'd change. Mark one as recommended.",
|
|
1306
|
-
"3. For each option, estimate the impact: linesReduced (number), miProjection (e.g. '3.5 ā 8'), cognitiveProjection (e.g. '1533 ā 1400').",
|
|
1307
|
-
"4. Include an option to skip to the next worst offender.",
|
|
1308
|
-
"5. Call the `interviewer` tool with:",
|
|
1309
|
-
" - `question`: what you're asking the user",
|
|
1310
|
-
" - `options`: array of { value, label, context, recommended, impact: { linesReduced, miProjection, cognitiveProjection } }",
|
|
1311
|
-
"6. The user picks an option or types a free-text response in the browser form.",
|
|
1312
|
-
"7. Implement the refactoring. After changes, run `git diff HEAD~1` to capture what was changed.",
|
|
1313
|
-
"8. Run a complexity scan on the changed file(s) to compute the metrics delta (before vs after MI, cognitive).",
|
|
1314
|
-
"9. Call the `interviewer` tool AGAIN with confirmationMode=true. The plan should contain: what was changed (summary + diff lines), how metrics evolved, and a free-chat option for refinements.",
|
|
1315
|
-
"10. If the user describes changes: make further edits, re-run the scan, call interviewer again with an updated report. Repeat until satisfied.",
|
|
1316
|
-
].join("\n");
|
|
1317
|
-
|
|
1318
|
-
pi.sendUserMessage(steer, { deliverAs: "steer" });
|
|
1319
|
-
},
|
|
286
|
+
handler: (args, ctx) =>
|
|
287
|
+
handleRefactor(
|
|
288
|
+
args,
|
|
289
|
+
ctx,
|
|
290
|
+
{
|
|
291
|
+
astGrep: astGrepClient,
|
|
292
|
+
complexity: complexityClient,
|
|
293
|
+
architect: architectClient,
|
|
294
|
+
},
|
|
295
|
+
pi,
|
|
296
|
+
SKIP_RULES,
|
|
297
|
+
RULE_ACTIONS,
|
|
298
|
+
),
|
|
1320
299
|
});
|
|
1321
300
|
|
|
1322
301
|
pi.registerCommand("lens-metrics", {
|
|
@@ -1326,7 +305,6 @@ export default function (pi: ExtensionAPI) {
|
|
|
1326
305
|
const targetPath = args.trim() || ctx.cwd || process.cwd();
|
|
1327
306
|
ctx.ui.notify("š Measuring code metrics...", "info");
|
|
1328
307
|
|
|
1329
|
-
const fs = require("node:fs");
|
|
1330
308
|
const reviewDir = path.join(process.cwd(), ".pi-lens", "reviews");
|
|
1331
309
|
const timestamp = new Date()
|
|
1332
310
|
.toISOString()
|
|
@@ -1337,38 +315,18 @@ export default function (pi: ExtensionAPI) {
|
|
|
1337
315
|
const results: import("./clients/complexity-client.js").FileComplexity[] =
|
|
1338
316
|
[];
|
|
1339
317
|
|
|
1340
|
-
const
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
const fullPath = path.join(dir, entry.name);
|
|
1350
|
-
if (
|
|
1351
|
-
entry.isDirectory() &&
|
|
1352
|
-
![
|
|
1353
|
-
"node_modules",
|
|
1354
|
-
".git",
|
|
1355
|
-
"dist",
|
|
1356
|
-
"build",
|
|
1357
|
-
".next",
|
|
1358
|
-
".pi-lens",
|
|
1359
|
-
].includes(entry.name)
|
|
1360
|
-
) {
|
|
1361
|
-
scanDir(fullPath);
|
|
1362
|
-
} else if (complexityClient.isSupportedFile(fullPath)) {
|
|
1363
|
-
const metrics = complexityClient.analyzeFile(fullPath);
|
|
1364
|
-
if (metrics) {
|
|
1365
|
-
results.push(metrics);
|
|
1366
|
-
}
|
|
318
|
+
const isTsProject = nodeFs.existsSync(
|
|
319
|
+
path.join(targetPath, "tsconfig.json"),
|
|
320
|
+
);
|
|
321
|
+
const files = getSourceFiles(targetPath, isTsProject);
|
|
322
|
+
for (const fullPath of files) {
|
|
323
|
+
if (complexityClient.isSupportedFile(fullPath)) {
|
|
324
|
+
const metrics = complexityClient.analyzeFile(fullPath);
|
|
325
|
+
if (metrics) {
|
|
326
|
+
results.push(metrics);
|
|
1367
327
|
}
|
|
1368
328
|
}
|
|
1369
|
-
}
|
|
1370
|
-
|
|
1371
|
-
scanDir(targetPath);
|
|
329
|
+
}
|
|
1372
330
|
|
|
1373
331
|
if (results.length === 0) {
|
|
1374
332
|
ctx.ui.notify("No supported files found to analyze", "warning");
|
|
@@ -1405,7 +363,22 @@ export default function (pi: ExtensionAPI) {
|
|
|
1405
363
|
});
|
|
1406
364
|
|
|
1407
365
|
const gradeCount = { A: 0, B: 0, C: 0, D: 0, F: 0 };
|
|
1408
|
-
|
|
366
|
+
for (const g of grades) {
|
|
367
|
+
gradeCount[g.letter as keyof typeof gradeCount]++;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// Capture snapshots for history tracking
|
|
371
|
+
const history = captureSnapshots(
|
|
372
|
+
results.map((r) => ({
|
|
373
|
+
filePath: r.filePath,
|
|
374
|
+
metrics: {
|
|
375
|
+
maintainabilityIndex: r.maintainabilityIndex,
|
|
376
|
+
cognitiveComplexity: r.cognitiveComplexity,
|
|
377
|
+
maxNestingDepth: r.maxNestingDepth,
|
|
378
|
+
linesOfCode: r.linesOfCode,
|
|
379
|
+
},
|
|
380
|
+
})),
|
|
381
|
+
);
|
|
1409
382
|
|
|
1410
383
|
// Build report
|
|
1411
384
|
let report = `# Code Metrics Report: ${projectName}\n\n`;
|
|
@@ -1479,8 +452,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
1479
452
|
|
|
1480
453
|
// All files table (sorted by MI ascending)
|
|
1481
454
|
report += `## All Files\n\n`;
|
|
1482
|
-
report += `| Grade | File | MI | Cognitive |
|
|
1483
|
-
report +=
|
|
455
|
+
report += `| Grade | File | MI | Cognitive | Nesting | LOC | Trend |\n`;
|
|
456
|
+
report += `|-------|------|-----|-----------|---------|-----|-------|\n`;
|
|
1484
457
|
|
|
1485
458
|
const sorted = [...results].sort(
|
|
1486
459
|
(a, b) => a.maintainabilityIndex - b.maintainabilityIndex,
|
|
@@ -1496,11 +469,30 @@ export default function (pi: ExtensionAPI) {
|
|
|
1496
469
|
|
|
1497
470
|
// Make path relative for readability
|
|
1498
471
|
const relPath = path.relative(targetPath, f.filePath);
|
|
472
|
+
const trendCell = formatTrendCell(f.filePath, history);
|
|
1499
473
|
|
|
1500
|
-
report += `| ${grade} | ${relPath} | ${mi.toFixed(1)} | ${f.cognitiveComplexity} | ${f.
|
|
474
|
+
report += `| ${grade} | ${relPath} | ${mi.toFixed(1)} | ${f.cognitiveComplexity} | ${f.maxNestingDepth} | ${f.linesOfCode} | ${trendCell} |\n`;
|
|
1501
475
|
}
|
|
1502
476
|
report += `\n`;
|
|
1503
477
|
|
|
478
|
+
// Trend Summary
|
|
479
|
+
const trendSummary = getTrendSummary(history);
|
|
480
|
+
report += `## Trend Summary\n\n`;
|
|
481
|
+
report += `| Trend | Count |\n`;
|
|
482
|
+
report += `|-------|-------|\n`;
|
|
483
|
+
report += `| š Improving | ${trendSummary.improving} |\n`;
|
|
484
|
+
report += `| ā”ļø Stable | ${trendSummary.stable} |\n`;
|
|
485
|
+
report += `| š Regressing | ${trendSummary.regressing} |\n\n`;
|
|
486
|
+
|
|
487
|
+
if (trendSummary.worstRegressions.length > 0) {
|
|
488
|
+
report += `### Top Regressions\n\n`;
|
|
489
|
+
report += `Files with largest MI decline since last scan:\n\n`;
|
|
490
|
+
for (const r of trendSummary.worstRegressions) {
|
|
491
|
+
report += `- **${r.file}**: MI ${r.miDelta > 0 ? "+" : ""}${r.miDelta}\n`;
|
|
492
|
+
}
|
|
493
|
+
report += `\n`;
|
|
494
|
+
}
|
|
495
|
+
|
|
1504
496
|
// Top 10 worst files (actionable)
|
|
1505
497
|
report += `## Top 10 Files Needing Attention\n\n`;
|
|
1506
498
|
report += `These files have the lowest maintainability scores:\n\n`;
|
|
@@ -1539,14 +531,14 @@ export default function (pi: ExtensionAPI) {
|
|
|
1539
531
|
report += `\n`;
|
|
1540
532
|
|
|
1541
533
|
// Save report
|
|
1542
|
-
|
|
534
|
+
nodeFs.mkdirSync(reviewDir, { recursive: true });
|
|
1543
535
|
|
|
1544
536
|
const reportPath = path.join(reviewDir, `metrics-${timestamp}.md`);
|
|
1545
|
-
|
|
537
|
+
nodeFs.writeFileSync(reportPath, report, "utf-8");
|
|
1546
538
|
|
|
1547
539
|
// Also save latest.md for easy access
|
|
1548
540
|
const latestPath = path.join(reviewDir, "latest.md");
|
|
1549
|
-
|
|
541
|
+
nodeFs.writeFileSync(latestPath, report, "utf-8");
|
|
1550
542
|
|
|
1551
543
|
// Console summary
|
|
1552
544
|
const summary = [
|
|
@@ -1581,32 +573,19 @@ export default function (pi: ExtensionAPI) {
|
|
|
1581
573
|
let formatted = 0;
|
|
1582
574
|
let skipped = 0;
|
|
1583
575
|
|
|
1584
|
-
const
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
return;
|
|
1590
|
-
}
|
|
576
|
+
const targetPath = ctx.cwd || process.cwd();
|
|
577
|
+
const isTsProject = nodeFs.existsSync(
|
|
578
|
+
path.join(targetPath, "tsconfig.json"),
|
|
579
|
+
);
|
|
580
|
+
const files = getSourceFiles(targetPath, isTsProject);
|
|
1591
581
|
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
entry.name,
|
|
1598
|
-
)
|
|
1599
|
-
) {
|
|
1600
|
-
formatDir(fullPath);
|
|
1601
|
-
} else if (/\.(ts|tsx|js|jsx|json|css)$/.test(entry.name)) {
|
|
1602
|
-
const result = biomeClient.formatFile(fullPath);
|
|
1603
|
-
if (result.changed) formatted++;
|
|
1604
|
-
else if (result.success) skipped++;
|
|
1605
|
-
}
|
|
582
|
+
for (const fullPath of files) {
|
|
583
|
+
if (/\.(ts|tsx|js|jsx|json|css)$/.test(fullPath)) {
|
|
584
|
+
const result = biomeClient.formatFile(fullPath);
|
|
585
|
+
if (result.changed) formatted++;
|
|
586
|
+
else if (result.success) skipped++;
|
|
1606
587
|
}
|
|
1607
|
-
}
|
|
1608
|
-
|
|
1609
|
-
formatDir(ctx.cwd || process.cwd());
|
|
588
|
+
}
|
|
1610
589
|
ctx.ui.notify(
|
|
1611
590
|
`ā Formatted ${formatted} file(s), ${skipped} already clean`,
|
|
1612
591
|
"info",
|
|
@@ -1656,10 +635,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
1656
635
|
"yaml",
|
|
1657
636
|
] as const;
|
|
1658
637
|
|
|
1659
|
-
// --- Interviewer tool (browser-based interview with diff confirmation) ---
|
|
638
|
+
// --- Interviewer tool (browser-based interview with diff confirmation) ---
|
|
1660
639
|
buildInterviewer(pi, dbg);
|
|
1661
640
|
|
|
1662
|
-
|
|
1663
641
|
pi.registerTool({
|
|
1664
642
|
name: "ast_grep_search",
|
|
1665
643
|
label: "AST Search",
|
|
@@ -1919,11 +897,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
1919
897
|
|
|
1920
898
|
if (!filePath) return;
|
|
1921
899
|
|
|
1922
|
-
const fs = require("node:fs") as typeof import("node:fs");
|
|
1923
900
|
dbg(
|
|
1924
|
-
`tool_call fired for: ${filePath} (exists: ${
|
|
901
|
+
`tool_call fired for: ${filePath} (exists: ${nodeFs.existsSync(filePath)})`,
|
|
1925
902
|
);
|
|
1926
|
-
if (!
|
|
903
|
+
if (!nodeFs.existsSync(filePath)) return;
|
|
1927
904
|
|
|
1928
905
|
// Record complexity baseline for TS/JS files
|
|
1929
906
|
if (
|
|
@@ -1939,7 +916,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1939
916
|
const hints: string[] = [];
|
|
1940
917
|
|
|
1941
918
|
if (/\.(ts|tsx|js|jsx)$/.test(filePath) && !pi.getFlag("no-lsp")) {
|
|
1942
|
-
tsClient.updateFile(filePath,
|
|
919
|
+
tsClient.updateFile(filePath, nodeFs.readFileSync(filePath, "utf-8"));
|
|
1943
920
|
const diags = tsClient.getDiagnostics(filePath);
|
|
1944
921
|
if (diags.length > 0) {
|
|
1945
922
|
hints.push(
|
|
@@ -2012,9 +989,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
2012
989
|
preWriteHints.delete(filePath);
|
|
2013
990
|
|
|
2014
991
|
// Record write for metrics (silent tracking)
|
|
2015
|
-
|
|
992
|
+
|
|
2016
993
|
try {
|
|
2017
|
-
const content =
|
|
994
|
+
const content = nodeFs.readFileSync(filePath, "utf-8");
|
|
2018
995
|
metricsClient.recordWrite(filePath, content);
|
|
2019
996
|
} catch (err) {
|
|
2020
997
|
void err;
|
|
@@ -2025,7 +1002,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
2025
1002
|
// TypeScript LSP diagnostics
|
|
2026
1003
|
if (!pi.getFlag("no-lsp") && tsClient.isTypeScriptFile(filePath)) {
|
|
2027
1004
|
try {
|
|
2028
|
-
tsClient.updateFile(filePath,
|
|
1005
|
+
tsClient.updateFile(filePath, nodeFs.readFileSync(filePath, "utf-8"));
|
|
2029
1006
|
} catch (err) {
|
|
2030
1007
|
void err;
|
|
2031
1008
|
}
|
|
@@ -2134,24 +1111,27 @@ export default function (pi: ExtensionAPI) {
|
|
|
2134
1111
|
}
|
|
2135
1112
|
|
|
2136
1113
|
// Architectural rule validation (post-write)
|
|
2137
|
-
if (architectClient.hasConfig() &&
|
|
1114
|
+
if (architectClient.hasConfig() && nodeFs.existsSync(filePath)) {
|
|
2138
1115
|
const relPath = path.relative(projectRoot, filePath).replace(/\\/g, "/");
|
|
2139
|
-
const content =
|
|
1116
|
+
const content = nodeFs.readFileSync(filePath, "utf-8");
|
|
2140
1117
|
const lineCount = content.split("\n").length;
|
|
2141
1118
|
|
|
2142
1119
|
// Check for violations
|
|
2143
1120
|
const violations = architectClient.checkFile(relPath, content);
|
|
2144
1121
|
if (violations.length > 0) {
|
|
2145
|
-
lspOutput += `\n\nš“
|
|
1122
|
+
lspOutput += `\n\nš“ STOP ā ${violations.length} architectural violation(s). Fix before continuing:\n`;
|
|
2146
1123
|
for (const v of violations) {
|
|
2147
|
-
|
|
1124
|
+
const lineStr = v.line ? `L${v.line}: ` : "";
|
|
1125
|
+
lspOutput += ` ${lineStr}${v.message}\n`;
|
|
2148
1126
|
}
|
|
1127
|
+
lspOutput += ` ā Refactor the code to comply with the project's architectural rules.\n`;
|
|
2149
1128
|
}
|
|
2150
1129
|
|
|
2151
1130
|
// Check file size limit ā hard stop, file is too large to reason about
|
|
2152
1131
|
const sizeViolation = architectClient.checkFileSize(relPath, lineCount);
|
|
2153
1132
|
if (sizeViolation) {
|
|
2154
|
-
lspOutput += `\n\nš“ STOP ā
|
|
1133
|
+
lspOutput += `\n\nš“ STOP ā Architectural Limit Exceeded:\n`;
|
|
1134
|
+
lspOutput += ` ${sizeViolation.message}\n`;
|
|
2155
1135
|
lspOutput += ` ā Split into smaller, focused modules before adding more code.\n`;
|
|
2156
1136
|
}
|
|
2157
1137
|
}
|
|
@@ -2422,9 +1402,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
2422
1402
|
if (depResult.hasCircular && depResult.circular.length > 0) {
|
|
2423
1403
|
const circularDeps = depResult.circular
|
|
2424
1404
|
.flatMap((d) => d.path)
|
|
2425
|
-
.filter(
|
|
2426
|
-
(p: string) => !filePath.endsWith(require("node:path").basename(p)),
|
|
2427
|
-
);
|
|
1405
|
+
.filter((p: string) => !filePath.endsWith(path.basename(p)));
|
|
2428
1406
|
const uniqueDeps = [...new Set(circularDeps)];
|
|
2429
1407
|
if (uniqueDeps.length > 0) {
|
|
2430
1408
|
lspOutput += `\n\n${depChecker.formatWarning(filePath, uniqueDeps)}`;
|