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.
- package/CHANGELOG.md +10 -2
- package/package.json +4 -4
- package/tsconfig.json +1 -1
- package/clients/__tests__/file-time.test.js +0 -216
- package/clients/__tests__/file-time.test.ts +0 -276
- package/clients/__tests__/format-service.test.js +0 -245
- package/clients/__tests__/format-service.test.ts +0 -339
- package/clients/__tests__/formatters.test.js +0 -271
- package/clients/__tests__/formatters.test.ts +0 -401
- package/clients/agent-behavior-client.js +0 -110
- package/clients/agent-behavior-client.test.js +0 -94
- package/clients/agent-behavior-client.test.ts +0 -116
- package/clients/amain-types.js +0 -164
- package/clients/architect-client.js +0 -291
- package/clients/ast-grep-client.js +0 -253
- package/clients/ast-grep-parser.js +0 -84
- package/clients/ast-grep-rule-manager.js +0 -89
- package/clients/ast-grep-types.js +0 -9
- package/clients/auto-loop.js +0 -131
- package/clients/biome-client.js +0 -420
- package/clients/biome-client.test.js +0 -144
- package/clients/biome-client.test.ts +0 -163
- package/clients/cache/rule-cache.js +0 -72
- package/clients/cache-manager.js +0 -245
- package/clients/cache-manager.test.js +0 -197
- package/clients/cache-manager.test.ts +0 -299
- package/clients/complexity-client.js +0 -675
- package/clients/complexity-client.test.js +0 -234
- package/clients/complexity-client.test.ts +0 -255
- package/clients/config-validator.js +0 -465
- package/clients/dependency-checker.js +0 -325
- package/clients/dependency-checker.test.js +0 -60
- package/clients/dependency-checker.test.ts +0 -71
- package/clients/dispatch/__tests__/autofix-integration.test.js +0 -245
- package/clients/dispatch/__tests__/autofix-integration.test.ts +0 -300
- package/clients/dispatch/__tests__/runner-registration.test.js +0 -234
- package/clients/dispatch/__tests__/runner-registration.test.ts +0 -286
- package/clients/dispatch/debug.log +0 -1
- package/clients/dispatch/dispatcher.edge.test.js +0 -82
- package/clients/dispatch/dispatcher.edge.test.ts +0 -100
- package/clients/dispatch/dispatcher.format.test.js +0 -46
- package/clients/dispatch/dispatcher.format.test.ts +0 -58
- package/clients/dispatch/dispatcher.inline.test.js +0 -74
- package/clients/dispatch/dispatcher.inline.test.ts +0 -93
- package/clients/dispatch/dispatcher.js +0 -381
- package/clients/dispatch/dispatcher.test.js +0 -116
- package/clients/dispatch/dispatcher.test.ts +0 -149
- package/clients/dispatch/integration.js +0 -108
- package/clients/dispatch/plan.js +0 -183
- package/clients/dispatch/runners/architect.js +0 -83
- package/clients/dispatch/runners/architect.test.js +0 -138
- package/clients/dispatch/runners/architect.test.ts +0 -162
- package/clients/dispatch/runners/ast-grep-napi.js +0 -405
- package/clients/dispatch/runners/ast-grep-napi.test.js +0 -107
- package/clients/dispatch/runners/ast-grep-napi.test.ts +0 -129
- package/clients/dispatch/runners/ast-grep.js +0 -157
- package/clients/dispatch/runners/biome.js +0 -55
- package/clients/dispatch/runners/config-validation.js +0 -67
- package/clients/dispatch/runners/go-vet.js +0 -48
- package/clients/dispatch/runners/index.js +0 -47
- package/clients/dispatch/runners/lsp.js +0 -102
- package/clients/dispatch/runners/oxlint.js +0 -67
- package/clients/dispatch/runners/oxlint.test.js +0 -230
- package/clients/dispatch/runners/oxlint.test.ts +0 -303
- package/clients/dispatch/runners/pyright.js +0 -100
- package/clients/dispatch/runners/pyright.test.js +0 -98
- package/clients/dispatch/runners/pyright.test.ts +0 -121
- package/clients/dispatch/runners/python-slop.js +0 -97
- package/clients/dispatch/runners/python-slop.test.js +0 -203
- package/clients/dispatch/runners/python-slop.test.ts +0 -298
- package/clients/dispatch/runners/ruff.js +0 -48
- package/clients/dispatch/runners/rust-clippy.js +0 -102
- package/clients/dispatch/runners/scan_codebase.test.js +0 -89
- package/clients/dispatch/runners/scan_codebase.test.ts +0 -105
- package/clients/dispatch/runners/shellcheck.js +0 -147
- package/clients/dispatch/runners/shellcheck.test.js +0 -98
- package/clients/dispatch/runners/shellcheck.test.ts +0 -129
- package/clients/dispatch/runners/similarity.js +0 -230
- package/clients/dispatch/runners/spellcheck.js +0 -106
- package/clients/dispatch/runners/spellcheck.test.js +0 -158
- package/clients/dispatch/runners/spellcheck.test.ts +0 -214
- package/clients/dispatch/runners/tree-sitter.js +0 -246
- package/clients/dispatch/runners/ts-lsp.js +0 -125
- package/clients/dispatch/runners/ts-slop.js +0 -113
- package/clients/dispatch/runners/type-safety.js +0 -142
- package/clients/dispatch/runners/utils/diagnostic-parsers.js +0 -134
- package/clients/dispatch/runners/utils/runner-helpers.js +0 -115
- package/clients/dispatch/runners/utils.js +0 -51
- package/clients/dispatch/runners/yaml-rule-parser.js +0 -360
- package/clients/dispatch/types.js +0 -16
- package/clients/dispatch/utils/format-utils.js +0 -44
- package/clients/dogfood.test.js +0 -201
- package/clients/dogfood.test.ts +0 -269
- package/clients/file-kinds.js +0 -177
- package/clients/file-kinds.test.js +0 -169
- package/clients/file-kinds.test.ts +0 -210
- package/clients/file-time.js +0 -152
- package/clients/file-utils.js +0 -40
- package/clients/fix-scanners.js +0 -204
- package/clients/format-service.js +0 -184
- package/clients/formatters.js +0 -488
- package/clients/go-client.js +0 -203
- package/clients/go-client.test.js +0 -127
- package/clients/go-client.test.ts +0 -143
- package/clients/installer/index.js +0 -403
- package/clients/interviewer-templates.js +0 -75
- package/clients/interviewer.js +0 -173
- package/clients/jscpd-client.js +0 -196
- package/clients/jscpd-client.test.js +0 -127
- package/clients/jscpd-client.test.ts +0 -145
- package/clients/knip-client.js +0 -239
- package/clients/knip-client.test.js +0 -112
- package/clients/knip-client.test.ts +0 -128
- package/clients/latency-logger.js +0 -40
- package/clients/lsp/__tests__/client.test.js +0 -310
- package/clients/lsp/__tests__/client.test.ts +0 -412
- package/clients/lsp/__tests__/config.test.js +0 -167
- package/clients/lsp/__tests__/config.test.ts +0 -217
- package/clients/lsp/__tests__/error-recovery.test.js +0 -213
- package/clients/lsp/__tests__/error-recovery.test.ts +0 -279
- package/clients/lsp/__tests__/integration.test.js +0 -127
- package/clients/lsp/__tests__/integration.test.ts +0 -160
- package/clients/lsp/__tests__/launch.test.js +0 -313
- package/clients/lsp/__tests__/launch.test.ts +0 -394
- package/clients/lsp/__tests__/server.test.js +0 -259
- package/clients/lsp/__tests__/server.test.ts +0 -332
- package/clients/lsp/__tests__/service.test.js +0 -438
- package/clients/lsp/__tests__/service.test.ts +0 -530
- package/clients/lsp/client.js +0 -350
- package/clients/lsp/config.js +0 -112
- package/clients/lsp/index.js +0 -318
- package/clients/lsp/installer/index.js +0 -391
- package/clients/lsp/interactive-install.js +0 -221
- package/clients/lsp/language.js +0 -170
- package/clients/lsp/launch.js +0 -329
- package/clients/lsp/lsp/launch.js +0 -116
- package/clients/lsp/lsp/server.js +0 -532
- package/clients/lsp/lsp-index.js +0 -10
- package/clients/lsp/path-utils.js +0 -5
- package/clients/lsp/server.js +0 -725
- package/clients/lsp/test-py-spawn/requirements.txt +0 -1
- package/clients/lsp/test-py-spawn/test.py +0 -3
- package/clients/lsp/test-py-svc/requirements.txt +0 -1
- package/clients/lsp/test-py-svc/test.py +0 -3
- package/clients/lsp/test-python-project/requirements.txt +0 -1
- package/clients/lsp/test-python-project/test.py +0 -5
- package/clients/metrics-client.js +0 -107
- package/clients/metrics-client.test.js +0 -128
- package/clients/metrics-client.test.ts +0 -163
- package/clients/metrics-history.js +0 -367
- package/clients/path-utils.js +0 -142
- package/clients/pipeline.js +0 -272
- package/clients/production-readiness.js +0 -522
- package/clients/project-index.js +0 -255
- package/clients/project-metadata.js +0 -531
- package/clients/ruff-client.js +0 -325
- package/clients/ruff-client.test.js +0 -132
- package/clients/ruff-client.test.ts +0 -153
- package/clients/rules-scanner.js +0 -97
- package/clients/runner-tracker.js +0 -152
- package/clients/rust-client.js +0 -205
- package/clients/rust-client.test.js +0 -108
- package/clients/rust-client.test.ts +0 -130
- package/clients/safe-spawn-async.js +0 -163
- package/clients/safe-spawn.js +0 -241
- package/clients/sanitize.js +0 -291
- package/clients/sanitize.test.js +0 -177
- package/clients/sanitize.test.ts +0 -223
- package/clients/scan-architectural-debt.js +0 -167
- package/clients/scan-utils.js +0 -83
- package/clients/secrets-scanner.js +0 -119
- package/clients/secrets-scanner.test.js +0 -100
- package/clients/secrets-scanner.test.ts +0 -113
- package/clients/sg-runner.js +0 -292
- package/clients/state-matrix.js +0 -160
- package/clients/subprocess-client.js +0 -65
- package/clients/symbol-types.js +0 -5
- package/clients/test-runner-client.js +0 -523
- package/clients/test-runner-client.test.js +0 -192
- package/clients/test-runner-client.test.ts +0 -253
- package/clients/test-utils.js +0 -27
- package/clients/test-utils.ts +0 -36
- package/clients/todo-scanner.js +0 -200
- package/clients/todo-scanner.test.js +0 -301
- package/clients/todo-scanner.test.ts +0 -352
- package/clients/tool-availability.js +0 -207
- package/clients/tree-sitter-client.js +0 -601
- package/clients/tree-sitter-query-loader.js +0 -355
- package/clients/tree-sitter-symbol-extractor.js +0 -289
- package/clients/ts-service.js +0 -129
- package/clients/type-coverage-client.js +0 -127
- package/clients/type-coverage-client.test.js +0 -105
- package/clients/type-coverage-client.test.ts +0 -125
- package/clients/type-safety-client.js +0 -138
- package/clients/types.js +0 -11
- package/clients/typescript-client.codefix.test.js +0 -157
- package/clients/typescript-client.codefix.test.ts +0 -186
- package/clients/typescript-client.js +0 -509
- package/clients/typescript-client.test.js +0 -105
- package/clients/typescript-client.test.ts +0 -126
- package/commands/booboo.js +0 -1007
- package/commands/fix-from-booboo.js +0 -398
- package/commands/fix-simplified.js +0 -618
- package/commands/rate.js +0 -281
- package/commands/rate.test.js +0 -119
- package/commands/rate.test.ts +0 -131
- package/commands/refactor.js +0 -130
package/commands/booboo.js
DELETED
|
@@ -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
|
-
}
|