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