pi-lens 3.6.2 โ†’ 3.6.4

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 (207) hide show
  1. package/CHANGELOG.md +10 -2
  2. package/package.json +4 -4
  3. package/tsconfig.json +1 -1
  4. package/clients/__tests__/file-time.test.js +0 -216
  5. package/clients/__tests__/file-time.test.ts +0 -276
  6. package/clients/__tests__/format-service.test.js +0 -245
  7. package/clients/__tests__/format-service.test.ts +0 -339
  8. package/clients/__tests__/formatters.test.js +0 -271
  9. package/clients/__tests__/formatters.test.ts +0 -401
  10. package/clients/agent-behavior-client.js +0 -110
  11. package/clients/agent-behavior-client.test.js +0 -94
  12. package/clients/agent-behavior-client.test.ts +0 -116
  13. package/clients/amain-types.js +0 -164
  14. package/clients/architect-client.js +0 -291
  15. package/clients/ast-grep-client.js +0 -253
  16. package/clients/ast-grep-parser.js +0 -84
  17. package/clients/ast-grep-rule-manager.js +0 -89
  18. package/clients/ast-grep-types.js +0 -9
  19. package/clients/auto-loop.js +0 -131
  20. package/clients/biome-client.js +0 -420
  21. package/clients/biome-client.test.js +0 -144
  22. package/clients/biome-client.test.ts +0 -163
  23. package/clients/cache/rule-cache.js +0 -72
  24. package/clients/cache-manager.js +0 -245
  25. package/clients/cache-manager.test.js +0 -197
  26. package/clients/cache-manager.test.ts +0 -299
  27. package/clients/complexity-client.js +0 -675
  28. package/clients/complexity-client.test.js +0 -234
  29. package/clients/complexity-client.test.ts +0 -255
  30. package/clients/config-validator.js +0 -465
  31. package/clients/dependency-checker.js +0 -325
  32. package/clients/dependency-checker.test.js +0 -60
  33. package/clients/dependency-checker.test.ts +0 -71
  34. package/clients/dispatch/__tests__/autofix-integration.test.js +0 -245
  35. package/clients/dispatch/__tests__/autofix-integration.test.ts +0 -300
  36. package/clients/dispatch/__tests__/runner-registration.test.js +0 -234
  37. package/clients/dispatch/__tests__/runner-registration.test.ts +0 -286
  38. package/clients/dispatch/debug.log +0 -1
  39. package/clients/dispatch/dispatcher.edge.test.js +0 -82
  40. package/clients/dispatch/dispatcher.edge.test.ts +0 -100
  41. package/clients/dispatch/dispatcher.format.test.js +0 -46
  42. package/clients/dispatch/dispatcher.format.test.ts +0 -58
  43. package/clients/dispatch/dispatcher.inline.test.js +0 -74
  44. package/clients/dispatch/dispatcher.inline.test.ts +0 -93
  45. package/clients/dispatch/dispatcher.js +0 -381
  46. package/clients/dispatch/dispatcher.test.js +0 -116
  47. package/clients/dispatch/dispatcher.test.ts +0 -149
  48. package/clients/dispatch/integration.js +0 -108
  49. package/clients/dispatch/plan.js +0 -183
  50. package/clients/dispatch/runners/architect.js +0 -83
  51. package/clients/dispatch/runners/architect.test.js +0 -138
  52. package/clients/dispatch/runners/architect.test.ts +0 -162
  53. package/clients/dispatch/runners/ast-grep-napi.js +0 -405
  54. package/clients/dispatch/runners/ast-grep-napi.test.js +0 -107
  55. package/clients/dispatch/runners/ast-grep-napi.test.ts +0 -129
  56. package/clients/dispatch/runners/ast-grep.js +0 -157
  57. package/clients/dispatch/runners/biome.js +0 -55
  58. package/clients/dispatch/runners/config-validation.js +0 -67
  59. package/clients/dispatch/runners/go-vet.js +0 -48
  60. package/clients/dispatch/runners/index.js +0 -47
  61. package/clients/dispatch/runners/lsp.js +0 -102
  62. package/clients/dispatch/runners/oxlint.js +0 -67
  63. package/clients/dispatch/runners/oxlint.test.js +0 -230
  64. package/clients/dispatch/runners/oxlint.test.ts +0 -303
  65. package/clients/dispatch/runners/pyright.js +0 -100
  66. package/clients/dispatch/runners/pyright.test.js +0 -98
  67. package/clients/dispatch/runners/pyright.test.ts +0 -121
  68. package/clients/dispatch/runners/python-slop.js +0 -97
  69. package/clients/dispatch/runners/python-slop.test.js +0 -203
  70. package/clients/dispatch/runners/python-slop.test.ts +0 -298
  71. package/clients/dispatch/runners/ruff.js +0 -48
  72. package/clients/dispatch/runners/rust-clippy.js +0 -102
  73. package/clients/dispatch/runners/scan_codebase.test.js +0 -89
  74. package/clients/dispatch/runners/scan_codebase.test.ts +0 -105
  75. package/clients/dispatch/runners/shellcheck.js +0 -147
  76. package/clients/dispatch/runners/shellcheck.test.js +0 -98
  77. package/clients/dispatch/runners/shellcheck.test.ts +0 -129
  78. package/clients/dispatch/runners/similarity.js +0 -230
  79. package/clients/dispatch/runners/spellcheck.js +0 -106
  80. package/clients/dispatch/runners/spellcheck.test.js +0 -158
  81. package/clients/dispatch/runners/spellcheck.test.ts +0 -214
  82. package/clients/dispatch/runners/tree-sitter.js +0 -246
  83. package/clients/dispatch/runners/ts-lsp.js +0 -125
  84. package/clients/dispatch/runners/ts-slop.js +0 -113
  85. package/clients/dispatch/runners/type-safety.js +0 -142
  86. package/clients/dispatch/runners/utils/diagnostic-parsers.js +0 -134
  87. package/clients/dispatch/runners/utils/runner-helpers.js +0 -115
  88. package/clients/dispatch/runners/utils.js +0 -51
  89. package/clients/dispatch/runners/yaml-rule-parser.js +0 -360
  90. package/clients/dispatch/types.js +0 -16
  91. package/clients/dispatch/utils/format-utils.js +0 -44
  92. package/clients/dogfood.test.js +0 -201
  93. package/clients/dogfood.test.ts +0 -269
  94. package/clients/file-kinds.js +0 -177
  95. package/clients/file-kinds.test.js +0 -169
  96. package/clients/file-kinds.test.ts +0 -210
  97. package/clients/file-time.js +0 -152
  98. package/clients/file-utils.js +0 -40
  99. package/clients/fix-scanners.js +0 -204
  100. package/clients/format-service.js +0 -184
  101. package/clients/formatters.js +0 -488
  102. package/clients/go-client.js +0 -203
  103. package/clients/go-client.test.js +0 -127
  104. package/clients/go-client.test.ts +0 -143
  105. package/clients/installer/index.js +0 -403
  106. package/clients/interviewer-templates.js +0 -75
  107. package/clients/interviewer.js +0 -173
  108. package/clients/jscpd-client.js +0 -196
  109. package/clients/jscpd-client.test.js +0 -127
  110. package/clients/jscpd-client.test.ts +0 -145
  111. package/clients/knip-client.js +0 -239
  112. package/clients/knip-client.test.js +0 -112
  113. package/clients/knip-client.test.ts +0 -128
  114. package/clients/latency-logger.js +0 -40
  115. package/clients/lsp/__tests__/client.test.js +0 -310
  116. package/clients/lsp/__tests__/client.test.ts +0 -412
  117. package/clients/lsp/__tests__/config.test.js +0 -167
  118. package/clients/lsp/__tests__/config.test.ts +0 -217
  119. package/clients/lsp/__tests__/error-recovery.test.js +0 -213
  120. package/clients/lsp/__tests__/error-recovery.test.ts +0 -279
  121. package/clients/lsp/__tests__/integration.test.js +0 -127
  122. package/clients/lsp/__tests__/integration.test.ts +0 -160
  123. package/clients/lsp/__tests__/launch.test.js +0 -313
  124. package/clients/lsp/__tests__/launch.test.ts +0 -394
  125. package/clients/lsp/__tests__/server.test.js +0 -259
  126. package/clients/lsp/__tests__/server.test.ts +0 -332
  127. package/clients/lsp/__tests__/service.test.js +0 -438
  128. package/clients/lsp/__tests__/service.test.ts +0 -530
  129. package/clients/lsp/client.js +0 -350
  130. package/clients/lsp/config.js +0 -112
  131. package/clients/lsp/index.js +0 -318
  132. package/clients/lsp/installer/index.js +0 -391
  133. package/clients/lsp/interactive-install.js +0 -221
  134. package/clients/lsp/language.js +0 -170
  135. package/clients/lsp/launch.js +0 -329
  136. package/clients/lsp/lsp/launch.js +0 -116
  137. package/clients/lsp/lsp/server.js +0 -532
  138. package/clients/lsp/lsp-index.js +0 -10
  139. package/clients/lsp/path-utils.js +0 -5
  140. package/clients/lsp/server.js +0 -725
  141. package/clients/lsp/test-py-spawn/requirements.txt +0 -1
  142. package/clients/lsp/test-py-spawn/test.py +0 -3
  143. package/clients/lsp/test-py-svc/requirements.txt +0 -1
  144. package/clients/lsp/test-py-svc/test.py +0 -3
  145. package/clients/lsp/test-python-project/requirements.txt +0 -1
  146. package/clients/lsp/test-python-project/test.py +0 -5
  147. package/clients/metrics-client.js +0 -107
  148. package/clients/metrics-client.test.js +0 -128
  149. package/clients/metrics-client.test.ts +0 -163
  150. package/clients/metrics-history.js +0 -367
  151. package/clients/path-utils.js +0 -142
  152. package/clients/pipeline.js +0 -272
  153. package/clients/production-readiness.js +0 -522
  154. package/clients/project-index.js +0 -255
  155. package/clients/project-metadata.js +0 -531
  156. package/clients/ruff-client.js +0 -325
  157. package/clients/ruff-client.test.js +0 -132
  158. package/clients/ruff-client.test.ts +0 -153
  159. package/clients/rules-scanner.js +0 -97
  160. package/clients/runner-tracker.js +0 -152
  161. package/clients/rust-client.js +0 -205
  162. package/clients/rust-client.test.js +0 -108
  163. package/clients/rust-client.test.ts +0 -130
  164. package/clients/safe-spawn-async.js +0 -163
  165. package/clients/safe-spawn.js +0 -241
  166. package/clients/sanitize.js +0 -291
  167. package/clients/sanitize.test.js +0 -177
  168. package/clients/sanitize.test.ts +0 -223
  169. package/clients/scan-architectural-debt.js +0 -167
  170. package/clients/scan-utils.js +0 -83
  171. package/clients/secrets-scanner.js +0 -119
  172. package/clients/secrets-scanner.test.js +0 -100
  173. package/clients/secrets-scanner.test.ts +0 -113
  174. package/clients/sg-runner.js +0 -292
  175. package/clients/state-matrix.js +0 -160
  176. package/clients/subprocess-client.js +0 -65
  177. package/clients/symbol-types.js +0 -5
  178. package/clients/test-runner-client.js +0 -523
  179. package/clients/test-runner-client.test.js +0 -192
  180. package/clients/test-runner-client.test.ts +0 -253
  181. package/clients/test-utils.js +0 -27
  182. package/clients/test-utils.ts +0 -36
  183. package/clients/todo-scanner.js +0 -200
  184. package/clients/todo-scanner.test.js +0 -301
  185. package/clients/todo-scanner.test.ts +0 -352
  186. package/clients/tool-availability.js +0 -207
  187. package/clients/tree-sitter-client.js +0 -601
  188. package/clients/tree-sitter-query-loader.js +0 -355
  189. package/clients/tree-sitter-symbol-extractor.js +0 -289
  190. package/clients/ts-service.js +0 -129
  191. package/clients/type-coverage-client.js +0 -127
  192. package/clients/type-coverage-client.test.js +0 -105
  193. package/clients/type-coverage-client.test.ts +0 -125
  194. package/clients/type-safety-client.js +0 -138
  195. package/clients/types.js +0 -11
  196. package/clients/typescript-client.codefix.test.js +0 -157
  197. package/clients/typescript-client.codefix.test.ts +0 -186
  198. package/clients/typescript-client.js +0 -509
  199. package/clients/typescript-client.test.js +0 -105
  200. package/clients/typescript-client.test.ts +0 -126
  201. package/commands/booboo.js +0 -1007
  202. package/commands/fix-from-booboo.js +0 -398
  203. package/commands/fix-simplified.js +0 -618
  204. package/commands/rate.js +0 -281
  205. package/commands/rate.test.js +0 -119
  206. package/commands/rate.test.ts +0 -131
  207. package/commands/refactor.js +0 -130
@@ -1,1007 +0,0 @@
1
- import * as nodeFs from "node:fs";
2
- import * as path from "node:path";
3
- import { EXCLUDED_DIRS, isTestFile } from "../clients/file-utils.js";
4
- import { validateProductionReadiness } from "../clients/production-readiness.js";
5
- import { buildProjectIndex, } from "../clients/project-index.js";
6
- import { detectProjectMetadata, formatProjectMetadata, getAvailableCommands, } from "../clients/project-metadata.js";
7
- import { RunnerTracker } from "../clients/runner-tracker.js";
8
- import { safeSpawn } from "../clients/safe-spawn.js";
9
- import { getSourceFiles } from "../clients/scan-utils.js";
10
- import { calculateSimilarity } from "../clients/state-matrix.js";
11
- import { TreeSitterClient } from "../clients/tree-sitter-client.js";
12
- const getExtensionDir = () => {
13
- if (typeof __dirname !== "undefined") {
14
- return __dirname;
15
- }
16
- return ".";
17
- };
18
- /**
19
- * Centralized test file exclusion for booboo runners.
20
- * Mirrors the dispatch system's skipTestFiles behavior.
21
- */
22
- function shouldIncludeFile(filePath) {
23
- return !isTestFile(filePath);
24
- }
25
- /** Standard test file glob exclusions for CLI tools */
26
- const _TEST_FILE_EXCLUDES = [
27
- "!**/*.test.ts",
28
- "!**/*.test.tsx",
29
- "!**/*.test.js",
30
- "!**/*.test.jsx",
31
- "!**/*.spec.ts",
32
- "!**/*.spec.tsx",
33
- "!**/*.spec.js",
34
- "!**/*.spec.jsx",
35
- "!**/*.poc.test.ts",
36
- "!**/*.poc.test.tsx",
37
- "!**/test-utils.ts",
38
- "!**/test-*.ts",
39
- "!**/__tests__/**",
40
- "!**/tests/**",
41
- "!**/test/**",
42
- ];
43
- export async function handleBooboo(args, ctx, clients, pi) {
44
- const targetPath = args.trim() || ctx.cwd || process.cwd();
45
- // Detect project metadata for richer reporting
46
- const projectMeta = detectProjectMetadata(targetPath);
47
- const _metaDisplay = formatProjectMetadata(projectMeta);
48
- // No noisy notification at start - just run the review silently
49
- // Detect project type once for all runners
50
- const isTsProject = nodeFs.existsSync(path.join(targetPath, "tsconfig.json"));
51
- // Get available commands for the project
52
- const availableCommands = getAvailableCommands(projectMeta);
53
- // Load false positives from fix session to filter them out
54
- const sessionFile = path.join(process.cwd(), ".pi-lens", "fix-session.json");
55
- let falsePositives = [];
56
- try {
57
- const sessionData = JSON.parse(nodeFs.readFileSync(sessionFile, "utf-8") || "{}");
58
- falsePositives = sessionData.falsePositives || [];
59
- }
60
- catch {
61
- // No session file yet
62
- }
63
- // Helper to check if an issue is marked as false positive
64
- const isFalsePositive = (category, file, line) => {
65
- const fpKey = line !== undefined
66
- ? `${category}:${file}:${line}`
67
- : `${category}:${file}`;
68
- return falsePositives.some((fp) => fp === fpKey || fp.startsWith(`${category}:${file}`));
69
- };
70
- // Summary counts for terminal display
71
- const summaryItems = [];
72
- const fullReport = [];
73
- const timestamp = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19);
74
- const reviewDir = path.join(process.cwd(), ".pi-lens", "reviews");
75
- // Initialize runner tracker (no per-runner progress to avoid UI overwriting)
76
- const tracker = new RunnerTracker();
77
- // Helper to format elapsed time
78
- const formatElapsed = (ms) => ms < 1000 ? `${ms}ms` : `${(ms / 1000).toFixed(1)}s`;
79
- // Runner 1: Design smells via ast-grep
80
- await tracker.run("ast-grep (design smells)", async () => {
81
- if (!clients.astGrep.isAvailable()) {
82
- return { findings: 0, status: "skipped" };
83
- }
84
- const configPath = path.join(getExtensionDir(), "..", "rules", "ast-grep-rules", ".sgconfig.yml");
85
- try {
86
- const result = safeSpawn("npx", [
87
- "sg",
88
- "scan",
89
- "--config",
90
- configPath,
91
- "--json",
92
- "--globs",
93
- "!**/*.test.ts",
94
- "--globs",
95
- "!**/*.spec.ts",
96
- "--globs",
97
- "!**/*.poc.test.ts",
98
- "--globs",
99
- "!**/test-utils.ts",
100
- "--globs",
101
- "!**/test-*.ts",
102
- "--globs",
103
- "!**/__tests__/**",
104
- "--globs",
105
- "!**/tests/**",
106
- "--globs",
107
- "!**/.pi-lens/**",
108
- "--globs",
109
- "!**/.pi/**",
110
- "--globs",
111
- "!**/node_modules/**",
112
- "--globs",
113
- "!**/.git/**",
114
- "--globs",
115
- "!**/.ruff_cache/**",
116
- targetPath,
117
- ], {
118
- timeout: 30000,
119
- });
120
- const output = result.stdout || result.stderr || "";
121
- if (output.trim() && result.status !== undefined) {
122
- const issues = [];
123
- const parseItems = (raw) => {
124
- const trimmed = raw.trim();
125
- if (trimmed.startsWith("[")) {
126
- try {
127
- return JSON.parse(trimmed);
128
- }
129
- catch {
130
- return [];
131
- }
132
- }
133
- return raw.split("\n").flatMap((l) => {
134
- try {
135
- return [JSON.parse(l)];
136
- }
137
- catch {
138
- return [];
139
- }
140
- });
141
- };
142
- for (const item of parseItems(output)) {
143
- const ruleId = item.ruleId || item.rule?.title || item.name || "unknown";
144
- const ruleDesc = clients.astGrep.getRuleDescription?.(ruleId);
145
- const message = ruleDesc?.message || item.message || ruleId;
146
- const lineNum = item.labels?.[0]?.range?.start?.line ||
147
- item.spans?.[0]?.range?.start?.line ||
148
- item.range?.start?.line ||
149
- 0;
150
- issues.push({
151
- file: item.file || item.path || targetPath,
152
- line: lineNum + 1,
153
- rule: ruleId,
154
- message: message,
155
- });
156
- }
157
- const filteredIssues = issues.filter((issue) => !isFalsePositive("ast_issues", issue.file, issue.line));
158
- if (filteredIssues.length > 0) {
159
- summaryItems.push({
160
- category: "ast-grep",
161
- count: filteredIssues.length,
162
- severity: filteredIssues.length > 10 ? "๐Ÿ”ด" : "๐ŸŸก",
163
- fixable: true,
164
- });
165
- let fullSection = `## ast-grep (Structural Issues)\n\n**${filteredIssues.length} issue(s) found**\n\n`;
166
- fullSection +=
167
- "| Line | Rule | Message |\n|------|------|--------|\n";
168
- for (const issue of filteredIssues) {
169
- fullSection += `| ${issue.line} | ${issue.rule} | ${issue.message} |\n`;
170
- }
171
- fullSection += "\n### ๐Ÿ’ก How to Fix\n\n";
172
- const seenRules = new Set();
173
- for (const issue of filteredIssues.slice(0, 5)) {
174
- if (seenRules.has(issue.rule))
175
- continue;
176
- seenRules.add(issue.rule);
177
- const ruleDesc = clients.astGrep.getRuleDescription?.(issue.rule);
178
- if (ruleDesc?.note || ruleDesc?.fix) {
179
- fullSection += `**${issue.rule}:**\n`;
180
- if (ruleDesc.note)
181
- fullSection += `${ruleDesc.note}\n\n`;
182
- if (ruleDesc.fix)
183
- fullSection += `Suggested fix:\n\`\`\`typescript\n${ruleDesc.fix}\n\`\`\`\n\n`;
184
- }
185
- }
186
- fullReport.push(fullSection);
187
- }
188
- return { findings: filteredIssues.length, status: "done" };
189
- }
190
- return { findings: 0, status: "done" };
191
- }
192
- catch {
193
- return { findings: 0, status: "error" };
194
- }
195
- });
196
- // Runner 2: Similar functions
197
- await tracker.run("ast-grep (similar functions)", async () => {
198
- if (!clients.astGrep.isAvailable()) {
199
- return { findings: 0, status: "skipped" };
200
- }
201
- const similarGroups = await clients.astGrep.findSimilarFunctions(targetPath, "typescript");
202
- // Filter out test files using centralized exclusion
203
- const filteredGroups = similarGroups
204
- .map((group) => ({
205
- ...group,
206
- functions: group.functions.filter((fn) => shouldIncludeFile(fn.file)),
207
- }))
208
- .filter((group) => group.functions.length > 1); // Need at least 2 non-test functions
209
- if (filteredGroups.length > 0) {
210
- summaryItems.push({
211
- category: "Similar Functions",
212
- count: filteredGroups.length,
213
- severity: "๐ŸŸก",
214
- fixable: true,
215
- });
216
- let fullSection = `## Similar Functions\n\n**${filteredGroups.length} group(s) of structurally similar functions**\n\n`;
217
- for (const group of filteredGroups) {
218
- fullSection += `### Pattern: ${group.functions.map((f) => f.name).join(", ")}\n\n`;
219
- fullSection +=
220
- "| Function | File | Line |\n|----------|------|------|\n";
221
- for (const fn of group.functions) {
222
- fullSection += `| ${fn.name} | ${fn.file} | ${fn.line} |\n`;
223
- }
224
- fullSection += "\n";
225
- }
226
- fullReport.push(fullSection);
227
- }
228
- return { findings: filteredGroups.length, status: "done" };
229
- });
230
- // Runner 3: Semantic similarity
231
- await tracker.run("semantic similarity (Amain)", async () => {
232
- try {
233
- const { glob } = await import("glob");
234
- const sourceFiles = await glob("**/*.ts", {
235
- cwd: targetPath,
236
- ignore: [
237
- "**/node_modules/**",
238
- "**/*.test.ts",
239
- "**/*.test.tsx",
240
- "**/*.spec.ts",
241
- "**/*.spec.tsx",
242
- "**/*.poc.test.ts",
243
- "**/*.poc.test.tsx",
244
- "**/test-utils.ts",
245
- "**/test-*.ts",
246
- "**/__tests__/**",
247
- "**/tests/**",
248
- "**/dist/**",
249
- ],
250
- });
251
- if (sourceFiles.length === 0) {
252
- return { findings: 0, status: "done" };
253
- }
254
- // Filter out test files using centralized exclusion
255
- const absoluteFiles = sourceFiles
256
- .map((f) => path.join(targetPath, f))
257
- .filter(shouldIncludeFile);
258
- const index = await buildProjectIndex(targetPath, absoluteFiles);
259
- const topPairs = findTopSimilarPairs(index, 10);
260
- if (topPairs.length > 0) {
261
- summaryItems.push({
262
- category: "Semantic Duplicates",
263
- count: topPairs.length,
264
- severity: "๐ŸŸก",
265
- fixable: true,
266
- });
267
- let fullSection = `## Semantic Duplicates (Amain Algorithm)\n\n`;
268
- fullSection += `**${topPairs.length} pair(s) with >75% semantic similarity**\n\n`;
269
- fullSection +=
270
- "Functions with different names/variables but similar logic structures.\n\n";
271
- for (const pair of topPairs) {
272
- fullSection += `### ${pair.func1} โ†” ${pair.func2}\n\n`;
273
- fullSection += `- Similarity: **${(pair.similarity * 100).toFixed(1)}%**\n`;
274
- fullSection += `- Consider consolidating or extracting shared logic\n\n`;
275
- }
276
- fullReport.push(fullSection);
277
- }
278
- return { findings: topPairs.length, status: "done" };
279
- }
280
- catch (err) {
281
- console.error("[booboo] Semantic similarity analysis failed:", err);
282
- return { findings: 0, status: "error" };
283
- }
284
- });
285
- // Runner 4: Complexity metrics
286
- await tracker.run("complexity metrics", async () => {
287
- const results = [];
288
- const aiSlopIssues = [];
289
- const files = getSourceFiles(targetPath, isTsProject).filter(shouldIncludeFile);
290
- for (const fullPath of files) {
291
- if (clients.complexity.isSupportedFile(fullPath)) {
292
- const metrics = clients.complexity.analyzeFile(fullPath);
293
- if (metrics) {
294
- results.push(metrics);
295
- // AI slop check - already filtered by shouldIncludeFile above
296
- const warnings = clients.complexity.checkThresholds(metrics);
297
- if (warnings.length > 0) {
298
- aiSlopIssues.push(` ${metrics.filePath}:`);
299
- for (const w of warnings) {
300
- aiSlopIssues.push(` โš  ${w}`);
301
- }
302
- }
303
- }
304
- }
305
- }
306
- if (results.length > 0) {
307
- const avgMI = results.reduce((a, b) => a + b.maintainabilityIndex, 0) /
308
- results.length;
309
- const avgCognitive = results.reduce((a, b) => a + b.cognitiveComplexity, 0) / results.length;
310
- const avgCyclomatic = results.reduce((a, b) => a + b.cyclomaticComplexity, 0) /
311
- results.length;
312
- const maxNesting = Math.max(...results.map((r) => r.maxNestingDepth));
313
- const maxCognitive = Math.max(...results.map((r) => r.cognitiveComplexity));
314
- const minMI = Math.min(...results.map((r) => r.maintainabilityIndex));
315
- // Only flag files with EXTREME issues (tuned to reduce false positives)
316
- // MI < 20 is "critically unmaintainable" (was < 40, too aggressive)
317
- const severeLowMI = results
318
- .filter((r) => r.maintainabilityIndex < 20 && !isTestFile(r.filePath))
319
- .sort((a, b) => a.maintainabilityIndex - b.maintainabilityIndex);
320
- // Cognitive > 80 is extreme (was > 30, flagged too many files)
321
- const veryHighCognitive = results
322
- .filter((r) => r.cognitiveComplexity > 80 && !isTestFile(r.filePath))
323
- .sort((a, b) => b.cognitiveComplexity - a.cognitiveComplexity);
324
- // Deep nesting > 8 levels is extreme (was > 5, normal code hits this)
325
- const deepNesting = results
326
- .filter((r) => r.maxNestingDepth > 8 && !isTestFile(r.filePath))
327
- .sort((a, b) => b.maxNestingDepth - a.maxNestingDepth);
328
- let findings = 0;
329
- if (severeLowMI.length > 0) {
330
- findings += severeLowMI.length;
331
- summaryItems.push({
332
- category: "Low Maintainability",
333
- count: severeLowMI.length,
334
- severity: "๐Ÿ”ด",
335
- fixable: false,
336
- });
337
- }
338
- if (veryHighCognitive.length > 0) {
339
- findings += veryHighCognitive.length;
340
- summaryItems.push({
341
- category: "Very High Complexity",
342
- count: veryHighCognitive.length,
343
- severity: "๐Ÿ”ด",
344
- fixable: true,
345
- });
346
- }
347
- if (deepNesting.length > 0) {
348
- findings += deepNesting.length;
349
- summaryItems.push({
350
- category: "Deep Nesting",
351
- count: deepNesting.length,
352
- severity: "๐ŸŸก",
353
- fixable: true,
354
- });
355
- }
356
- if (aiSlopIssues.length > 0) {
357
- findings += Math.floor(aiSlopIssues.length / 2);
358
- summaryItems.push({
359
- category: "AI Slop",
360
- count: Math.floor(aiSlopIssues.length / 2),
361
- severity: "๐ŸŸก",
362
- fixable: true,
363
- });
364
- }
365
- let fullSection = `## Complexity Metrics\n\n**${results.length} file(s) scanned**\n\n`;
366
- fullSection += `### Summary\n\n| Metric | Value |\n|--------|-------|\n`;
367
- fullSection += `| Avg Maintainability Index | ${avgMI.toFixed(1)} |\n`;
368
- fullSection += `| Min Maintainability Index | ${minMI.toFixed(1)} |\n`;
369
- fullSection += `| Avg Cognitive Complexity | ${avgCognitive.toFixed(1)} |\n`;
370
- fullSection += `| Max Cognitive Complexity | ${maxCognitive} |\n`;
371
- fullSection += `| Avg Cyclomatic Complexity | ${avgCyclomatic.toFixed(1)} |\n`;
372
- fullSection += `| Max Nesting Depth | ${maxNesting} |\n`;
373
- fullSection += `| Total Files | ${results.length} |\n\n`;
374
- // Report severe issues (thresholds match findings count)
375
- if (severeLowMI.length > 0) {
376
- fullSection += `### Low Maintainability (MI < 40)\n\n| File | MI | Cognitive | Cyclomatic | Nesting |\n|------|-----|-----------|------------|--------|\n`;
377
- for (const f of severeLowMI) {
378
- fullSection += `| ${f.filePath} | ${f.maintainabilityIndex.toFixed(1)} | ${f.cognitiveComplexity} | ${f.cyclomaticComplexity} | ${f.maxNestingDepth} |\n`;
379
- }
380
- fullSection += "\n";
381
- }
382
- if (veryHighCognitive.length > 0) {
383
- fullSection += `### Very High Cognitive Complexity (> 30)\n\n| File | Cognitive | MI | Cyclomatic | Nesting |\n|------|-----------|-----|------------|--------|\n`;
384
- for (const f of veryHighCognitive) {
385
- fullSection += `| ${f.filePath} | ${f.cognitiveComplexity} | ${f.maintainabilityIndex.toFixed(1)} | ${f.cyclomaticComplexity} | ${f.maxNestingDepth} |\n`;
386
- }
387
- fullSection += "\n";
388
- }
389
- if (deepNesting.length > 0) {
390
- fullSection += `### Deep Nesting (> 5 levels)\n\n| File | Nesting | Cognitive | MI |\n|------|---------|-----------|-----|\n`;
391
- for (const f of deepNesting) {
392
- fullSection += `| ${f.filePath} | ${f.maxNestingDepth} | ${f.cognitiveComplexity} | ${f.maintainabilityIndex.toFixed(1)} |\n`;
393
- }
394
- fullSection += "\n";
395
- }
396
- // Only show "All Files" table in verbose mode - it's informational noise
397
- if (pi.getFlag("lens-verbose")) {
398
- fullSection += `### All Files\n\n| File | MI | Cognitive | Cyclomatic | Nesting | Entropy |\n|------|-----|-----------|------------|---------|--------|\n`;
399
- for (const f of results.sort((a, b) => a.maintainabilityIndex - b.maintainabilityIndex)) {
400
- fullSection += `| ${f.filePath} | ${f.maintainabilityIndex.toFixed(1)} | ${f.cognitiveComplexity} | ${f.cyclomaticComplexity} | ${f.maxNestingDepth} | ${f.codeEntropy.toFixed(2)} |\n`;
401
- }
402
- fullSection += "\n";
403
- }
404
- if (aiSlopIssues.length > 0) {
405
- fullSection += `### AI Slop Indicators\n\n`;
406
- for (const issue of aiSlopIssues) {
407
- fullSection += `${issue}\n`;
408
- }
409
- fullSection += "\n";
410
- }
411
- fullReport.push(fullSection);
412
- return { findings, status: "done" };
413
- }
414
- return { findings: 0, status: "done" };
415
- });
416
- // Runner 4: Tree-sitter patterns (complementary to ast-grep)
417
- // - Falls back to tree-sitter if ast-grep unavailable
418
- // - Detects patterns ast-grep can't easily do (multi-statement, complex nesting)
419
- // - Captures values for richer reporting
420
- await tracker.run("tree-sitter patterns", async () => {
421
- const client = new TreeSitterClient();
422
- if (!client.isAvailable()) {
423
- return { findings: 0, status: "skipped" };
424
- }
425
- const languageId = isTsProject ? "typescript" : "javascript";
426
- let findings = 0;
427
- const structuralIssues = [];
428
- // Only run basic patterns if ast-grep is NOT available (avoid duplication)
429
- const astGrepAvailable = clients.astGrep.isAvailable();
430
- if (!astGrepAvailable) {
431
- // Fallback: console.log detection (ast-grep normally handles this)
432
- const consoleLogs = await client.structuralSearch("console.$METHOD($MSG)", languageId, targetPath, { maxResults: 30, fileFilter: shouldIncludeFile });
433
- for (const match of consoleLogs) {
434
- const method = match.captures.METHOD || "log";
435
- if (["log", "debug", "info", "warn"].includes(method)) {
436
- structuralIssues.push({
437
- file: match.file,
438
- line: match.line,
439
- pattern: `console.${method}()`,
440
- severity: "๐ŸŸก",
441
- fixable: true,
442
- note: astGrepAvailable
443
- ? undefined
444
- : "(fallback - ast-grep not available)",
445
- });
446
- findings++;
447
- }
448
- }
449
- }
450
- // Pattern 1: Nested promise chains (ast-grep struggles with multi-statement nesting)
451
- // This detects: .then().catch().then() chains that could be async/await
452
- const promiseChains = await client.structuralSearch("$PROMISE.then($$$HANDLER1).catch($$$HANDLER2).then($$$HANDLER3)", languageId, targetPath, { maxResults: 20, fileFilter: shouldIncludeFile });
453
- for (const match of promiseChains) {
454
- structuralIssues.push({
455
- file: match.file,
456
- line: match.line,
457
- pattern: "deep promise chain (3+ levels)",
458
- severity: "๐ŸŸก",
459
- fixable: true,
460
- note: "Consider converting to async/await for readability",
461
- });
462
- findings++;
463
- }
464
- // Pattern 2: Callback pyramids (error-first callbacks nested 3+ levels)
465
- const callbackPyramids = await client.structuralSearch("$FUNC($$$ARGS, ($ERR, $$$PARAMS) => { $$$BODY })", languageId, targetPath, { maxResults: 20, fileFilter: shouldIncludeFile });
466
- // Filter for actual callback nesting (error parameter pattern)
467
- const nestedCallbacks = callbackPyramids.filter((m) => {
468
- const body = m.captures.BODY || "";
469
- // Check if body contains another callback
470
- return body.includes("(") && body.includes("=>");
471
- });
472
- for (const match of nestedCallbacks.slice(0, 10)) {
473
- structuralIssues.push({
474
- file: match.file,
475
- line: match.line,
476
- pattern: "callback pyramid (error-first pattern)",
477
- severity: "๐ŸŸก",
478
- fixable: true,
479
- note: "Consider promisify + async/await",
480
- });
481
- findings++;
482
- }
483
- // Pattern 3: Mixed async patterns (async function + .then() + callback)
484
- // Detects inconsistent async styles in same function
485
- const asyncFunctions = await client.structuralSearch("async function $NAME($$$PARAMS) { $BODY }", languageId, targetPath, { maxResults: 50, fileFilter: shouldIncludeFile });
486
- for (const match of asyncFunctions) {
487
- const body = match.captures.BODY || "";
488
- // Check if async function uses both await and .then()
489
- const hasAwait = body.includes("await");
490
- const hasThen = body.match(/\.\s*then\s*\(/);
491
- if (hasAwait && hasThen) {
492
- structuralIssues.push({
493
- file: match.file,
494
- line: match.line,
495
- pattern: "mixed async/await + promise chains",
496
- severity: "๐ŸŸก",
497
- fixable: true,
498
- note: "Use consistent async style (prefer await)",
499
- });
500
- findings++;
501
- }
502
- }
503
- // Pattern 4: Complex nested if/else (ast-grep can do this, but tree-sitter captures entire block)
504
- const deepIfs = await client.structuralSearch("if ($COND1) { if ($COND2) { if ($COND3) { $$$BODY } } }", languageId, targetPath, { maxResults: 15, fileFilter: shouldIncludeFile });
505
- for (const match of deepIfs) {
506
- structuralIssues.push({
507
- file: match.file,
508
- line: match.line,
509
- pattern: "deeply nested conditionals (3+ levels)",
510
- severity: "๐ŸŸก",
511
- fixable: true,
512
- note: "Consider early returns or guard clauses",
513
- });
514
- findings++;
515
- }
516
- // Add to summary if issues found
517
- if (findings > 0) {
518
- summaryItems.push({
519
- category: astGrepAvailable
520
- ? "Advanced Structural"
521
- : "Structural Patterns (fallback)",
522
- count: findings,
523
- severity: "๐ŸŸก",
524
- fixable: true,
525
- });
526
- // Build detailed report
527
- let fullSection = `## ${astGrepAvailable ? "Advanced Structural" : "Structural Patterns"} (Tree-sitter)\n\n`;
528
- fullSection += `**${findings} issue(s) found**`;
529
- if (!astGrepAvailable) {
530
- fullSection += ` *(ast-grep not available - showing basic + advanced patterns)*`;
531
- }
532
- fullSection += `\n\n`;
533
- // Group by pattern type
534
- const byPattern = {};
535
- for (const issue of structuralIssues) {
536
- if (!byPattern[issue.pattern])
537
- byPattern[issue.pattern] = [];
538
- byPattern[issue.pattern].push(issue);
539
- }
540
- for (const [pattern, issues] of Object.entries(byPattern)) {
541
- fullSection += `### ${pattern} (${issues.length})\n\n`;
542
- fullSection += "| File | Line | Note |\n|------|------|------|\n";
543
- for (const issue of issues.slice(0, 10)) {
544
- fullSection += `| ${issue.file} | ${issue.line} | ${issue.note || ""} |\n`;
545
- }
546
- if (issues.length > 10) {
547
- fullSection += `| ... | ... | ... |\n`;
548
- }
549
- fullSection += "\n";
550
- }
551
- fullReport.push(fullSection);
552
- }
553
- return { findings, status: "done" };
554
- });
555
- // Runner 5: TODOs (cache test edit)
556
- await tracker.run("TODO scanner", async () => {
557
- const todoResult = clients.todo.scanDirectory(targetPath);
558
- if (todoResult.items.length > 0) {
559
- summaryItems.push({
560
- category: "TODOs",
561
- count: todoResult.items.length,
562
- severity: "โ„น๏ธ",
563
- fixable: false,
564
- });
565
- let fullSection = `## TODOs / Annotations\n\n`;
566
- fullSection += `**${todoResult.items.length} annotation(s) found**\n\n`;
567
- fullSection +=
568
- "| Type | File | Line | Text |\n|------|------|------|------|\n";
569
- for (const item of todoResult.items) {
570
- fullSection += `| ${item.type} | ${item.file} | ${item.line} | ${item.message} |\n`;
571
- }
572
- fullSection += "\n";
573
- fullReport.push(fullSection);
574
- }
575
- return { findings: todoResult.items.length, status: "done" };
576
- });
577
- // Runner 6: Dead code
578
- await tracker.run("dead code (Knip)", async () => {
579
- if (!clients.knip.isAvailable()) {
580
- return { findings: 0, status: "skipped" };
581
- }
582
- // Exclude test files from Knip analysis
583
- const knipResult = clients.knip.analyze(targetPath, [
584
- "**/*.test.ts",
585
- "**/*.test.tsx",
586
- "**/*.test.js",
587
- "**/*.spec.ts",
588
- "**/*.spec.tsx",
589
- "**/*.spec.js",
590
- "**/*.poc.test.ts",
591
- "**/*.poc.test.tsx",
592
- "**/__tests__/**",
593
- "**/tests/**",
594
- ]);
595
- // Filter out test file issues as additional safeguard
596
- const filteredIssues = knipResult.issues.filter((issue) => !issue.file || shouldIncludeFile(issue.file));
597
- if (filteredIssues.length > 0) {
598
- summaryItems.push({
599
- category: "Dead Code",
600
- count: filteredIssues.length,
601
- severity: "๐ŸŸก",
602
- fixable: true,
603
- });
604
- let fullSection = `## Dead Code (Knip)\n\n`;
605
- fullSection += `**${filteredIssues.length} issue(s) found**\n\n`;
606
- fullSection += "| Type | Name | File |\n|------|------|------|\n";
607
- for (const issue of filteredIssues) {
608
- fullSection += `| ${issue.type} | ${issue.name} | ${issue.file ?? ""} |\n`;
609
- }
610
- fullSection += "\n";
611
- fullReport.push(fullSection);
612
- }
613
- return { findings: filteredIssues.length, status: "done" };
614
- });
615
- // Runner 7: Duplicate code
616
- await tracker.run("duplicate code (jscpd)", async () => {
617
- if (!clients.jscpd.isAvailable()) {
618
- return { findings: 0, status: "skipped" };
619
- }
620
- // In TS projects, exclude .js files (they're compiled artifacts)
621
- const jscpdResult = clients.jscpd.scan(targetPath, 5, 50, isTsProject);
622
- // Filter out test file duplicates using centralized exclusion
623
- const filteredClones = jscpdResult.clones.filter((dup) => shouldIncludeFile(dup.fileA) && shouldIncludeFile(dup.fileB));
624
- if (filteredClones.length > 0) {
625
- summaryItems.push({
626
- category: "Duplicates",
627
- count: filteredClones.length,
628
- severity: "๐ŸŸก",
629
- fixable: true,
630
- });
631
- let fullSection = `## Code Duplication (jscpd)\n\n`;
632
- fullSection += `**${filteredClones.length} duplicate block(s) found** (${jscpdResult.duplicatedLines}/${jscpdResult.totalLines} lines, ${jscpdResult.percentage.toFixed(1)}%)\n\n`;
633
- fullSection +=
634
- "| File A | Line A | File B | Line B | Lines | Tokens |\n|--------|--------|--------|--------|-------|--------|\n";
635
- for (const dup of filteredClones) {
636
- fullSection += `| ${dup.fileA} | ${dup.startA} | ${dup.fileB} | ${dup.startB} | ${dup.lines} | ${dup.tokens} |\n`;
637
- }
638
- fullSection += "\n";
639
- fullReport.push(fullSection);
640
- }
641
- return { findings: filteredClones.length, status: "done" };
642
- });
643
- // Runner 8: Type coverage
644
- await tracker.run("type coverage", async () => {
645
- if (!clients.typeCoverage.isAvailable()) {
646
- return { findings: 0, status: "skipped" };
647
- }
648
- const tcResult = clients.typeCoverage.scan(targetPath);
649
- if (tcResult.percentage < 100) {
650
- // Filter out test file locations using centralized exclusion
651
- const filteredLocations = tcResult.untypedLocations.filter((u) => shouldIncludeFile(u.file));
652
- const filesWithLowCoverage = new Set(filteredLocations
653
- .filter(() => tcResult.percentage < 90)
654
- .map((u) => u.file)).size;
655
- summaryItems.push({
656
- category: "Type Coverage",
657
- count: filesWithLowCoverage || 1,
658
- severity: tcResult.percentage < 90 ? "๐ŸŸก" : "โ„น๏ธ",
659
- fixable: false,
660
- });
661
- let fullSection = `## Type Coverage\n\n**${tcResult.percentage.toFixed(1)}% typed** (${tcResult.typed}/${tcResult.total} identifiers)\n\n`;
662
- const byFile = {};
663
- for (const u of filteredLocations) {
664
- byFile[u.file] = (byFile[u.file] || 0) + 1;
665
- }
666
- const sortedFiles = Object.entries(byFile)
667
- .filter(([file]) => shouldIncludeFile(file))
668
- .sort((a, b) => b[1] - a[1])
669
- .slice(0, 10);
670
- if (sortedFiles.length > 0) {
671
- fullSection += `### Top Files by Untyped Count\n\n| File | Untyped Count |\n|------|---------------|\n`;
672
- for (const [file, count] of sortedFiles) {
673
- fullSection += `| ${file} | ${count} |\n`;
674
- }
675
- if (Object.keys(byFile).length > 10) {
676
- fullSection += `| ... | +${Object.keys(byFile).length - 10} more files |\n`;
677
- }
678
- }
679
- fullSection += "\n";
680
- fullReport.push(fullSection);
681
- return { findings: filesWithLowCoverage || 1, status: "done" };
682
- }
683
- return { findings: 0, status: "done" };
684
- });
685
- // Runner 9: Circular deps
686
- await tracker.run("circular deps (Madge)", async () => {
687
- if (pi.getFlag("no-madge") || !clients.depChecker.isAvailable()) {
688
- return { findings: 0, status: "skipped" };
689
- }
690
- const { circular } = clients.depChecker.scanProject(targetPath);
691
- // Filter out circular deps involving only test files using centralized exclusion
692
- const filteredCircular = circular.filter((dep) => {
693
- // Keep if ANY file in the chain is not a test file
694
- return dep.path.some((file) => shouldIncludeFile(file));
695
- });
696
- if (filteredCircular.length > 0) {
697
- summaryItems.push({
698
- category: "Circular Deps",
699
- count: filteredCircular.length,
700
- severity: "๐Ÿ”ด",
701
- fixable: false,
702
- });
703
- let fullSection = `## Circular Dependencies (Madge)\n\n`;
704
- fullSection += `**${filteredCircular.length} circular chain(s) found**\n\n`;
705
- for (const dep of filteredCircular) {
706
- fullSection += `- ${dep.path.join(" โ†’ ")}\n`;
707
- }
708
- fullReport.push(`${fullSection}\n`);
709
- }
710
- return { findings: filteredCircular.length, status: "done" };
711
- });
712
- // Runner 10: Arch rules
713
- await tracker.run("architectural rules", async () => {
714
- if (!clients.architect.hasConfig()) {
715
- clients.architect.loadConfig(process.cwd());
716
- }
717
- if (!clients.architect.hasConfig()) {
718
- return { findings: 0, status: "skipped" };
719
- }
720
- // Detect TypeScript project - skip .js files in TS projects (compiled artifacts)
721
- const isTsProject = nodeFs.existsSync(path.join(targetPath, "tsconfig.json"));
722
- const archViolations = [];
723
- const archScanDir = (dir) => {
724
- for (const entry of nodeFs.readdirSync(dir, { withFileTypes: true })) {
725
- const full = path.join(dir, entry.name);
726
- if (entry.isDirectory()) {
727
- if (EXCLUDED_DIRS.includes(entry.name))
728
- continue;
729
- archScanDir(full);
730
- }
731
- else if (/\.(ts|tsx|js|jsx|py|go|rs)$/.test(entry.name)) {
732
- if (isTestFile(full))
733
- continue;
734
- // In TS projects, skip .js files (they're compiled artifacts)
735
- if (isTsProject &&
736
- /\.(js|jsx)$/.test(entry.name) &&
737
- nodeFs.existsSync(full.replace(/\.(js|jsx)$/, ".ts")))
738
- continue;
739
- const relPath = path.relative(targetPath, full).replace(/\\/g, "/");
740
- const content = nodeFs.readFileSync(full, "utf-8");
741
- const lineCount = content.split("\n").length;
742
- for (const v of clients.architect.checkFile(relPath, content)) {
743
- archViolations.push({ file: relPath, message: v.message });
744
- }
745
- const sizeV = clients.architect.checkFileSize(relPath, lineCount);
746
- if (sizeV)
747
- archViolations.push({ file: relPath, message: sizeV.message });
748
- }
749
- }
750
- };
751
- archScanDir(targetPath);
752
- if (archViolations.length > 0) {
753
- summaryItems.push({
754
- category: "Architectural",
755
- count: archViolations.length,
756
- severity: "๐Ÿ”ด",
757
- fixable: false,
758
- });
759
- let fullSection = `## Architectural Rules\n\n`;
760
- fullSection += `**${archViolations.length} violation(s) found**\n\n`;
761
- for (const v of archViolations) {
762
- fullSection += `- **${v.file}**: ${v.message}\n`;
763
- }
764
- fullReport.push(`${fullSection}\n`);
765
- }
766
- return { findings: archViolations.length, status: "done" };
767
- });
768
- // Runner 11: Production Readiness (inspired by pi-validate)
769
- await tracker.run("production readiness", async () => {
770
- const readiness = validateProductionReadiness(targetPath);
771
- // Add to summary if not perfect
772
- if (readiness.overallScore < 100) {
773
- const severity = readiness.grade === "A"
774
- ? "๐ŸŸข"
775
- : readiness.grade === "B"
776
- ? "๐ŸŸข"
777
- : readiness.grade === "C"
778
- ? "๐ŸŸก"
779
- : "๐ŸŸ ";
780
- // Count issues across all categories
781
- const totalIssues_ = Object.values(readiness.categories).reduce((sum, cat) => sum + cat.issues.length, 0);
782
- if (totalIssues_ > 0) {
783
- summaryItems.push({
784
- category: "Production Readiness",
785
- count: totalIssues_,
786
- severity: severity,
787
- fixable: true,
788
- });
789
- }
790
- }
791
- // Add to full report
792
- let section = `## Production Readiness\n\n`;
793
- section += `**Score:** ${readiness.overallScore}/100 **Grade:** ${readiness.grade}\n\n`;
794
- for (const [key, cat] of Object.entries(readiness.categories)) {
795
- section += `### ${key.charAt(0).toUpperCase() + key.slice(1)} (${cat.score}/100)\n\n`;
796
- if (cat.details.length > 0) {
797
- for (const detail of cat.details) {
798
- section += `- ${detail}\n`;
799
- }
800
- }
801
- if (cat.issues.length > 0) {
802
- for (const issue of cat.issues) {
803
- section += `- โš ๏ธ ${issue}\n`;
804
- }
805
- }
806
- if (cat.details.length === 0 && cat.issues.length === 0) {
807
- section += `- โœ… No issues\n`;
808
- }
809
- section += "\n";
810
- }
811
- fullReport.push(section);
812
- // Add metadata to report
813
- const criticalIssues = [];
814
- for (const [key, cat] of Object.entries(readiness.categories)) {
815
- for (const issue of cat.issues) {
816
- // Flag critical issues
817
- if (key === "code" && issue.includes("debugger")) {
818
- criticalIssues.push(`[CRITICAL] ${issue}`);
819
- }
820
- else if (key === "tests" && cat.score < 50) {
821
- criticalIssues.push(`[CRITICAL] No tests found`);
822
- }
823
- }
824
- }
825
- return {
826
- findings: Object.values(readiness.categories).reduce((sum, cat) => sum + cat.issues.length, 0),
827
- status: "done",
828
- };
829
- });
830
- // --- Create structured JSON report ---
831
- nodeFs.mkdirSync(reviewDir, { recursive: true });
832
- const projectName = path.basename(process.cwd());
833
- const totalIssues = summaryItems.reduce((sum, s) => sum + s.count, 0);
834
- const fixableCount = summaryItems
835
- .filter((s) => s.fixable)
836
- .reduce((sum, s) => sum + s.count, 0);
837
- const refactorNeeded = summaryItems
838
- .filter((s) => !s.fixable)
839
- .reduce((sum, s) => sum + s.count, 0);
840
- // Build runner summary
841
- const runnerSummary = tracker.getRunners().map((r) => ({
842
- name: r.name,
843
- status: r.status,
844
- findings: r.findings,
845
- time: formatElapsed(r.elapsedMs),
846
- }));
847
- const jsonReport = {
848
- meta: {
849
- timestamp: new Date().toISOString(),
850
- project: projectName,
851
- path: targetPath,
852
- totalIssues,
853
- fixableCount,
854
- refactorNeeded,
855
- // New: runner execution details
856
- runners: runnerSummary,
857
- totalTime: formatElapsed(runnerSummary.reduce((sum, r) => {
858
- const ms = r.time.endsWith("ms")
859
- ? parseInt(r.time, 10)
860
- : parseFloat(r.time) * 1000;
861
- return sum + (Number.isNaN(ms) ? 0 : ms);
862
- }, 0)),
863
- },
864
- // New: project metadata
865
- project: {
866
- type: projectMeta.type,
867
- name: projectMeta.name,
868
- version: projectMeta.version,
869
- packageManager: projectMeta.packageManager,
870
- languages: projectMeta.languages,
871
- hasTests: projectMeta.hasTests,
872
- testFramework: projectMeta.testFramework,
873
- hasLinting: projectMeta.hasLinting,
874
- linter: projectMeta.linter,
875
- hasFormatting: projectMeta.hasFormatting,
876
- formatter: projectMeta.formatter,
877
- hasTypeScript: projectMeta.hasTypeScript,
878
- configFiles: projectMeta.configFiles,
879
- scripts: projectMeta.scripts,
880
- },
881
- // New: available commands for the project
882
- commands: availableCommands,
883
- byCategory: summaryItems.reduce((acc, item) => {
884
- acc[item.category] = {
885
- count: item.count,
886
- severity: item.severity,
887
- fixable: item.fixable,
888
- falsePositivePrefix: `${item.category.toLowerCase().replace(/\s+/g, "-")}:`,
889
- };
890
- return acc;
891
- }, {}),
892
- howToMarkFalsePositive: {
893
- command: "Ignore via AGENTS.md rules or suppress comments",
894
- format: "Add to .claude/rules or use biome/oxlint ignore comments",
895
- examples: [
896
- "// biome-ignore lint/suspicious/noConsole: intentional debug",
897
- "// oxlint-disable-next-line no-console",
898
- ],
899
- },
900
- sessionFile: path.join(process.cwd(), ".pi-lens", "fix-session.json"),
901
- details: fullReport.join("\n"),
902
- };
903
- const jsonPath = path.join(reviewDir, `booboo-${timestamp}.json`);
904
- nodeFs.writeFileSync(jsonPath, JSON.stringify(jsonReport, null, 2), "utf-8");
905
- // --- Create markdown report ---
906
- // Build project info section
907
- let projectSection = `## Project Info\n\n**Type:** ${projectMeta.type}`;
908
- if (projectMeta.name)
909
- projectSection += ` | **Name:** ${projectMeta.name}`;
910
- if (projectMeta.version)
911
- projectSection += ` | **Version:** ${projectMeta.version}`;
912
- if (projectMeta.packageManager)
913
- projectSection += `\n**Package Manager:** ${projectMeta.packageManager}`;
914
- if (projectMeta.languages.length > 0)
915
- projectSection += `\n**Languages:** ${projectMeta.languages.join(", ")}`;
916
- // Tools
917
- const tools = [];
918
- if (projectMeta.testFramework)
919
- tools.push(`๐Ÿงช ${projectMeta.testFramework}`);
920
- else if (projectMeta.hasTests)
921
- tools.push("๐Ÿงช tests");
922
- if (projectMeta.linter)
923
- tools.push(`๐Ÿ” ${projectMeta.linter}`);
924
- if (projectMeta.formatter)
925
- tools.push(`โœจ ${projectMeta.formatter}`);
926
- if (tools.length > 0)
927
- projectSection += `\n**Tools:** ${tools.join(" | ")}`;
928
- // Available commands
929
- if (availableCommands.length > 0) {
930
- projectSection += `\n\n### Available Commands\n\n| Action | Command |\n|--------|---------|`;
931
- for (const cmd of availableCommands) {
932
- projectSection += `\n| ${cmd.action} | \`${cmd.command}\` |`;
933
- }
934
- }
935
- const mdReport = `# Code Review: ${projectName}
936
-
937
- **Scanned:** ${jsonReport.meta.timestamp}
938
- **Path:** \`${targetPath}\`
939
- **Summary:** ${jsonReport.meta.totalIssues} issues | ${jsonReport.meta.fixableCount} fixable | ${jsonReport.meta.refactorNeeded} need refactor
940
- **Total Time:** ${jsonReport.meta.totalTime}
941
-
942
- ${projectSection}
943
-
944
- ## Runner Summary
945
-
946
- | Runner | Status | Findings | Time |
947
- |--------|--------|----------|------|
948
- ${runnerSummary.map((r) => `| ${r.name} | ${r.status} | ${r.findings} | ${r.time} |`).join("\n")}
949
-
950
- ---
951
-
952
- ${fullReport.join("\n")}`;
953
- const mdPath = path.join(reviewDir, `booboo-${timestamp}.md`);
954
- nodeFs.writeFileSync(mdPath, mdReport, "utf-8");
955
- // --- Brief terminal summary ---
956
- if (summaryItems.length === 0) {
957
- ctx.ui.notify("โœ“ Code review clean", "info");
958
- }
959
- else {
960
- const { totalIssues, fixableCount, refactorNeeded } = jsonReport.meta;
961
- // Build runner lines for terminal output
962
- const runnerLines = tracker
963
- .getRunners()
964
- .filter((r) => r.findings > 0)
965
- .map((r) => ` ${r.status === "error" ? "โœ—" : "โš "} ${r.name}: ${r.findings} finding${r.findings !== 1 ? "s" : ""} (${formatElapsed(r.elapsedMs)})`);
966
- const summaryLines = [
967
- `๐Ÿ“Š Code Review: ${totalIssues} issues`,
968
- ...runnerLines,
969
- ` โฑ๏ธ Total: ${jsonReport.meta.totalTime}`,
970
- `๐Ÿ“„ MD: ${mdPath}`,
971
- ];
972
- ctx.ui.notify(summaryLines.join("\n"), "info");
973
- }
974
- }
975
- /**
976
- * Find top N most similar function pairs in the project index
977
- * Uses canonical pair ordering to avoid duplicates (A,B) vs (B,A)
978
- */
979
- function findTopSimilarPairs(index, maxPairs) {
980
- const entries = Array.from(index.entries.values());
981
- const seenPairs = new Set();
982
- const pairs = [];
983
- for (let i = 0; i < entries.length; i++) {
984
- for (let j = i + 1; j < entries.length; j++) {
985
- const entry1 = entries[i];
986
- const entry2 = entries[j];
987
- // Skip if same file (we want cross-file duplicates)
988
- if (entry1.filePath === entry2.filePath)
989
- continue;
990
- const similarity = calculateSimilarity(entry1.matrix, entry2.matrix);
991
- if (similarity >= 0.75) {
992
- // Canonical pair key (sorted to avoid duplicates)
993
- const pairKey = [entry1.id, entry2.id].sort().join("::");
994
- if (seenPairs.has(pairKey))
995
- continue;
996
- seenPairs.add(pairKey);
997
- pairs.push({
998
- func1: entry1.id,
999
- func2: entry2.id,
1000
- similarity,
1001
- });
1002
- }
1003
- }
1004
- }
1005
- // Sort by similarity descending, take top N
1006
- return pairs.sort((a, b) => b.similarity - a.similarity).slice(0, maxPairs);
1007
- }