pi-lens 2.2.9 โ 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +198 -0
- package/README.md +709 -519
- package/clients/__tests__/file-time.test.js +216 -0
- package/clients/__tests__/file-time.test.ts +276 -0
- package/clients/__tests__/format-service.test.js +245 -0
- package/clients/__tests__/format-service.test.ts +339 -0
- package/clients/__tests__/formatters.test.js +271 -0
- package/clients/__tests__/formatters.test.ts +401 -0
- package/clients/amain-types.js +164 -0
- package/clients/amain-types.ts +165 -0
- package/clients/architect-client.js +56 -12
- package/clients/architect-client.ts +81 -16
- package/clients/ast-grep-client.js +2 -2
- package/clients/ast-grep-client.ts +14 -39
- package/clients/ast-grep-parser.ts +1 -1
- package/clients/ast-grep-rule-manager.js +8 -0
- package/clients/ast-grep-rule-manager.ts +10 -1
- package/clients/ast-grep-types.js +9 -0
- package/clients/ast-grep-types.ts +106 -0
- package/clients/auto-loop.js +10 -0
- package/clients/auto-loop.ts +14 -1
- package/clients/biome-client.js +81 -19
- package/clients/biome-client.ts +103 -22
- package/clients/bus/bus.js +191 -0
- package/clients/bus/bus.ts +251 -0
- package/clients/bus/events.js +214 -0
- package/clients/bus/events.ts +279 -0
- package/clients/bus/index.js +8 -0
- package/clients/bus/index.ts +9 -0
- package/clients/bus/integration.js +158 -0
- package/clients/bus/integration.ts +214 -0
- package/clients/complexity-client.js +13 -7
- package/clients/complexity-client.ts +13 -7
- package/clients/config-validator.js +465 -0
- package/clients/config-validator.ts +558 -0
- package/clients/dependency-checker.js +4 -10
- package/clients/dependency-checker.ts +4 -10
- package/clients/dispatch/__tests__/autofix-integration.test.js +245 -0
- package/clients/dispatch/__tests__/autofix-integration.test.ts +300 -0
- package/clients/dispatch/__tests__/runner-registration.test.js +236 -0
- package/clients/dispatch/__tests__/runner-registration.test.ts +282 -0
- package/clients/dispatch/bus-dispatcher.js +177 -0
- package/clients/dispatch/bus-dispatcher.ts +251 -0
- package/clients/dispatch/dispatcher.edge.test.js +82 -0
- package/clients/dispatch/dispatcher.edge.test.ts +100 -0
- package/clients/dispatch/dispatcher.format.test.js +46 -0
- package/clients/dispatch/dispatcher.format.test.ts +58 -0
- package/clients/dispatch/dispatcher.inline.test.js +74 -0
- package/clients/dispatch/dispatcher.inline.test.ts +93 -0
- package/clients/dispatch/dispatcher.js +19 -53
- package/clients/dispatch/dispatcher.ts +20 -67
- package/clients/dispatch/plan.js +9 -4
- package/clients/dispatch/plan.ts +9 -4
- package/clients/dispatch/runners/architect.js +21 -7
- package/clients/dispatch/runners/architect.test.js +138 -0
- package/clients/dispatch/runners/architect.test.ts +162 -0
- package/clients/dispatch/runners/architect.ts +22 -7
- package/clients/dispatch/runners/ast-grep-napi.js +462 -0
- package/clients/dispatch/runners/ast-grep-napi.test.js +111 -0
- package/clients/dispatch/runners/ast-grep-napi.test.ts +133 -0
- package/clients/dispatch/runners/ast-grep-napi.ts +506 -0
- package/clients/dispatch/runners/ast-grep.js +62 -19
- package/clients/dispatch/runners/ast-grep.ts +70 -18
- package/clients/dispatch/runners/biome.js +29 -53
- package/clients/dispatch/runners/biome.ts +29 -63
- package/clients/dispatch/runners/config-validation.js +67 -0
- package/clients/dispatch/runners/config-validation.ts +82 -0
- package/clients/dispatch/runners/go-vet.js +4 -28
- package/clients/dispatch/runners/go-vet.ts +4 -32
- package/clients/dispatch/runners/index.js +30 -10
- package/clients/dispatch/runners/index.ts +30 -10
- package/clients/dispatch/runners/oxlint.js +141 -0
- package/clients/dispatch/runners/oxlint.test.js +230 -0
- package/clients/dispatch/runners/oxlint.test.ts +303 -0
- package/clients/dispatch/runners/oxlint.ts +175 -0
- package/clients/dispatch/runners/pyright.js +40 -70
- package/clients/dispatch/runners/pyright.test.js +16 -2
- package/clients/dispatch/runners/pyright.test.ts +14 -2
- package/clients/dispatch/runners/pyright.ts +48 -91
- package/clients/dispatch/runners/python-slop.js +97 -0
- package/clients/dispatch/runners/python-slop.test.js +203 -0
- package/clients/dispatch/runners/python-slop.test.ts +298 -0
- package/clients/dispatch/runners/python-slop.ts +124 -0
- package/clients/dispatch/runners/ruff.js +18 -71
- package/clients/dispatch/runners/ruff.ts +19 -79
- package/clients/dispatch/runners/rust-clippy.js +28 -32
- package/clients/dispatch/runners/rust-clippy.ts +29 -31
- package/clients/dispatch/runners/scan_codebase.test.js +89 -0
- package/clients/dispatch/runners/scan_codebase.test.ts +105 -0
- package/clients/dispatch/runners/shellcheck.js +147 -0
- package/clients/dispatch/runners/shellcheck.test.js +98 -0
- package/clients/dispatch/runners/shellcheck.test.ts +129 -0
- package/clients/dispatch/runners/shellcheck.ts +188 -0
- package/clients/dispatch/runners/similarity.js +230 -0
- package/clients/dispatch/runners/similarity.ts +339 -0
- package/clients/dispatch/runners/spellcheck.js +106 -0
- package/clients/dispatch/runners/spellcheck.test.js +158 -0
- package/clients/dispatch/runners/spellcheck.test.ts +214 -0
- package/clients/dispatch/runners/spellcheck.ts +136 -0
- package/clients/dispatch/runners/tree-sitter.js +107 -0
- package/clients/dispatch/runners/tree-sitter.ts +135 -0
- package/clients/dispatch/runners/ts-lsp.js +104 -33
- package/clients/dispatch/runners/ts-lsp.ts +120 -38
- package/clients/dispatch/runners/ts-slop.js +113 -0
- package/clients/dispatch/runners/ts-slop.test.js +180 -0
- package/clients/dispatch/runners/ts-slop.test.ts +230 -0
- package/clients/dispatch/runners/ts-slop.ts +142 -0
- package/clients/dispatch/runners/utils/diagnostic-parsers.js +134 -0
- package/clients/dispatch/runners/utils/diagnostic-parsers.ts +186 -0
- package/clients/dispatch/runners/utils/runner-helpers.js +115 -0
- package/clients/dispatch/runners/utils/runner-helpers.ts +167 -0
- package/clients/dispatch/runners/utils.js +2 -4
- package/clients/dispatch/runners/utils.ts +2 -4
- package/clients/dispatch/types.ts +1 -1
- package/clients/dispatch/utils/format-utils.js +49 -0
- package/clients/dispatch/utils/format-utils.ts +60 -0
- package/clients/dogfood.test.js +201 -0
- package/clients/dogfood.test.ts +269 -0
- package/clients/file-time.js +152 -0
- package/clients/file-time.ts +208 -0
- package/clients/file-utils.js +40 -0
- package/clients/file-utils.ts +44 -0
- package/clients/fix-scanners.js +10 -20
- package/clients/fix-scanners.ts +10 -22
- package/clients/format-service.js +172 -0
- package/clients/format-service.ts +254 -0
- package/clients/formatters.js +435 -0
- package/clients/formatters.ts +508 -0
- package/clients/go-client.js +5 -14
- package/clients/go-client.ts +5 -13
- package/clients/installer/index.js +356 -0
- package/clients/installer/index.ts +426 -0
- package/clients/jscpd-client.js +11 -9
- package/clients/jscpd-client.ts +12 -8
- package/clients/knip-client.js +3 -7
- package/clients/knip-client.ts +3 -6
- package/clients/lsp/__tests__/client.test.js +325 -0
- package/clients/lsp/__tests__/client.test.ts +434 -0
- package/clients/lsp/__tests__/config.test.js +166 -0
- package/clients/lsp/__tests__/config.test.ts +209 -0
- package/clients/lsp/__tests__/error-recovery.test.js +213 -0
- package/clients/lsp/__tests__/error-recovery.test.ts +279 -0
- package/clients/lsp/__tests__/integration.test.js +127 -0
- package/clients/lsp/__tests__/integration.test.ts +160 -0
- package/clients/lsp/__tests__/launch.test.js +260 -0
- package/clients/lsp/__tests__/launch.test.ts +329 -0
- package/clients/lsp/__tests__/server.test.js +259 -0
- package/clients/lsp/__tests__/server.test.ts +332 -0
- package/clients/lsp/__tests__/service.test.js +417 -0
- package/clients/lsp/__tests__/service.test.ts +499 -0
- package/clients/lsp/client.js +235 -0
- package/clients/lsp/client.ts +328 -0
- package/clients/lsp/config.js +115 -0
- package/clients/lsp/config.ts +149 -0
- package/clients/lsp/index.js +222 -0
- package/clients/lsp/index.ts +280 -0
- package/clients/lsp/installer/index.js +391 -0
- package/clients/lsp/interactive-install.js +210 -0
- package/clients/lsp/interactive-install.ts +251 -0
- package/clients/lsp/language.js +170 -0
- package/clients/lsp/language.ts +216 -0
- package/clients/lsp/launch.js +174 -0
- package/clients/lsp/launch.ts +240 -0
- package/clients/lsp/lsp/launch.js +116 -0
- package/clients/lsp/lsp/server.js +532 -0
- package/clients/lsp/lsp-index.js +10 -0
- package/clients/lsp/lsp-index.ts +11 -0
- package/clients/lsp/path-utils.js +48 -0
- package/clients/lsp/path-utils.ts +52 -0
- package/clients/lsp/server.js +615 -0
- package/clients/lsp/server.ts +800 -0
- package/clients/lsp/test-py-spawn/requirements.txt +1 -0
- package/clients/lsp/test-py-spawn/test.py +3 -0
- package/clients/lsp/test-py-svc/requirements.txt +1 -0
- package/clients/lsp/test-py-svc/test.py +3 -0
- package/clients/lsp/test-python-project/requirements.txt +1 -0
- package/clients/lsp/test-python-project/test.py +5 -0
- package/clients/metrics-history.js +2 -2
- package/clients/metrics-history.ts +2 -2
- package/clients/production-readiness.js +522 -0
- package/clients/production-readiness.ts +556 -0
- package/clients/project-index.js +255 -0
- package/clients/project-index.ts +383 -0
- package/clients/project-metadata.js +531 -0
- package/clients/project-metadata.ts +624 -0
- package/clients/ruff-client.js +56 -16
- package/clients/ruff-client.ts +72 -15
- package/clients/runner-tracker.js +152 -0
- package/clients/runner-tracker.ts +213 -0
- package/clients/rust-client.js +4 -11
- package/clients/rust-client.ts +5 -11
- package/clients/safe-spawn.js +96 -0
- package/clients/safe-spawn.ts +128 -0
- package/clients/scan-architectural-debt.js +3 -6
- package/clients/scan-architectural-debt.ts +3 -6
- package/clients/scan-utils.js +5 -20
- package/clients/scan-utils.ts +5 -29
- package/clients/secrets-scanner.js +3 -17
- package/clients/secrets-scanner.ts +4 -20
- package/clients/services/__tests__/effect-integration.test.js +86 -0
- package/clients/services/__tests__/effect-integration.test.ts +111 -0
- package/clients/services/effect-integration.js +194 -0
- package/clients/services/effect-integration.ts +268 -0
- package/clients/services/index.js +7 -0
- package/clients/services/index.ts +8 -0
- package/clients/services/runner-service.js +105 -0
- package/clients/services/runner-service.ts +179 -0
- package/clients/sg-runner.js +87 -13
- package/clients/sg-runner.ts +97 -13
- package/clients/state-matrix.js +160 -0
- package/clients/state-matrix.ts +202 -0
- package/clients/subprocess-client.js +10 -9
- package/clients/subprocess-client.ts +10 -8
- package/clients/test-runner-client.js +3 -7
- package/clients/test-runner-client.ts +3 -6
- package/clients/tool-availability.js +4 -10
- package/clients/tool-availability.ts +4 -9
- package/clients/tree-sitter-client.js +564 -0
- package/clients/tree-sitter-client.ts +797 -0
- package/clients/tree-sitter-query-loader.js +355 -0
- package/clients/tree-sitter-query-loader.ts +425 -0
- package/clients/type-coverage-client.js +3 -7
- package/clients/type-coverage-client.ts +3 -6
- package/clients/typescript-client.codefix.test.js +157 -0
- package/clients/typescript-client.codefix.test.ts +186 -0
- package/clients/typescript-client.js +43 -0
- package/clients/typescript-client.ts +98 -0
- package/commands/booboo.js +799 -219
- package/commands/booboo.ts +1004 -225
- package/commands/clients/ast-grep-client.js +250 -0
- package/commands/clients/ast-grep-parser.js +86 -0
- package/commands/clients/ast-grep-rule-manager.js +91 -0
- package/commands/clients/ast-grep-types.js +9 -0
- package/commands/clients/biome-client.js +380 -0
- package/commands/clients/complexity-client.js +667 -0
- package/commands/clients/file-kinds.js +177 -0
- package/commands/clients/file-utils.js +40 -0
- package/commands/clients/jscpd-client.js +169 -0
- package/commands/clients/knip-client.js +211 -0
- package/commands/clients/ruff-client.js +297 -0
- package/commands/clients/safe-spawn.js +88 -0
- package/commands/clients/scan-utils.js +83 -0
- package/commands/clients/sg-runner.js +190 -0
- package/commands/clients/types.js +11 -0
- package/commands/clients/typescript-client.js +505 -0
- package/commands/fix-from-booboo.js +398 -0
- package/commands/fix-from-booboo.ts +485 -0
- package/commands/fix-simplified.js +618 -0
- package/commands/fix-simplified.ts +768 -0
- package/commands/rate.js +10 -14
- package/commands/rate.ts +9 -16
- package/default-architect.yaml +59 -15
- package/index.ts +342 -429
- package/package.json +16 -3
- package/rules/ast-grep-rules/rules/empty-catch.yml +38 -13
- package/rules/ast-grep-rules/rules/no-array-constructor.yml +1 -0
- package/rules/ast-grep-rules/rules/no-debugger.yml +2 -0
- package/rules/python-slop-rules/.sgconfig.yml +4 -0
- package/rules/python-slop-rules/rules/slop-rules.yml +647 -0
- package/rules/tree-sitter-queries/python/bare-except.yml +54 -0
- package/rules/tree-sitter-queries/python/eval-exec.yml +50 -0
- package/rules/tree-sitter-queries/python/is-vs-equals.yml +60 -0
- package/rules/tree-sitter-queries/python/mutable-default-arg.yml +57 -0
- package/rules/tree-sitter-queries/python/unreachable-except.yml +60 -0
- package/rules/tree-sitter-queries/python/wildcard-import.yml +46 -0
- package/rules/tree-sitter-queries/tsx/dangerously-set-inner-html.yml +63 -0
- package/rules/tree-sitter-queries/typescript/await-in-loop.yml +56 -0
- package/rules/tree-sitter-queries/typescript/console-statement.yml +47 -0
- package/rules/tree-sitter-queries/typescript/debugger.yml +47 -0
- package/rules/tree-sitter-queries/typescript/deep-nesting.yml +117 -0
- package/rules/tree-sitter-queries/typescript/deep-promise-chain.yml +73 -0
- package/rules/tree-sitter-queries/typescript/empty-catch.yml +64 -0
- package/rules/tree-sitter-queries/typescript/eval.yml +48 -0
- package/rules/tree-sitter-queries/typescript/hardcoded-secrets.yml +78 -0
- package/rules/tree-sitter-queries/typescript/long-parameter-list.yml +62 -0
- package/rules/tree-sitter-queries/typescript/mixed-async-styles.yml +49 -0
- package/rules/tree-sitter-queries/typescript/nested-ternary.yml +45 -0
- package/rules/ts-slop-rules/.sgconfig.yml +4 -0
- package/rules/ts-slop-rules/rules/in-correct-optional-input-type.yml +10 -0
- package/rules/ts-slop-rules/rules/jwt-no-verify.yml +13 -0
- package/rules/ts-slop-rules/rules/no-architecture-violation.yml +10 -0
- package/rules/ts-slop-rules/rules/no-case-declarations.yml +10 -0
- package/rules/ts-slop-rules/rules/no-dangerously-set-inner-html.yml +10 -0
- package/rules/ts-slop-rules/rules/no-debugger.yml +10 -0
- package/rules/ts-slop-rules/rules/no-dupe-args.yml +10 -0
- package/rules/ts-slop-rules/rules/no-dupe-class-members.yml +10 -0
- package/rules/ts-slop-rules/rules/no-dupe-keys.yml +10 -0
- package/rules/ts-slop-rules/rules/no-eval.yml +13 -0
- package/rules/ts-slop-rules/rules/no-hardcoded-secrets.yml +12 -0
- package/rules/ts-slop-rules/rules/no-implied-eval.yml +12 -0
- package/rules/ts-slop-rules/rules/no-inner-html.yml +13 -0
- package/rules/ts-slop-rules/rules/no-javascript-url.yml +10 -0
- package/rules/ts-slop-rules/rules/no-mutable-default.yml +10 -0
- package/rules/ts-slop-rules/rules/no-nested-links.yml +12 -0
- package/rules/ts-slop-rules/rules/no-new-symbol.yml +10 -0
- package/rules/ts-slop-rules/rules/no-new-wrappers.yml +13 -0
- package/rules/ts-slop-rules/rules/no-open-redirect.yml +16 -0
- package/rules/ts-slop-rules/rules/slop-rules.yml +455 -0
- package/rules/ts-slop-rules/rules/weak-rsa-key.yml +12 -0
- package/skills/ast-grep/SKILL.md +182 -0
- package/clients/dispatch/runners/secrets.js +0 -109
- package/commands/fix.js +0 -244
- package/commands/fix.ts +0 -373
- package/rules/ast-grep-rules/rules/no-lonely-if.yml +0 -13
package/commands/booboo.js
CHANGED
|
@@ -1,26 +1,89 @@
|
|
|
1
|
-
import * as childProcess from "node:child_process";
|
|
2
1
|
import * as nodeFs from "node:fs";
|
|
3
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";
|
|
4
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";
|
|
5
12
|
const getExtensionDir = () => {
|
|
6
13
|
if (typeof __dirname !== "undefined") {
|
|
7
14
|
return __dirname;
|
|
8
15
|
}
|
|
9
16
|
return ".";
|
|
10
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
|
+
];
|
|
11
43
|
export async function handleBooboo(args, ctx, clients, pi) {
|
|
12
44
|
const targetPath = args.trim() || ctx.cwd || process.cwd();
|
|
13
|
-
|
|
45
|
+
// Detect project metadata for richer reporting
|
|
46
|
+
const projectMeta = detectProjectMetadata(targetPath);
|
|
47
|
+
const metaDisplay = formatProjectMetadata(projectMeta);
|
|
48
|
+
ctx.ui.notify(`๐ Running full codebase review...\n${metaDisplay}`, "info");
|
|
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
|
+
};
|
|
14
70
|
// Summary counts for terminal display
|
|
15
71
|
const summaryItems = [];
|
|
16
72
|
const fullReport = [];
|
|
17
73
|
const timestamp = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19);
|
|
18
74
|
const reviewDir = path.join(process.cwd(), ".pi-lens", "reviews");
|
|
19
|
-
//
|
|
20
|
-
|
|
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
|
+
}
|
|
21
84
|
const configPath = path.join(getExtensionDir(), "..", "rules", "ast-grep-rules", ".sgconfig.yml");
|
|
22
85
|
try {
|
|
23
|
-
const result =
|
|
86
|
+
const result = safeSpawn("npx", [
|
|
24
87
|
"sg",
|
|
25
88
|
"scan",
|
|
26
89
|
"--config",
|
|
@@ -31,15 +94,28 @@ export async function handleBooboo(args, ctx, clients, pi) {
|
|
|
31
94
|
"--globs",
|
|
32
95
|
"!**/*.spec.ts",
|
|
33
96
|
"--globs",
|
|
97
|
+
"!**/*.poc.test.ts",
|
|
98
|
+
"--globs",
|
|
34
99
|
"!**/test-utils.ts",
|
|
35
100
|
"--globs",
|
|
101
|
+
"!**/test-*.ts",
|
|
102
|
+
"--globs",
|
|
103
|
+
"!**/__tests__/**",
|
|
104
|
+
"--globs",
|
|
105
|
+
"!**/tests/**",
|
|
106
|
+
"--globs",
|
|
36
107
|
"!**/.pi-lens/**",
|
|
108
|
+
"--globs",
|
|
109
|
+
"!**/.pi/**",
|
|
110
|
+
"--globs",
|
|
111
|
+
"!**/node_modules/**",
|
|
112
|
+
"--globs",
|
|
113
|
+
"!**/.git/**",
|
|
114
|
+
"--globs",
|
|
115
|
+
"!**/.ruff_cache/**",
|
|
37
116
|
targetPath,
|
|
38
117
|
], {
|
|
39
|
-
encoding: "utf-8",
|
|
40
118
|
timeout: 30000,
|
|
41
|
-
shell: true,
|
|
42
|
-
maxBuffer: 32 * 1024 * 1024, // 32MB
|
|
43
119
|
});
|
|
44
120
|
const output = result.stdout || result.stderr || "";
|
|
45
121
|
if (output.trim() && result.status !== undefined) {
|
|
@@ -50,8 +126,7 @@ export async function handleBooboo(args, ctx, clients, pi) {
|
|
|
50
126
|
try {
|
|
51
127
|
return JSON.parse(trimmed);
|
|
52
128
|
}
|
|
53
|
-
catch
|
|
54
|
-
void err;
|
|
129
|
+
catch {
|
|
55
130
|
return [];
|
|
56
131
|
}
|
|
57
132
|
}
|
|
@@ -59,8 +134,7 @@ export async function handleBooboo(args, ctx, clients, pi) {
|
|
|
59
134
|
try {
|
|
60
135
|
return [JSON.parse(l)];
|
|
61
136
|
}
|
|
62
|
-
catch
|
|
63
|
-
void err;
|
|
137
|
+
catch {
|
|
64
138
|
return [];
|
|
65
139
|
}
|
|
66
140
|
});
|
|
@@ -74,45 +148,73 @@ export async function handleBooboo(args, ctx, clients, pi) {
|
|
|
74
148
|
item.range?.start?.line ||
|
|
75
149
|
0;
|
|
76
150
|
issues.push({
|
|
151
|
+
file: item.file || item.path || targetPath,
|
|
77
152
|
line: lineNum + 1,
|
|
78
153
|
rule: ruleId,
|
|
79
154
|
message: message,
|
|
80
155
|
});
|
|
81
156
|
}
|
|
82
|
-
|
|
157
|
+
const filteredIssues = issues.filter((issue) => !isFalsePositive("ast_issues", issue.file, issue.line));
|
|
158
|
+
if (filteredIssues.length > 0) {
|
|
83
159
|
summaryItems.push({
|
|
84
160
|
category: "ast-grep",
|
|
85
|
-
count:
|
|
86
|
-
severity:
|
|
161
|
+
count: filteredIssues.length,
|
|
162
|
+
severity: filteredIssues.length > 10 ? "๐ด" : "๐ก",
|
|
87
163
|
fixable: true,
|
|
88
164
|
});
|
|
89
|
-
let fullSection = `## ast-grep (Structural Issues)\n\n**${
|
|
165
|
+
let fullSection = `## ast-grep (Structural Issues)\n\n**${filteredIssues.length} issue(s) found**\n\n`;
|
|
90
166
|
fullSection +=
|
|
91
167
|
"| Line | Rule | Message |\n|------|------|--------|\n";
|
|
92
|
-
for (const issue of
|
|
168
|
+
for (const issue of filteredIssues) {
|
|
93
169
|
fullSection += `| ${issue.line} | ${issue.rule} | ${issue.message} |\n`;
|
|
94
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
|
+
}
|
|
95
186
|
fullReport.push(fullSection);
|
|
96
187
|
}
|
|
188
|
+
return { findings: filteredIssues.length, status: "done" };
|
|
97
189
|
}
|
|
190
|
+
return { findings: 0, status: "done" };
|
|
98
191
|
}
|
|
99
|
-
catch
|
|
100
|
-
|
|
101
|
-
|
|
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" };
|
|
102
200
|
}
|
|
103
|
-
}
|
|
104
|
-
// Part 2: Similar functions
|
|
105
|
-
if (clients.astGrep.isAvailable()) {
|
|
106
201
|
const similarGroups = await clients.astGrep.findSimilarFunctions(targetPath, "typescript");
|
|
107
|
-
|
|
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) {
|
|
108
210
|
summaryItems.push({
|
|
109
211
|
category: "Similar Functions",
|
|
110
|
-
count:
|
|
212
|
+
count: filteredGroups.length,
|
|
111
213
|
severity: "๐ก",
|
|
112
214
|
fixable: true,
|
|
113
215
|
});
|
|
114
|
-
let fullSection = `## Similar Functions\n\n**${
|
|
115
|
-
for (const group of
|
|
216
|
+
let fullSection = `## Similar Functions\n\n**${filteredGroups.length} group(s) of structurally similar functions**\n\n`;
|
|
217
|
+
for (const group of filteredGroups) {
|
|
116
218
|
fullSection += `### Pattern: ${group.functions.map((f) => f.name).join(", ")}\n\n`;
|
|
117
219
|
fullSection +=
|
|
118
220
|
"| Function | File | Line |\n|----------|------|------|\n";
|
|
@@ -123,18 +225,74 @@ export async function handleBooboo(args, ctx, clients, pi) {
|
|
|
123
225
|
}
|
|
124
226
|
fullReport.push(fullSection);
|
|
125
227
|
}
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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
|
|
138
296
|
const warnings = clients.complexity.checkThresholds(metrics);
|
|
139
297
|
if (warnings.length > 0) {
|
|
140
298
|
aiSlopIssues.push(` ${metrics.filePath}:`);
|
|
@@ -145,227 +303,439 @@ export async function handleBooboo(args, ctx, clients, pi) {
|
|
|
145
303
|
}
|
|
146
304
|
}
|
|
147
305
|
}
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
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" };
|
|
191
413
|
}
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
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)",
|
|
196
458
|
severity: "๐ก",
|
|
197
459
|
fixable: true,
|
|
460
|
+
note: "Consider converting to async/await for readability",
|
|
198
461
|
});
|
|
462
|
+
findings++;
|
|
199
463
|
}
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
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)",
|
|
204
477
|
severity: "๐ก",
|
|
205
478
|
fixable: true,
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
fullSection += `### Summary\n\n| Metric | Value |\n|--------|-------|\n| Avg Maintainability Index | ${avgMI.toFixed(1)} |\n| Min Maintainability Index | ${minMI.toFixed(1)} |\n| Avg Cognitive Complexity | ${avgCognitive.toFixed(1)} |\n| Max Cognitive Complexity | ${maxCognitive} |\n| Avg Cyclomatic Complexity | ${avgCyclomatic.toFixed(1)} |\n| Max Nesting Depth | ${maxNesting} |\n| Total Files | ${results.length} |\n\n`;
|
|
210
|
-
if (lowMI.length > 0) {
|
|
211
|
-
fullSection += `### Low Maintainability (MI < 60)\n\n| File | MI | Cognitive | Cyclomatic | Nesting |\n|------|-----|-----------|------------|--------|\n`;
|
|
212
|
-
for (const f of lowMI) {
|
|
213
|
-
fullSection += `| ${f.filePath} | ${f.maintainabilityIndex.toFixed(1)} | ${f.cognitiveComplexity} | ${f.cyclomaticComplexity} | ${f.maxNestingDepth} |\n`;
|
|
214
|
-
}
|
|
215
|
-
fullSection += "\n";
|
|
479
|
+
note: "Consider promisify + async/await",
|
|
480
|
+
});
|
|
481
|
+
findings++;
|
|
216
482
|
}
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
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++;
|
|
221
501
|
}
|
|
222
|
-
fullSection += "\n";
|
|
223
502
|
}
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
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++;
|
|
227
515
|
}
|
|
228
|
-
|
|
229
|
-
if (
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
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)*`;
|
|
233
531
|
}
|
|
234
|
-
fullSection +=
|
|
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);
|
|
235
552
|
}
|
|
236
|
-
|
|
237
|
-
}
|
|
238
|
-
//
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
summaryItems.push({
|
|
242
|
-
category: "TODOs",
|
|
243
|
-
count: todoResult.items.length,
|
|
244
|
-
severity: "โน๏ธ",
|
|
245
|
-
fixable: false,
|
|
246
|
-
});
|
|
247
|
-
let fullSection = `## TODOs / Annotations\n\n`;
|
|
553
|
+
return { findings, status: "done" };
|
|
554
|
+
});
|
|
555
|
+
// Runner 5: TODOs
|
|
556
|
+
await tracker.run("TODO scanner", async () => {
|
|
557
|
+
const todoResult = clients.todo.scanDirectory(targetPath);
|
|
248
558
|
if (todoResult.items.length > 0) {
|
|
249
|
-
|
|
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";
|
|
250
569
|
for (const item of todoResult.items) {
|
|
251
570
|
fullSection += `| ${item.type} | ${item.file} | ${item.line} | ${item.message} |\n`;
|
|
252
571
|
}
|
|
572
|
+
fullSection += "\n";
|
|
573
|
+
fullReport.push(fullSection);
|
|
253
574
|
}
|
|
254
|
-
|
|
255
|
-
|
|
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" };
|
|
256
581
|
}
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
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) {
|
|
264
598
|
summaryItems.push({
|
|
265
599
|
category: "Dead Code",
|
|
266
|
-
count:
|
|
600
|
+
count: filteredIssues.length,
|
|
267
601
|
severity: "๐ก",
|
|
268
602
|
fixable: true,
|
|
269
603
|
});
|
|
270
604
|
let fullSection = `## Dead Code (Knip)\n\n`;
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
}
|
|
276
|
-
}
|
|
277
|
-
else {
|
|
278
|
-
fullSection += `No dead code issues found.\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`;
|
|
279
609
|
}
|
|
280
610
|
fullSection += "\n";
|
|
281
611
|
fullReport.push(fullSection);
|
|
282
612
|
}
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
if (
|
|
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) {
|
|
288
625
|
summaryItems.push({
|
|
289
626
|
category: "Duplicates",
|
|
290
|
-
count:
|
|
627
|
+
count: filteredClones.length,
|
|
291
628
|
severity: "๐ก",
|
|
292
629
|
fixable: true,
|
|
293
630
|
});
|
|
294
631
|
let fullSection = `## Code Duplication (jscpd)\n\n`;
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
else {
|
|
302
|
-
fullSection += `No duplicate code found.\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`;
|
|
303
637
|
}
|
|
304
638
|
fullSection += "\n";
|
|
305
639
|
fullReport.push(fullSection);
|
|
306
640
|
}
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
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
|
+
}
|
|
310
648
|
const tcResult = clients.typeCoverage.scan(targetPath);
|
|
311
649
|
if (tcResult.percentage < 100) {
|
|
312
|
-
|
|
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;
|
|
313
655
|
summaryItems.push({
|
|
314
|
-
category: "
|
|
315
|
-
count:
|
|
656
|
+
category: "Type Coverage",
|
|
657
|
+
count: filesWithLowCoverage || 1,
|
|
316
658
|
severity: tcResult.percentage < 90 ? "๐ก" : "โน๏ธ",
|
|
317
659
|
fixable: false,
|
|
318
660
|
});
|
|
319
661
|
let fullSection = `## Type Coverage\n\n**${tcResult.percentage.toFixed(1)}% typed** (${tcResult.typed}/${tcResult.total} identifiers)\n\n`;
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
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`;
|
|
324
677
|
}
|
|
325
678
|
}
|
|
326
679
|
fullSection += "\n";
|
|
327
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" };
|
|
328
689
|
}
|
|
329
|
-
}
|
|
330
|
-
// Part 8: Circular deps
|
|
331
|
-
if (!pi.getFlag("no-madge") && clients.depChecker.isAvailable()) {
|
|
332
690
|
const { circular } = clients.depChecker.scanProject(targetPath);
|
|
333
|
-
|
|
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) {
|
|
334
697
|
summaryItems.push({
|
|
335
698
|
category: "Circular Deps",
|
|
336
|
-
count:
|
|
699
|
+
count: filteredCircular.length,
|
|
337
700
|
severity: "๐ด",
|
|
338
701
|
fixable: false,
|
|
339
702
|
});
|
|
340
|
-
let fullSection = `## Circular Dependencies (Madge)\n\n
|
|
341
|
-
|
|
703
|
+
let fullSection = `## Circular Dependencies (Madge)\n\n`;
|
|
704
|
+
fullSection += `**${filteredCircular.length} circular chain(s) found**\n\n`;
|
|
705
|
+
for (const dep of filteredCircular) {
|
|
342
706
|
fullSection += `- ${dep.path.join(" โ ")}\n`;
|
|
343
707
|
}
|
|
344
708
|
fullReport.push(`${fullSection}\n`);
|
|
345
709
|
}
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
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"));
|
|
352
722
|
const archViolations = [];
|
|
353
723
|
const archScanDir = (dir) => {
|
|
354
724
|
for (const entry of nodeFs.readdirSync(dir, { withFileTypes: true })) {
|
|
355
725
|
const full = path.join(dir, entry.name);
|
|
356
726
|
if (entry.isDirectory()) {
|
|
357
|
-
if (
|
|
358
|
-
"node_modules",
|
|
359
|
-
".git",
|
|
360
|
-
"dist",
|
|
361
|
-
"build",
|
|
362
|
-
".next",
|
|
363
|
-
".pi-lens",
|
|
364
|
-
].includes(entry.name))
|
|
727
|
+
if (EXCLUDED_DIRS.includes(entry.name))
|
|
365
728
|
continue;
|
|
366
729
|
archScanDir(full);
|
|
367
730
|
}
|
|
368
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;
|
|
369
739
|
const relPath = path.relative(targetPath, full).replace(/\\/g, "/");
|
|
370
740
|
const content = nodeFs.readFileSync(full, "utf-8");
|
|
371
741
|
const lineCount = content.split("\n").length;
|
|
@@ -386,44 +756,254 @@ export async function handleBooboo(args, ctx, clients, pi) {
|
|
|
386
756
|
severity: "๐ด",
|
|
387
757
|
fixable: false,
|
|
388
758
|
});
|
|
389
|
-
let fullSection = `## Architectural Rules\n\n
|
|
759
|
+
let fullSection = `## Architectural Rules\n\n`;
|
|
760
|
+
fullSection += `**${archViolations.length} violation(s) found**\n\n`;
|
|
390
761
|
for (const v of archViolations) {
|
|
391
762
|
fullSection += `- **${v.file}**: ${v.message}\n`;
|
|
392
763
|
}
|
|
393
764
|
fullReport.push(`${fullSection}\n`);
|
|
394
765
|
}
|
|
395
|
-
|
|
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 ---
|
|
396
831
|
nodeFs.mkdirSync(reviewDir, { recursive: true });
|
|
397
832
|
const projectName = path.basename(process.cwd());
|
|
398
|
-
const
|
|
399
|
-
const
|
|
400
|
-
|
|
401
|
-
|
|
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 ---
|
|
402
956
|
if (summaryItems.length === 0) {
|
|
403
|
-
ctx.ui.notify("โ Code review clean
|
|
957
|
+
ctx.ui.notify("โ Code review clean", "info");
|
|
404
958
|
}
|
|
405
959
|
else {
|
|
406
|
-
const totalIssues
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
.
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
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
|
+
` ๐ง ${fixableCount} fixable | ๐๏ธ ${refactorNeeded} refactor`,
|
|
970
|
+
` โฑ๏ธ Total: ${jsonReport.meta.totalTime}`,
|
|
971
|
+
`๐ JSON: ${jsonPath}`,
|
|
972
|
+
`๐ MD: ${mdPath}`,
|
|
973
|
+
];
|
|
974
|
+
ctx.ui.notify(summaryLines.join("\n"), "info");
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
/**
|
|
978
|
+
* Find top N most similar function pairs in the project index
|
|
979
|
+
* Uses canonical pair ordering to avoid duplicates (A,B) vs (B,A)
|
|
980
|
+
*/
|
|
981
|
+
function findTopSimilarPairs(index, maxPairs) {
|
|
982
|
+
const entries = Array.from(index.entries.values());
|
|
983
|
+
const seenPairs = new Set();
|
|
984
|
+
const pairs = [];
|
|
985
|
+
for (let i = 0; i < entries.length; i++) {
|
|
986
|
+
for (let j = i + 1; j < entries.length; j++) {
|
|
987
|
+
const entry1 = entries[i];
|
|
988
|
+
const entry2 = entries[j];
|
|
989
|
+
// Skip if same file (we want cross-file duplicates)
|
|
990
|
+
if (entry1.filePath === entry2.filePath)
|
|
991
|
+
continue;
|
|
992
|
+
const similarity = calculateSimilarity(entry1.matrix, entry2.matrix);
|
|
993
|
+
if (similarity >= 0.75) {
|
|
994
|
+
// Canonical pair key (sorted to avoid duplicates)
|
|
995
|
+
const pairKey = [entry1.id, entry2.id].sort().join("::");
|
|
996
|
+
if (seenPairs.has(pairKey))
|
|
997
|
+
continue;
|
|
998
|
+
seenPairs.add(pairKey);
|
|
999
|
+
pairs.push({
|
|
1000
|
+
func1: entry1.id,
|
|
1001
|
+
func2: entry2.id,
|
|
1002
|
+
similarity,
|
|
1003
|
+
});
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
428
1006
|
}
|
|
1007
|
+
// Sort by similarity descending, take top N
|
|
1008
|
+
return pairs.sort((a, b) => b.similarity - a.similarity).slice(0, maxPairs);
|
|
429
1009
|
}
|