pi-lens 2.1.0 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/README.md +70 -1
  3. package/clients/ast-grep-client.js +12 -12
  4. package/clients/ast-grep-client.ts +21 -11
  5. package/clients/dispatch/dispatcher.js +2 -2
  6. package/clients/dispatch/dispatcher.ts +2 -2
  7. package/clients/dispatch/runners/index.js +3 -1
  8. package/clients/dispatch/runners/index.ts +3 -1
  9. package/clients/dispatch/runners/pyright.js +68 -0
  10. package/clients/dispatch/runners/pyright.test.js +84 -0
  11. package/clients/dispatch/runners/pyright.test.ts +109 -0
  12. package/clients/dispatch/runners/pyright.ts +102 -0
  13. package/clients/dispatch/runners/secrets.js +109 -0
  14. package/clients/secrets-scanner.js +113 -0
  15. package/clients/secrets-scanner.test.js +100 -0
  16. package/clients/secrets-scanner.test.ts +113 -0
  17. package/clients/secrets-scanner.ts +134 -0
  18. package/clients/sg-runner.js +15 -2
  19. package/clients/sg-runner.ts +25 -2
  20. package/commands/fix.js +48 -50
  21. package/commands/fix.ts +71 -61
  22. package/commands/rate.js +285 -0
  23. package/commands/rate.test.js +119 -0
  24. package/commands/rate.test.ts +131 -0
  25. package/commands/rate.ts +348 -0
  26. package/commands/refactor.js +33 -9
  27. package/commands/refactor.ts +44 -11
  28. package/default-architect.yaml +7 -0
  29. package/index.ts +58 -10
  30. package/package.json +1 -1
  31. package/rules/ast-grep-rules/rules/no-default-export.yml +19 -0
  32. package/rules/ast-grep-rules/rules/no-hardcoded-secrets.yml +9 -6
  33. package/rules/ast-grep-rules/rules/no-process-env.yml +12 -12
  34. package/rules/ast-grep-rules/rules/no-relative-imports.yml +21 -0
@@ -0,0 +1,119 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { formatRateResult } from "./rate.js";
3
+ // Test the formatting functions directly with mock data
4
+ describe("formatRateResult", () => {
5
+ it("should format a visual score breakdown", () => {
6
+ const result = {
7
+ overall: 75,
8
+ categories: [
9
+ { name: "Type Safety", score: 85, icon: "šŸ”·", issues: [] },
10
+ { name: "Complexity", score: 70, icon: "🧩", issues: [] },
11
+ { name: "Security", score: 100, icon: "šŸ”’", issues: [] },
12
+ { name: "Architecture", score: 85, icon: "šŸ—ļø", issues: [] },
13
+ { name: "Dead Code", score: 100, icon: "šŸ—‘ļø", issues: [] },
14
+ { name: "Tests", score: 100, icon: "āœ…", issues: [] },
15
+ ],
16
+ };
17
+ const output = formatRateResult(result);
18
+ expect(output).toContain("CODE QUALITY SCORE");
19
+ expect(output).toContain("75/100");
20
+ expect(output).toContain("Type Safety");
21
+ expect(output).toContain("Security");
22
+ expect(output).toContain("Tests");
23
+ });
24
+ it("should show correct grade for A", () => {
25
+ const result = {
26
+ overall: 95,
27
+ categories: Array(6).fill({
28
+ name: "Test",
29
+ score: 95,
30
+ icon: "āœ…",
31
+ issues: [],
32
+ }),
33
+ };
34
+ const output = formatRateResult(result);
35
+ expect(output).toContain("A");
36
+ });
37
+ it("should show correct grade for B", () => {
38
+ const result = {
39
+ overall: 85,
40
+ categories: Array(6).fill({
41
+ name: "Test",
42
+ score: 85,
43
+ icon: "āœ…",
44
+ issues: [],
45
+ }),
46
+ };
47
+ const output = formatRateResult(result);
48
+ expect(output).toContain("B");
49
+ });
50
+ it("should show correct grade for C", () => {
51
+ const result = {
52
+ overall: 75,
53
+ categories: Array(6).fill({
54
+ name: "Test",
55
+ score: 75,
56
+ icon: "āœ…",
57
+ issues: [],
58
+ }),
59
+ };
60
+ const output = formatRateResult(result);
61
+ expect(output).toContain("C");
62
+ });
63
+ it("should show issues section when there are problems", () => {
64
+ const result = {
65
+ overall: 50,
66
+ categories: [
67
+ {
68
+ name: "Type Safety",
69
+ score: 50,
70
+ icon: "šŸ”·",
71
+ issues: ["50 untyped identifiers"],
72
+ },
73
+ {
74
+ name: "Complexity",
75
+ score: 50,
76
+ icon: "🧩",
77
+ issues: ["High complexity: foo.ts"],
78
+ },
79
+ { name: "Security", score: 100, icon: "šŸ”’", issues: [] },
80
+ { name: "Architecture", score: 100, icon: "šŸ—ļø", issues: [] },
81
+ { name: "Dead Code", score: 100, icon: "šŸ—‘ļø", issues: [] },
82
+ { name: "Tests", score: 100, icon: "āœ…", issues: [] },
83
+ ],
84
+ };
85
+ const output = formatRateResult(result);
86
+ expect(output).toContain("Issues to address");
87
+ expect(output).toContain("Type Safety");
88
+ expect(output).toContain("/lens-booboo");
89
+ });
90
+ it("should not show issues section when clean", () => {
91
+ const result = {
92
+ overall: 100,
93
+ categories: Array(6).fill({
94
+ name: "Test",
95
+ score: 100,
96
+ icon: "āœ…",
97
+ issues: [],
98
+ }),
99
+ };
100
+ const output = formatRateResult(result);
101
+ expect(output).not.toContain("Issues to address");
102
+ });
103
+ it("should use colored bars based on score", () => {
104
+ const resultHigh = {
105
+ overall: 90,
106
+ categories: [{ name: "Test", score: 85, icon: "āœ…", issues: [] }],
107
+ };
108
+ const resultLow = {
109
+ overall: 50,
110
+ categories: [{ name: "Test", score: 50, icon: "āœ…", issues: [] }],
111
+ };
112
+ const outputHigh = formatRateResult(resultHigh);
113
+ const outputLow = formatRateResult(resultLow);
114
+ // High score should have green squares
115
+ expect(outputHigh).toContain("🟩");
116
+ // Low score should have red squares
117
+ expect(outputLow).toContain("🟄");
118
+ });
119
+ });
@@ -0,0 +1,131 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { formatRateResult } from "./rate.js";
3
+
4
+ // Test the formatting functions directly with mock data
5
+
6
+ describe("formatRateResult", () => {
7
+ it("should format a visual score breakdown", () => {
8
+ const result = {
9
+ overall: 75,
10
+ categories: [
11
+ { name: "Type Safety", score: 85, icon: "šŸ”·", issues: [] },
12
+ { name: "Complexity", score: 70, icon: "🧩", issues: [] },
13
+ { name: "Security", score: 100, icon: "šŸ”’", issues: [] },
14
+ { name: "Architecture", score: 85, icon: "šŸ—ļø", issues: [] },
15
+ { name: "Dead Code", score: 100, icon: "šŸ—‘ļø", issues: [] },
16
+ { name: "Tests", score: 100, icon: "āœ…", issues: [] },
17
+ ],
18
+ };
19
+
20
+ const output = formatRateResult(result);
21
+
22
+ expect(output).toContain("CODE QUALITY SCORE");
23
+ expect(output).toContain("75/100");
24
+ expect(output).toContain("Type Safety");
25
+ expect(output).toContain("Security");
26
+ expect(output).toContain("Tests");
27
+ });
28
+
29
+ it("should show correct grade for A", () => {
30
+ const result = {
31
+ overall: 95,
32
+ categories: Array(6).fill({
33
+ name: "Test",
34
+ score: 95,
35
+ icon: "āœ…",
36
+ issues: [],
37
+ }),
38
+ };
39
+ const output = formatRateResult(result);
40
+ expect(output).toContain("A");
41
+ });
42
+
43
+ it("should show correct grade for B", () => {
44
+ const result = {
45
+ overall: 85,
46
+ categories: Array(6).fill({
47
+ name: "Test",
48
+ score: 85,
49
+ icon: "āœ…",
50
+ issues: [],
51
+ }),
52
+ };
53
+ const output = formatRateResult(result);
54
+ expect(output).toContain("B");
55
+ });
56
+
57
+ it("should show correct grade for C", () => {
58
+ const result = {
59
+ overall: 75,
60
+ categories: Array(6).fill({
61
+ name: "Test",
62
+ score: 75,
63
+ icon: "āœ…",
64
+ issues: [],
65
+ }),
66
+ };
67
+ const output = formatRateResult(result);
68
+ expect(output).toContain("C");
69
+ });
70
+
71
+ it("should show issues section when there are problems", () => {
72
+ const result = {
73
+ overall: 50,
74
+ categories: [
75
+ {
76
+ name: "Type Safety",
77
+ score: 50,
78
+ icon: "šŸ”·",
79
+ issues: ["50 untyped identifiers"],
80
+ },
81
+ {
82
+ name: "Complexity",
83
+ score: 50,
84
+ icon: "🧩",
85
+ issues: ["High complexity: foo.ts"],
86
+ },
87
+ { name: "Security", score: 100, icon: "šŸ”’", issues: [] },
88
+ { name: "Architecture", score: 100, icon: "šŸ—ļø", issues: [] },
89
+ { name: "Dead Code", score: 100, icon: "šŸ—‘ļø", issues: [] },
90
+ { name: "Tests", score: 100, icon: "āœ…", issues: [] },
91
+ ],
92
+ };
93
+ const output = formatRateResult(result);
94
+ expect(output).toContain("Issues to address");
95
+ expect(output).toContain("Type Safety");
96
+ expect(output).toContain("/lens-booboo");
97
+ });
98
+
99
+ it("should not show issues section when clean", () => {
100
+ const result = {
101
+ overall: 100,
102
+ categories: Array(6).fill({
103
+ name: "Test",
104
+ score: 100,
105
+ icon: "āœ…",
106
+ issues: [],
107
+ }),
108
+ };
109
+ const output = formatRateResult(result);
110
+ expect(output).not.toContain("Issues to address");
111
+ });
112
+
113
+ it("should use colored bars based on score", () => {
114
+ const resultHigh = {
115
+ overall: 90,
116
+ categories: [{ name: "Test", score: 85, icon: "āœ…", issues: [] }],
117
+ };
118
+ const resultLow = {
119
+ overall: 50,
120
+ categories: [{ name: "Test", score: 50, icon: "āœ…", issues: [] }],
121
+ };
122
+
123
+ const outputHigh = formatRateResult(resultHigh);
124
+ const outputLow = formatRateResult(resultLow);
125
+
126
+ // High score should have green squares
127
+ expect(outputHigh).toContain("🟩");
128
+ // Low score should have red squares
129
+ expect(outputLow).toContain("🟄");
130
+ });
131
+ });
@@ -0,0 +1,348 @@
1
+ /**
2
+ * /lens-rate command
3
+ *
4
+ * Provides a visual scoring breakdown of code quality across multiple dimensions.
5
+ * Uses existing scan data to calculate scores.
6
+ */
7
+
8
+ import * as childProcess from "node:child_process";
9
+ import * as nodeFs from "node:fs";
10
+ import * as path from "node:path";
11
+ import type { ExtensionContext } from "@mariozechner/pi-coding-agent";
12
+ import type { ArchitectClient } from "../clients/architect-client.js";
13
+ import type { ComplexityClient } from "../clients/complexity-client.js";
14
+ import type { KnipClient } from "../clients/knip-client.js";
15
+ import { getSourceFiles } from "../clients/scan-utils.js";
16
+ import type { TypeCoverageClient } from "../clients/type-coverage-client.js";
17
+
18
+ interface CategoryScore {
19
+ name: string;
20
+ score: number; // 0-100
21
+ icon: string;
22
+ issues: string[];
23
+ }
24
+
25
+ interface RateResult {
26
+ overall: number;
27
+ categories: CategoryScore[];
28
+ }
29
+
30
+ interface ScanClients {
31
+ complexity: ComplexityClient;
32
+ knip: KnipClient;
33
+ typeCoverage: TypeCoverageClient;
34
+ architect: ArchitectClient;
35
+ }
36
+
37
+ /**
38
+ * Run all scans and calculate scores
39
+ */
40
+ export async function gatherScores(
41
+ targetPath: string,
42
+ clients: ScanClients,
43
+ ): Promise<RateResult> {
44
+ const isTsProject = nodeFs.existsSync(path.join(targetPath, "tsconfig.json"));
45
+ const files = getSourceFiles(targetPath, isTsProject);
46
+ const categories: CategoryScore[] = [];
47
+
48
+ // ─── Type Safety ───
49
+ let typeCoverageScore = 100;
50
+ const typeIssues: string[] = [];
51
+
52
+ if (clients.typeCoverage.isAvailable()) {
53
+ const result = clients.typeCoverage.scan(targetPath);
54
+ if (result.success) {
55
+ typeCoverageScore = result.percentage;
56
+ if (result.percentage < 90) {
57
+ typeIssues.push(`${result.total - result.typed} untyped identifiers`);
58
+ }
59
+ }
60
+ }
61
+ categories.push({
62
+ name: "Type Safety",
63
+ score: Math.round(typeCoverageScore),
64
+ icon: "šŸ”·",
65
+ issues: typeIssues,
66
+ });
67
+
68
+ // ─── Complexity ───
69
+ let complexityScore = 100;
70
+ const complexityIssues: string[] = [];
71
+
72
+ let totalScore = 0;
73
+ let fileCount = 0;
74
+ let worstFile = "";
75
+ let worstScore = 100;
76
+
77
+ for (const file of files.slice(0, 50)) {
78
+ if (clients.complexity.isSupportedFile(file)) {
79
+ const metrics = clients.complexity.analyzeFile(file);
80
+ if (metrics) {
81
+ totalScore += metrics.maintainabilityIndex;
82
+ fileCount++;
83
+ if (metrics.maintainabilityIndex < worstScore) {
84
+ worstScore = metrics.maintainabilityIndex;
85
+ worstFile = path.basename(file);
86
+ }
87
+ }
88
+ }
89
+ }
90
+ if (fileCount > 0) {
91
+ complexityScore = totalScore / fileCount;
92
+ if (complexityScore < 70) {
93
+ complexityIssues.push(`High complexity: ${worstFile}`);
94
+ }
95
+ }
96
+ categories.push({
97
+ name: "Complexity",
98
+ score: Math.round(complexityScore),
99
+ icon: "🧩",
100
+ issues: complexityIssues,
101
+ });
102
+
103
+ // ─── Security ───
104
+ let securityScore = 100;
105
+ const securityIssues: string[] = [];
106
+ let secretsFound = 0;
107
+
108
+ // Check for secrets in source files
109
+ const secretPatterns = [
110
+ { name: "API Key (sk-)", pattern: /sk-[a-zA-Z0-9]{20,}/ },
111
+ { name: "GitHub Token", pattern: /ghp_[a-zA-Z0-9]{36}/ },
112
+ { name: "AWS Key", pattern: /AKIA[A-Z0-9]{16}/ },
113
+ { name: "Anthropic Key", pattern: /sk-ant-[a-zA-Z0-9]{20,}/ },
114
+ { name: "OpenAI Key", pattern: /sk-proj-[a-zA-Z0-9]{20,}/ },
115
+ {
116
+ name: "Private Key",
117
+ pattern: /-----BEGIN (?:RSA |EC |DSA )?PRIVATE KEY-----/,
118
+ },
119
+ ];
120
+
121
+ for (const file of files.slice(0, 100)) {
122
+ try {
123
+ const content = nodeFs.readFileSync(file, "utf-8");
124
+ for (const line of content.split("\n")) {
125
+ if (line.trim().startsWith("//") || line.trim().startsWith("#"))
126
+ continue;
127
+ for (const { name, pattern } of secretPatterns) {
128
+ if (pattern.test(line)) {
129
+ secretsFound++;
130
+ if (securityIssues.length < 3) {
131
+ securityIssues.push(`${name} in ${path.basename(file)}`);
132
+ }
133
+ }
134
+ }
135
+ }
136
+ } catch {
137
+ // Skip unreadable files
138
+ }
139
+ }
140
+ securityScore = Math.max(0, 100 - secretsFound * 15);
141
+ categories.push({
142
+ name: "Security",
143
+ score: securityScore,
144
+ icon: "šŸ”’",
145
+ issues: securityIssues,
146
+ });
147
+
148
+ // ─── Architecture ───
149
+ let archScore = 100;
150
+ const archIssues: string[] = [];
151
+
152
+ clients.architect.loadConfig(targetPath);
153
+ if (clients.architect.hasConfig()) {
154
+ let archViolations = 0;
155
+ const scanDir = (dir: string) => {
156
+ for (const entry of nodeFs.readdirSync(dir, { withFileTypes: true })) {
157
+ const full = path.join(dir, entry.name);
158
+ if (entry.isDirectory()) {
159
+ if (
160
+ [
161
+ "node_modules",
162
+ ".git",
163
+ "dist",
164
+ "build",
165
+ ".next",
166
+ ".pi-lens",
167
+ ].includes(entry.name)
168
+ )
169
+ continue;
170
+ scanDir(full);
171
+ } else if (/\.(ts|tsx|js|jsx|py|go|rs)$/.test(entry.name)) {
172
+ const relPath = path.relative(targetPath, full).replace(/\\/g, "/");
173
+ const content = nodeFs.readFileSync(full, "utf-8");
174
+ const violations = clients.architect.checkFile(relPath, content);
175
+ archViolations += violations.length;
176
+ if (violations.length > 0 && archIssues.length < 3) {
177
+ archIssues.push(`${violations.length} in ${path.basename(full)}`);
178
+ }
179
+ const sizeV = clients.architect.checkFileSize(
180
+ relPath,
181
+ content.split("\n").length,
182
+ );
183
+ if (sizeV) archViolations++;
184
+ }
185
+ }
186
+ };
187
+ scanDir(targetPath);
188
+ archScore = Math.max(0, 100 - archViolations * 10);
189
+ }
190
+ categories.push({
191
+ name: "Architecture",
192
+ score: archScore,
193
+ icon: "šŸ—ļø",
194
+ issues: archIssues,
195
+ });
196
+
197
+ // ─── Dead Code ───
198
+ let deadCodeScore = 100;
199
+ const deadCodeIssues: string[] = [];
200
+
201
+ if (clients.knip.isAvailable()) {
202
+ const result = clients.knip.analyze(targetPath);
203
+ if (result.success) {
204
+ const unusedExports = result.unusedExports.length;
205
+ const unusedFiles = result.unusedFiles.length;
206
+ const total = unusedExports + unusedFiles;
207
+ deadCodeScore = Math.max(0, 100 - total * 3);
208
+ if (unusedExports > 0) {
209
+ deadCodeIssues.push(`${unusedExports} unused export(s)`);
210
+ }
211
+ if (unusedFiles > 0) {
212
+ deadCodeIssues.push(`${unusedFiles} unused file(s)`);
213
+ }
214
+ }
215
+ }
216
+ categories.push({
217
+ name: "Dead Code",
218
+ score: deadCodeScore,
219
+ icon: "šŸ—‘ļø",
220
+ issues: deadCodeIssues,
221
+ });
222
+
223
+ // ─── Tests ───
224
+ let testScore = 100;
225
+ const testIssues: string[] = [];
226
+
227
+ // Quick test run
228
+ try {
229
+ const testResult = childProcess.spawnSync(
230
+ "npx",
231
+ ["vitest", "run", "--reporter=basic"],
232
+ {
233
+ encoding: "utf-8",
234
+ timeout: 60000,
235
+ shell: true,
236
+ cwd: targetPath,
237
+ },
238
+ );
239
+ if (testResult.status !== 0) {
240
+ const output = (testResult.stdout || "") + (testResult.stderr || "");
241
+ if (output.includes("failed")) {
242
+ // Count failing tests
243
+ const failMatch = output.match(/(\d+) failed/);
244
+ testScore = 50;
245
+ testIssues.push(
246
+ failMatch ? `${failMatch[1]} test(s) failing` : "Some tests failing",
247
+ );
248
+ } else {
249
+ testScore = 70;
250
+ testIssues.push("Tests timed out or errored");
251
+ }
252
+ }
253
+ } catch {
254
+ testScore = 70;
255
+ testIssues.push("Could not run tests");
256
+ }
257
+ categories.push({
258
+ name: "Tests",
259
+ score: testScore,
260
+ icon: "āœ…",
261
+ issues: testIssues,
262
+ });
263
+
264
+ // ─── Calculate Overall ───
265
+ const overall = Math.round(
266
+ categories.reduce((sum, c) => sum + c.score, 0) / categories.length,
267
+ );
268
+
269
+ return { overall, categories };
270
+ }
271
+
272
+ /**
273
+ * Format score as a bar
274
+ */
275
+ function scoreBar(score: number, width = 10): string {
276
+ const filled = Math.round((score / 100) * width);
277
+ const empty = width - filled;
278
+ const color = score >= 80 ? "🟩" : score >= 60 ? "🟨" : "🟄";
279
+ return color.repeat(filled) + "⬜".repeat(empty);
280
+ }
281
+
282
+ /**
283
+ * Get grade from score
284
+ */
285
+ function getGrade(score: number): string {
286
+ if (score >= 90) return "A";
287
+ if (score >= 80) return "B";
288
+ if (score >= 70) return "C";
289
+ if (score >= 60) return "D";
290
+ return "F";
291
+ }
292
+
293
+ /**
294
+ * Format rate result for terminal
295
+ */
296
+ export function formatRateResult(result: RateResult): string {
297
+ const lines: string[] = [];
298
+
299
+ lines.push("ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”");
300
+ const gradeText = ` (${getGrade(result.overall)})`;
301
+ const scoreText = `šŸ“Š CODE QUALITY SCORE: ${result.overall}/100${gradeText}`;
302
+ const padding = Math.max(0, 55 - scoreText.length);
303
+ lines.push(`│ ${scoreText}${" ".repeat(padding)}│`);
304
+ lines.push("ā”œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¤");
305
+
306
+ for (const cat of result.categories) {
307
+ const name = cat.name.padEnd(14);
308
+ const bar = scoreBar(cat.score);
309
+ const score = String(cat.score).padStart(3);
310
+ lines.push(`│ ${cat.icon} ${name} ${bar} ${score} │`);
311
+ }
312
+
313
+ lines.push("ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜");
314
+
315
+ // Show issues if any
316
+ const allIssues = result.categories
317
+ .filter((c) => c.issues.length > 0)
318
+ .flatMap((c) => c.issues.map((i) => `${c.icon} ${c.name}: ${i}`));
319
+
320
+ if (allIssues.length > 0) {
321
+ lines.push("");
322
+ lines.push("Issues to address:");
323
+ for (const issue of allIssues.slice(0, 5)) {
324
+ lines.push(` • ${issue}`);
325
+ }
326
+ if (allIssues.length > 5) {
327
+ lines.push(` ... and ${allIssues.length - 5} more`);
328
+ }
329
+ lines.push("");
330
+ lines.push("šŸ’” Run /lens-booboo for full details");
331
+ }
332
+
333
+ return lines.join("\n");
334
+ }
335
+
336
+ /**
337
+ * Handle /lens-rate command
338
+ */
339
+ export async function handleRate(
340
+ args: string,
341
+ ctx: ExtensionContext,
342
+ clients: ScanClients,
343
+ ): Promise<string> {
344
+ const targetPath = args.trim() || ctx.cwd || process.cwd();
345
+ ctx.ui.notify("šŸ“Š Calculating code quality scores...", "info");
346
+ const result = await gatherScores(targetPath, clients);
347
+ return formatRateResult(result);
348
+ }
@@ -1,7 +1,7 @@
1
1
  import * as nodeFs from "node:fs";
2
2
  import * as path from "node:path";
3
3
  import { createAutoLoop } from "../clients/auto-loop.js";
4
- import { extractCodeSnippet, scanArchitectViolations, scanComplexityMetrics, scanSkipViolations, scoreFiles, } from "../clients/scan-architectural-debt.js";
4
+ import { scanArchitectViolations, scanComplexityMetrics, scanSkipViolations, scoreFiles, } from "../clients/scan-architectural-debt.js";
5
5
  // Auto-loop singleton for refactor command (initialized at module load)
6
6
  let refactorLoop = null;
7
7
  export function initRefactorLoop(pi) {
@@ -49,15 +49,28 @@ export async function handleRefactor(args, ctx, clients, pi, skipRules, ruleActi
49
49
  ctx.ui.notify("āœ… No architectural debt found — codebase is clean.", "info");
50
50
  return;
51
51
  }
52
+ // --- Write ranked list to TSV for agent reference ---
53
+ const reportDir = path.join(process.cwd(), ".pi-lens", "reports");
54
+ nodeFs.mkdirSync(reportDir, { recursive: true });
55
+ const reportPath = path.join(reportDir, "refactor-ranked.tsv");
56
+ const tsvRows = [
57
+ "rank\tfile\tscore\tmi\tcognitive\tnesting\tviolations",
58
+ ];
59
+ scored.slice(0, 50).forEach((f, i) => {
60
+ const m = metricsByFile.get(f.file);
61
+ const skipCount = skipByFile.get(f.file)?.length ?? 0;
62
+ const archCount = architectViolations?.get(f.file)?.length ?? 0;
63
+ const totalViolations = skipCount + archCount;
64
+ const relPath = path.relative(targetPath, f.file).replace(/\\/g, "/");
65
+ tsvRows.push(`${i + 1}\t${relPath}\t${f.score}\t${m?.mi.toFixed(1) ?? "-"}\t${m?.cognitive ?? "-"}\t${m?.nesting ?? "-"}\t${totalViolations}`);
66
+ });
67
+ nodeFs.writeFileSync(reportPath, tsvRows.join("\n"), "utf-8");
68
+ // --- Current worst offender ---
52
69
  const { file: worstFile, score } = scored[0];
53
70
  const relFile = path.relative(targetPath, worstFile).replace(/\\/g, "/");
54
71
  const issues = skipByFile.get(worstFile) ?? [];
55
72
  const metrics = metricsByFile.get(worstFile);
56
73
  const archIssues = architectViolations.get(worstFile) ?? [];
57
- const snippetResult = issues.length > 0 ? extractCodeSnippet(worstFile, issues[0].line) : null;
58
- const snippet = snippetResult?.snippet ?? "";
59
- const snippetStart = snippetResult?.start ?? 1;
60
- const snippetEnd = snippetResult?.end ?? 1;
61
74
  const ruleGroups = new Map();
62
75
  for (const i of issues)
63
76
  ruleGroups.set(i.rule, (ruleGroups.get(i.rule) ?? 0) + 1);
@@ -68,6 +81,19 @@ export async function handleRefactor(args, ctx, clients, pi, skipRules, ruleActi
68
81
  const metricsSummary = metrics
69
82
  ? `MI: ${metrics.mi.toFixed(1)}, Cognitive: ${metrics.cognitive}, Nesting: ${metrics.nesting}`
70
83
  : "";
84
+ // First violation line for quick reference
85
+ const firstViolationLine = issues.length > 0 ? issues[0].line : null;
86
+ // --- Compact terminal summary ---
87
+ const topFiles = scored
88
+ .slice(0, 5)
89
+ .map((f, i) => {
90
+ const name = path.relative(targetPath, f.file).replace(/\\/g, "/");
91
+ return ` ${i + 1}. ${name} (score: ${f.score})`;
92
+ })
93
+ .join("\n");
94
+ ctx.ui.notify(`šŸ—ļø Worst: ${relFile} (score: ${score}) — ${scored.length} files with debt`, "info");
95
+ console.log(`\nšŸ“Š Top ${Math.min(scored.length, 5)} worst offenders:\n${topFiles}\nšŸ“„ Full ranked list: .pi-lens/reports/refactor-ranked.tsv\n`);
96
+ // --- Steer message for agent ---
71
97
  const steer = [
72
98
  `šŸ—ļø BOOBOO REFACTOR — worst offender identified`,
73
99
  "",
@@ -79,11 +105,9 @@ export async function handleRefactor(args, ctx, clients, pi, skipRules, ruleActi
79
105
  archIssues.length > 0
80
106
  ? `**Architectural rules violated**:\n${archSummary}`
81
107
  : "",
108
+ firstViolationLine ? `First violation at line ${firstViolationLine}` : "",
82
109
  "",
83
- `**Code** (\`${relFile}\` lines ${snippetStart}–${snippetEnd}):`,
84
- "```typescript",
85
- snippet,
86
- "```",
110
+ `šŸ“„ Full details: .pi-lens/reports/refactor-ranked.tsv — read \`${relFile}\` when ready`,
87
111
  "",
88
112
  "**Your job**:",
89
113
  "1. Analyze this code — what's the most impactful refactoring for this file?",