pi-lens 3.8.47 โ 3.8.51
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 +231 -1
- package/README.md +37 -6
- package/dist/clients/actionable-warnings-logger.js +51 -0
- package/dist/clients/actionable-warnings.js +542 -0
- package/dist/clients/agent-behavior-client.js +119 -0
- package/dist/clients/ast-grep-client.js +391 -0
- package/dist/clients/ast-grep-parser.js +86 -0
- package/dist/clients/ast-grep-rule-manager.js +91 -0
- package/dist/clients/ast-grep-tool-logger.js +150 -0
- package/dist/clients/ast-grep-types.js +9 -0
- package/dist/clients/ast-grep-yaml-synth.js +103 -0
- package/dist/clients/bash-file-access.js +360 -0
- package/dist/clients/biome-client.js +183 -0
- package/dist/clients/bootstrap.js +40 -0
- package/dist/clients/cache/rule-cache.js +75 -0
- package/dist/clients/cache-manager.js +273 -0
- package/dist/clients/call-graph.js +325 -0
- package/dist/clients/cascade-format.js +21 -0
- package/dist/clients/cascade-logger.js +25 -0
- package/dist/clients/cascade-types.js +1 -0
- package/dist/clients/code-quality-warnings.js +189 -0
- package/dist/clients/codebase-model.js +136 -0
- package/dist/clients/complexity-client.js +676 -0
- package/dist/clients/dependency-checker.js +359 -0
- package/dist/clients/diagnostic-logger.js +87 -0
- package/dist/clients/diagnostic-tracker.js +105 -0
- package/dist/clients/dispatch/diagnostic-taxonomy.js +66 -0
- package/dist/clients/dispatch/dispatcher.js +758 -0
- package/dist/clients/dispatch/fact-provider-types.js +1 -0
- package/dist/clients/dispatch/fact-rule-runner.js +17 -0
- package/dist/clients/dispatch/fact-runner.js +19 -0
- package/dist/clients/dispatch/fact-scheduler.js +68 -0
- package/dist/clients/dispatch/fact-store.js +47 -0
- package/dist/clients/dispatch/facts/comment-facts.js +35 -0
- package/dist/clients/dispatch/facts/file-content.js +19 -0
- package/dist/clients/dispatch/facts/function-facts.js +204 -0
- package/dist/clients/dispatch/facts/import-facts.js +163 -0
- package/dist/clients/dispatch/facts/try-catch-facts.js +112 -0
- package/dist/clients/dispatch/integration.js +1054 -0
- package/dist/clients/dispatch/plan.js +295 -0
- package/dist/clients/dispatch/priorities.js +21 -0
- package/dist/clients/dispatch/rules/async-noise.js +39 -0
- package/dist/clients/dispatch/rules/async-unnecessary-wrapper.js +27 -0
- package/dist/clients/dispatch/rules/error-obscuring.js +31 -0
- package/dist/clients/dispatch/rules/error-swallowing.js +28 -0
- package/dist/clients/dispatch/rules/high-complexity.js +36 -0
- package/dist/clients/dispatch/rules/high-fan-out.js +43 -0
- package/dist/clients/dispatch/rules/missing-error-propagation.js +62 -0
- package/dist/clients/dispatch/rules/pass-through-wrappers.js +38 -0
- package/dist/clients/dispatch/rules/placeholder-comments.js +40 -0
- package/dist/clients/dispatch/rules/quality-rules.js +297 -0
- package/dist/clients/dispatch/rules/sonar-rules.js +493 -0
- package/dist/clients/dispatch/rules/unsafe-boundary.js +95 -0
- package/dist/clients/dispatch/runner-context.js +52 -0
- package/dist/clients/dispatch/runners/actionlint.js +110 -0
- package/dist/clients/dispatch/runners/ast-grep-napi.js +302 -0
- package/dist/clients/dispatch/runners/biome-check.js +120 -0
- package/dist/clients/dispatch/runners/biome.js +69 -0
- package/dist/clients/dispatch/runners/cpp-check.js +225 -0
- package/dist/clients/dispatch/runners/credo.js +71 -0
- package/dist/clients/dispatch/runners/dart-analyze.js +202 -0
- package/dist/clients/dispatch/runners/detekt.js +165 -0
- package/dist/clients/dispatch/runners/dotnet-build.js +159 -0
- package/dist/clients/dispatch/runners/elixir-check.js +136 -0
- package/dist/clients/dispatch/runners/eslint.js +126 -0
- package/dist/clients/dispatch/runners/fact-rules.js +30 -0
- package/dist/clients/dispatch/runners/fish-indent.js +69 -0
- package/dist/clients/dispatch/runners/gleam-check.js +87 -0
- package/dist/clients/dispatch/runners/go-vet.js +50 -0
- package/dist/clients/dispatch/runners/golangci-lint.js +118 -0
- package/dist/clients/dispatch/runners/hadolint.js +71 -0
- package/dist/clients/dispatch/runners/htmlhint.js +90 -0
- package/dist/clients/dispatch/runners/index.js +105 -0
- package/dist/clients/dispatch/runners/javac.js +76 -0
- package/dist/clients/dispatch/runners/ktlint.js +136 -0
- package/dist/clients/dispatch/runners/lsp.js +217 -0
- package/dist/clients/dispatch/runners/markdownlint.js +113 -0
- package/dist/clients/dispatch/runners/mypy.js +68 -0
- package/dist/clients/dispatch/runners/oxlint.js +178 -0
- package/dist/clients/dispatch/runners/php-lint.js +62 -0
- package/dist/clients/dispatch/runners/phpstan.js +73 -0
- package/dist/clients/dispatch/runners/prisma-validate.js +62 -0
- package/dist/clients/dispatch/runners/psscriptanalyzer.js +149 -0
- package/dist/clients/dispatch/runners/pyright.js +115 -0
- package/dist/clients/dispatch/runners/python-slop.js +106 -0
- package/dist/clients/dispatch/runners/rubocop.js +90 -0
- package/dist/clients/dispatch/runners/ruff.js +94 -0
- package/dist/clients/dispatch/runners/rust-clippy.js +149 -0
- package/dist/clients/dispatch/runners/semgrep.js +197 -0
- package/dist/clients/dispatch/runners/shellcheck.js +160 -0
- package/dist/clients/dispatch/runners/shfmt.js +86 -0
- package/dist/clients/dispatch/runners/spellcheck.js +111 -0
- package/dist/clients/dispatch/runners/sqlfluff.js +146 -0
- package/dist/clients/dispatch/runners/stylelint.js +116 -0
- package/dist/clients/dispatch/runners/swiftlint.js +144 -0
- package/dist/clients/dispatch/runners/taplo.js +61 -0
- package/dist/clients/dispatch/runners/tflint.js +68 -0
- package/dist/clients/dispatch/runners/tree-sitter.js +690 -0
- package/dist/clients/dispatch/runners/ts-lsp.js +109 -0
- package/dist/clients/dispatch/runners/utils/diagnostic-parsers.js +149 -0
- package/dist/clients/dispatch/runners/utils/lazy-installer.js +38 -0
- package/dist/clients/dispatch/runners/utils/runner-helpers.js +385 -0
- package/dist/clients/dispatch/runners/utils.js +36 -0
- package/dist/clients/dispatch/runners/vale.js +105 -0
- package/dist/clients/dispatch/runners/yaml-rule-parser.js +167 -0
- package/dist/clients/dispatch/runners/yamllint.js +70 -0
- package/dist/clients/dispatch/runners/zig-check.js +87 -0
- package/dist/clients/dispatch/tool-profile.js +30 -0
- package/dist/clients/dispatch/types.js +13 -0
- package/dist/clients/dispatch/utils/format-utils.js +45 -0
- package/dist/clients/dispatch/utils/lsp-diagnostics.js +28 -0
- package/{clients/env-utils.ts โ dist/clients/env-utils.js} +9 -9
- package/dist/clients/event-loop-monitor.js +56 -0
- package/dist/clients/feature-hints.js +49 -0
- package/dist/clients/file-kinds.js +401 -0
- package/dist/clients/file-role.js +102 -0
- package/dist/clients/file-time.js +155 -0
- package/dist/clients/file-utils.js +454 -0
- package/dist/clients/fix-worklog.js +87 -0
- package/dist/clients/format-service.js +184 -0
- package/dist/clients/formatters.js +917 -0
- package/dist/clients/generated-artifacts.js +146 -0
- package/dist/clients/git-guard.js +32 -0
- package/dist/clients/gitleaks-client.js +317 -0
- package/dist/clients/go-client.js +84 -0
- package/dist/clients/govulncheck-client.js +378 -0
- package/dist/clients/indent-retarget.js +84 -0
- package/dist/clients/installer/index.js +1997 -0
- package/dist/clients/jscpd-client.js +282 -0
- package/dist/clients/knip-client.js +371 -0
- package/dist/clients/language-policy.js +232 -0
- package/dist/clients/language-profile.js +329 -0
- package/dist/clients/latency-logger.js +44 -0
- package/dist/clients/lens-config.js +135 -0
- package/dist/clients/lens-engine.js +91 -0
- package/dist/clients/lens-events.js +70 -0
- package/dist/clients/log-cleanup.js +203 -0
- package/dist/clients/lsp/aggregation.js +83 -0
- package/dist/clients/lsp/client.js +1053 -0
- package/dist/clients/lsp/config.js +165 -0
- package/dist/clients/lsp/edits.js +228 -0
- package/dist/clients/lsp/index.js +1405 -0
- package/dist/clients/lsp/interactive-install.js +355 -0
- package/dist/clients/lsp/language.js +175 -0
- package/dist/clients/lsp/launch.js +755 -0
- package/{clients/lsp/lsp-index.ts โ dist/clients/lsp/lsp-index.js} +1 -2
- package/dist/clients/lsp/path-utils.js +5 -0
- package/dist/clients/lsp/server-strategies.js +59 -0
- package/dist/clients/lsp/server.js +1600 -0
- package/dist/clients/mcp/analyze.js +170 -0
- package/dist/clients/mcp/host-shim.js +27 -0
- package/dist/clients/mcp/ipc.js +84 -0
- package/dist/clients/mcp/review.js +115 -0
- package/dist/clients/mcp/session.js +136 -0
- package/dist/clients/metrics-client.js +108 -0
- package/dist/clients/metrics-history.js +388 -0
- package/dist/clients/oldtext-autopatch.js +127 -0
- package/dist/clients/package-root.js +38 -0
- package/dist/clients/partial-edit-apply.js +43 -0
- package/dist/clients/path-utils.js +205 -0
- package/dist/clients/pipeline.js +835 -0
- package/dist/clients/production-readiness.js +517 -0
- package/dist/clients/project-changes.js +68 -0
- package/dist/clients/project-conventions.js +177 -0
- package/dist/clients/project-diagnostics/cache.js +51 -0
- package/dist/clients/project-diagnostics/runner-adapters/knip.js +44 -0
- package/dist/clients/project-diagnostics/scanner.js +159 -0
- package/dist/clients/project-diagnostics/types.js +1 -0
- package/dist/clients/project-metadata.js +710 -0
- package/dist/clients/project-scan-policy.js +49 -0
- package/dist/clients/project-snapshot.js +137 -0
- package/dist/clients/read-expansion.js +289 -0
- package/dist/clients/read-guard-logger.js +77 -0
- package/dist/clients/read-guard-tool-lines.js +1002 -0
- package/dist/clients/read-guard.js +855 -0
- package/dist/clients/reverse-deps.js +182 -0
- package/dist/clients/review-graph/builder.js +984 -0
- package/dist/clients/review-graph/format.js +33 -0
- package/dist/clients/review-graph/query.js +166 -0
- package/dist/clients/review-graph/service.js +44 -0
- package/dist/clients/review-graph/types.js +1 -0
- package/dist/clients/review-graph/workspace-modules.js +445 -0
- package/dist/clients/ruff-client.js +159 -0
- package/dist/clients/rules-scanner.js +118 -0
- package/dist/clients/runner-tracker.js +153 -0
- package/dist/clients/runtime-agent-end.js +227 -0
- package/dist/clients/runtime-config.js +73 -0
- package/dist/clients/runtime-context.js +42 -0
- package/dist/clients/runtime-coordinator.js +365 -0
- package/dist/clients/runtime-session.js +868 -0
- package/dist/clients/runtime-tool-result.js +509 -0
- package/dist/clients/runtime-turn.js +602 -0
- package/dist/clients/rust-client.js +83 -0
- package/dist/clients/safe-spawn.js +301 -0
- package/dist/clients/sanitize.js +291 -0
- package/dist/clients/scan-utils.js +80 -0
- package/dist/clients/search-read-registration.js +66 -0
- package/dist/clients/secrets-scanner.js +181 -0
- package/dist/clients/semgrep-config.js +157 -0
- package/dist/clients/session-state-store.js +97 -0
- package/dist/clients/session-summary.js +37 -0
- package/dist/clients/sg-runner.js +496 -0
- package/dist/clients/source-filter.js +274 -0
- package/dist/clients/source-groups.js +96 -0
- package/dist/clients/startup-scan.js +255 -0
- package/dist/clients/startup-timing.js +32 -0
- package/dist/clients/symbol-types.js +5 -0
- package/dist/clients/test-runner-client.js +766 -0
- package/dist/clients/todo-scanner.js +198 -0
- package/dist/clients/tool-event.js +19 -0
- package/dist/clients/tool-policy.js +1856 -0
- package/dist/clients/tree-sitter-cache.js +244 -0
- package/dist/clients/tree-sitter-client.js +1235 -0
- package/dist/clients/tree-sitter-fixer.js +127 -0
- package/dist/clients/tree-sitter-logger.js +25 -0
- package/dist/clients/tree-sitter-navigator.js +269 -0
- package/dist/clients/tree-sitter-query-loader.js +428 -0
- package/dist/clients/tree-sitter-symbol-extractor.js +654 -0
- package/dist/clients/ts-service.js +130 -0
- package/dist/clients/type-coverage-client.js +128 -0
- package/dist/clients/types.js +11 -0
- package/dist/clients/typescript-client.js +509 -0
- package/dist/clients/widget-state.js +533 -0
- package/dist/clients/word-index.js +250 -0
- package/dist/commands/booboo.js +1412 -0
- package/dist/i18n.js +61 -0
- package/dist/index.js +1743 -0
- package/dist/mcp/analyze-cli.js +105 -0
- package/dist/mcp/server.js +745 -0
- package/dist/mcp/worker.js +46 -0
- package/dist/tools/ast-dump.js +62 -0
- package/dist/tools/ast-grep-replace.js +176 -0
- package/dist/tools/ast-grep-search.js +334 -0
- package/dist/tools/lens-diagnostics.js +469 -0
- package/{tools โ dist/tools}/lsp-navigation.js +368 -16
- package/package.json +28 -18
- package/rules/ast-grep-rules/rules/nested-ternary-js.yml +2 -0
- package/rules/ast-grep-rules/rules/nested-ternary.yml +2 -0
- package/rules/ast-grep-rules/rules/no-constant-condition-js.yml +11 -18
- package/rules/ast-grep-rules/rules/no-constant-condition.yml +11 -18
- package/rules/ast-grep-rules/rules/no-mutable-export.yml +13 -0
- package/rules/ast-grep-rules/rules/no-octal-literal.yml +14 -0
- package/rules/ast-grep-rules/rules/no-sort-without-comparator.yml +14 -0
- package/rules/ast-grep-rules/rules/redos-nested-quantifier.yml +23 -0
- package/rules/ast-grep-rules/rules/switch-without-default.yml +19 -0
- package/rules/ast-grep-rules/{rules โ rules-disabled}/constructor-super-js.yml +2 -0
- package/rules/ast-grep-rules/{rules โ rules-disabled}/constructor-super.yml +3 -0
- package/rules/ast-grep-rules/{rules โ rules-disabled}/no-hardcoded-secrets-js.yml +3 -0
- package/rules/ast-grep-rules/{rules โ rules-disabled}/no-hardcoded-secrets.yml +3 -0
- package/rules/ast-grep-rules/{rules โ rules-disabled}/no-process-env.yml +3 -0
- package/rules/ast-grep-rules/{rules โ rules-disabled}/unchecked-sync-fs-js.yml +3 -0
- package/rules/ast-grep-rules/{rules โ rules-disabled}/unchecked-sync-fs.yml +3 -0
- package/rules/ast-grep-rules/slop-patterns.yml +26 -35
- package/rules/rule-catalog.json +3 -5
- package/rules/tree-sitter-queries/java/{infinite-loop.yml โ infinite-loop-java.yml} +1 -1
- package/rules/tree-sitter-queries/typescript/no-equality-in-for-condition.yml +40 -0
- package/rules/tree-sitter-queries/typescript/no-jump-in-finally.yml +47 -0
- package/scripts/analyze-pi-lens-logs.mjs +211 -7
- package/scripts/download-grammars.js +15 -0
- package/skills/ast-grep/SKILL.md +71 -2
- package/skills/write-ast-grep-rule/SKILL.md +93 -3
- package/skills/write-tree-sitter-rule/SKILL.md +36 -0
- package/clients/actionable-warnings-logger.ts +0 -65
- package/clients/actionable-warnings.ts +0 -653
- package/clients/agent-behavior-client.ts +0 -155
- package/clients/amain-types.ts +0 -165
- package/clients/ast-grep-client.ts +0 -396
- package/clients/ast-grep-parser.ts +0 -130
- package/clients/ast-grep-rule-manager.ts +0 -104
- package/clients/ast-grep-types.ts +0 -106
- package/clients/biome-client.ts +0 -657
- package/clients/bootstrap.ts +0 -83
- package/clients/cache/rule-cache.ts +0 -110
- package/clients/cache-manager.ts +0 -361
- package/clients/cascade-format.ts +0 -27
- package/clients/cascade-logger.ts +0 -78
- package/clients/cascade-types.ts +0 -36
- package/clients/code-quality-warnings.ts +0 -313
- package/clients/complexity-client.ts +0 -922
- package/clients/dependency-checker.ts +0 -466
- package/clients/diagnostic-logger.ts +0 -151
- package/clients/diagnostic-tracker.ts +0 -163
- package/clients/dispatch/diagnostic-taxonomy.ts +0 -81
- package/clients/dispatch/dispatcher.ts +0 -986
- package/clients/dispatch/fact-provider-types.ts +0 -22
- package/clients/dispatch/fact-rule-runner.ts +0 -22
- package/clients/dispatch/fact-runner.ts +0 -28
- package/clients/dispatch/fact-scheduler.ts +0 -79
- package/clients/dispatch/fact-store.ts +0 -65
- package/clients/dispatch/facts/comment-facts.ts +0 -59
- package/clients/dispatch/facts/file-content.ts +0 -20
- package/clients/dispatch/facts/function-facts.ts +0 -256
- package/clients/dispatch/facts/import-facts.ts +0 -68
- package/clients/dispatch/facts/try-catch-facts.ts +0 -177
- package/clients/dispatch/integration.ts +0 -1326
- package/clients/dispatch/plan.ts +0 -331
- package/clients/dispatch/priorities.ts +0 -22
- package/clients/dispatch/rules/async-noise.ts +0 -50
- package/clients/dispatch/rules/async-unnecessary-wrapper.ts +0 -35
- package/clients/dispatch/rules/error-obscuring.ts +0 -40
- package/clients/dispatch/rules/error-swallowing.ts +0 -35
- package/clients/dispatch/rules/high-complexity.ts +0 -44
- package/clients/dispatch/rules/high-fan-out.ts +0 -55
- package/clients/dispatch/rules/missing-error-propagation.ts +0 -71
- package/clients/dispatch/rules/pass-through-wrappers.ts +0 -52
- package/clients/dispatch/rules/placeholder-comments.ts +0 -47
- package/clients/dispatch/rules/quality-rules.ts +0 -375
- package/clients/dispatch/rules/sonar-rules.ts +0 -508
- package/clients/dispatch/rules/unsafe-boundary.ts +0 -104
- package/clients/dispatch/runner-context.ts +0 -61
- package/clients/dispatch/runners/actionlint.ts +0 -145
- package/clients/dispatch/runners/ast-grep-napi.ts +0 -576
- package/clients/dispatch/runners/biome-check.ts +0 -166
- package/clients/dispatch/runners/biome.ts +0 -78
- package/clients/dispatch/runners/cpp-check.ts +0 -267
- package/clients/dispatch/runners/credo.ts +0 -99
- package/clients/dispatch/runners/dart-analyze.ts +0 -226
- package/clients/dispatch/runners/detekt.ts +0 -192
- package/clients/dispatch/runners/dotnet-build.ts +0 -195
- package/clients/dispatch/runners/elixir-check.ts +0 -149
- package/clients/dispatch/runners/eslint.ts +0 -155
- package/clients/dispatch/runners/fact-rules.ts +0 -44
- package/clients/dispatch/runners/fish-indent.ts +0 -83
- package/clients/dispatch/runners/gleam-check.ts +0 -108
- package/clients/dispatch/runners/go-vet.ts +0 -66
- package/clients/dispatch/runners/golangci-lint.ts +0 -175
- package/clients/dispatch/runners/hadolint.ts +0 -103
- package/clients/dispatch/runners/htmlhint.ts +0 -118
- package/clients/dispatch/runners/index.ts +0 -113
- package/clients/dispatch/runners/javac.ts +0 -99
- package/clients/dispatch/runners/ktlint.ts +0 -173
- package/clients/dispatch/runners/lsp.ts +0 -243
- package/clients/dispatch/runners/markdownlint.ts +0 -132
- package/clients/dispatch/runners/mypy.ts +0 -89
- package/clients/dispatch/runners/oxlint.ts +0 -214
- package/clients/dispatch/runners/php-lint.ts +0 -82
- package/clients/dispatch/runners/phpstan.ts +0 -113
- package/clients/dispatch/runners/prisma-validate.ts +0 -89
- package/clients/dispatch/runners/psscriptanalyzer.ts +0 -177
- package/clients/dispatch/runners/pyright.ts +0 -143
- package/clients/dispatch/runners/python-slop.ts +0 -133
- package/clients/dispatch/runners/rubocop.ts +0 -144
- package/clients/dispatch/runners/ruff.ts +0 -133
- package/clients/dispatch/runners/rust-clippy.ts +0 -181
- package/clients/dispatch/runners/semgrep.ts +0 -271
- package/clients/dispatch/runners/shellcheck.ts +0 -195
- package/clients/dispatch/runners/shfmt.ts +0 -100
- package/clients/dispatch/runners/similarity.ts +0 -510
- package/clients/dispatch/runners/spellcheck.ts +0 -145
- package/clients/dispatch/runners/sqlfluff.ts +0 -174
- package/clients/dispatch/runners/stylelint.ts +0 -155
- package/clients/dispatch/runners/swiftlint.ts +0 -199
- package/clients/dispatch/runners/taplo.ts +0 -93
- package/clients/dispatch/runners/tflint.ts +0 -100
- package/clients/dispatch/runners/tree-sitter.ts +0 -812
- package/clients/dispatch/runners/ts-lsp.ts +0 -136
- package/clients/dispatch/runners/type-safety.ts +0 -197
- package/clients/dispatch/runners/utils/diagnostic-parsers.ts +0 -223
- package/clients/dispatch/runners/utils/lazy-installer.ts +0 -54
- package/clients/dispatch/runners/utils/runner-helpers.ts +0 -572
- package/clients/dispatch/runners/utils.ts +0 -58
- package/clients/dispatch/runners/vale.ts +0 -175
- package/clients/dispatch/runners/yaml-rule-parser.ts +0 -417
- package/clients/dispatch/runners/yamllint.ts +0 -92
- package/clients/dispatch/runners/zig-check.ts +0 -113
- package/clients/dispatch/tool-profile.ts +0 -41
- package/clients/dispatch/types.ts +0 -185
- package/clients/dispatch/utils/format-utils.ts +0 -56
- package/clients/dispatch/utils/lsp-diagnostics.ts +0 -42
- package/clients/feature-hints.ts +0 -79
- package/clients/file-kinds.ts +0 -467
- package/clients/file-role.ts +0 -145
- package/clients/file-time.ts +0 -208
- package/clients/file-utils.ts +0 -503
- package/clients/fix-worklog.ts +0 -121
- package/clients/format-service.ts +0 -276
- package/clients/formatters.ts +0 -1028
- package/clients/generated-artifacts.ts +0 -140
- package/clients/git-guard.ts +0 -41
- package/clients/go-client.ts +0 -242
- package/clients/indent-retarget.ts +0 -90
- package/clients/installer/index.ts +0 -2325
- package/clients/jscpd-client.ts +0 -348
- package/clients/knip-client.ts +0 -437
- package/clients/language-policy.ts +0 -263
- package/clients/language-profile.ts +0 -258
- package/clients/latency-logger.ts +0 -74
- package/clients/lens-config.ts +0 -177
- package/clients/lens-events.ts +0 -151
- package/clients/log-cleanup.ts +0 -255
- package/clients/lsp/aggregation.ts +0 -91
- package/clients/lsp/client.ts +0 -1505
- package/clients/lsp/config.ts +0 -216
- package/clients/lsp/edits.ts +0 -294
- package/clients/lsp/index.ts +0 -1439
- package/clients/lsp/interactive-install.ts +0 -424
- package/clients/lsp/language.ts +0 -223
- package/clients/lsp/launch.ts +0 -928
- package/clients/lsp/path-utils.ts +0 -12
- package/clients/lsp/server-strategies.ts +0 -81
- package/clients/lsp/server.ts +0 -2051
- package/clients/metrics-client.ts +0 -153
- package/clients/metrics-history.ts +0 -510
- package/clients/oldtext-autopatch.ts +0 -114
- package/clients/package-root.ts +0 -44
- package/clients/partial-edit-apply.ts +0 -76
- package/clients/path-utils.ts +0 -223
- package/clients/pipeline.ts +0 -1131
- package/clients/production-readiness.ts +0 -552
- package/clients/project-changes.ts +0 -112
- package/clients/project-conventions.ts +0 -215
- package/clients/project-index.ts +0 -403
- package/clients/project-metadata.ts +0 -809
- package/clients/project-scan-policy.ts +0 -79
- package/clients/project-snapshot.ts +0 -221
- package/clients/read-expansion.ts +0 -283
- package/clients/read-guard-logger.ts +0 -101
- package/clients/read-guard-tool-lines.ts +0 -804
- package/clients/read-guard.ts +0 -1044
- package/clients/reverse-deps.ts +0 -244
- package/clients/review-graph/builder.ts +0 -927
- package/clients/review-graph/format.ts +0 -51
- package/clients/review-graph/query.ts +0 -162
- package/clients/review-graph/service.ts +0 -69
- package/clients/review-graph/types.ts +0 -46
- package/clients/review-graph/workspace-modules.ts +0 -497
- package/clients/ruff-client.ts +0 -511
- package/clients/rules-scanner.ts +0 -149
- package/clients/runner-tracker.ts +0 -215
- package/clients/runtime-agent-end.ts +0 -331
- package/clients/runtime-config.ts +0 -77
- package/clients/runtime-context.ts +0 -79
- package/clients/runtime-coordinator.ts +0 -472
- package/clients/runtime-session.ts +0 -784
- package/clients/runtime-tool-result.ts +0 -650
- package/clients/runtime-turn.ts +0 -656
- package/clients/rust-client.ts +0 -270
- package/clients/safe-spawn.ts +0 -339
- package/clients/sanitize.ts +0 -356
- package/clients/scan-utils.ts +0 -75
- package/clients/secrets-scanner.ts +0 -214
- package/clients/semgrep-config.ts +0 -203
- package/clients/session-summary.ts +0 -53
- package/clients/sg-runner.ts +0 -470
- package/clients/source-filter.ts +0 -263
- package/clients/source-groups.ts +0 -140
- package/clients/startup-scan.ts +0 -150
- package/clients/state-matrix.ts +0 -202
- package/clients/subprocess-client.ts +0 -101
- package/clients/symbol-types.ts +0 -77
- package/clients/test-runner-client.ts +0 -1134
- package/clients/todo-scanner.ts +0 -243
- package/clients/tool-availability.ts +0 -250
- package/clients/tool-policy.ts +0 -2063
- package/clients/tree-sitter-cache.ts +0 -316
- package/clients/tree-sitter-client.ts +0 -1541
- package/clients/tree-sitter-fixer.ts +0 -217
- package/clients/tree-sitter-logger.ts +0 -51
- package/clients/tree-sitter-navigator.ts +0 -329
- package/clients/tree-sitter-query-loader.ts +0 -521
- package/clients/tree-sitter-symbol-extractor.ts +0 -442
- package/clients/ts-service.ts +0 -154
- package/clients/type-coverage-client.ts +0 -164
- package/clients/type-safety-client.ts +0 -193
- package/clients/types.ts +0 -59
- package/clients/typescript-client.ts +0 -698
- package/clients/widget-state.ts +0 -477
- package/commands/booboo.ts +0 -1837
- package/i18n.ts +0 -66
- package/index.ts +0 -1962
- package/tools/ast-grep-replace.js +0 -75
- package/tools/ast-grep-replace.ts +0 -112
- package/tools/ast-grep-search.js +0 -168
- package/tools/ast-grep-search.ts +0 -222
- package/tools/lsp-diagnostics.ts +0 -706
- package/tools/lsp-navigation.ts +0 -1124
- package/tools/shared.ts +0 -31
- package/tsconfig.json +0 -18
- /package/{tools โ dist/tools}/lsp-diagnostics.js +0 -0
- /package/{tools โ dist/tools}/shared.js +0 -0
- /package/rules/tree-sitter-queries/{abap โ abap-disabled}/delete-where.yml +0 -0
- /package/rules/tree-sitter-queries/{cobol โ cobol-disabled}/alter-statement.yml +0 -0
- /package/rules/tree-sitter-queries/{cobol โ cobol-disabled}/lock-table-cobol.yml +0 -0
- /package/rules/tree-sitter-queries/{plsql โ plsql-disabled}/delete-update-where.yml +0 -0
- /package/rules/tree-sitter-queries/{plsql โ plsql-disabled}/end-loop-semicolon.yml +0 -0
- /package/rules/tree-sitter-queries/{plsql โ plsql-disabled}/fetch-bulk-collect-limit.yml +0 -0
- /package/rules/tree-sitter-queries/{plsql โ plsql-disabled}/forallsave-exceptions.yml +0 -0
- /package/rules/tree-sitter-queries/{plsql โ plsql-disabled}/lock-table.yml +0 -0
- /package/rules/tree-sitter-queries/{plsql โ plsql-disabled}/nchar-nvarchar2-bytes.yml +0 -0
- /package/rules/tree-sitter-queries/{plsql โ plsql-disabled}/no-synchronize.yml +0 -0
- /package/rules/tree-sitter-queries/{plsql โ plsql-disabled}/not-null-initialization.yml +0 -0
- /package/rules/tree-sitter-queries/{plsql โ plsql-disabled}/raise-application-error-codes.yml +0 -0
package/commands/booboo.ts
DELETED
|
@@ -1,1837 +0,0 @@
|
|
|
1
|
-
import * as nodeFs from "node:fs";
|
|
2
|
-
import * as path from "node:path";
|
|
3
|
-
import type {
|
|
4
|
-
ExtensionAPI,
|
|
5
|
-
ExtensionContext,
|
|
6
|
-
} from "@earendil-works/pi-coding-agent";
|
|
7
|
-
import type { AstGrepClient } from "../clients/ast-grep-client.js";
|
|
8
|
-
import type { ComplexityClient } from "../clients/complexity-client.js";
|
|
9
|
-
import type { DependencyChecker } from "../clients/dependency-checker.js";
|
|
10
|
-
import { createDispatchContext } from "../clients/dispatch/dispatcher.js";
|
|
11
|
-
import { evaluateRules } from "../clients/dispatch/fact-rule-runner.js";
|
|
12
|
-
import { runProviders } from "../clients/dispatch/fact-runner.js";
|
|
13
|
-
import { FactStore } from "../clients/dispatch/fact-store.js";
|
|
14
|
-
import {
|
|
15
|
-
getKnipIgnorePatterns,
|
|
16
|
-
getProjectIgnoreGlobs,
|
|
17
|
-
isTestFile,
|
|
18
|
-
} from "../clients/file-utils.js";
|
|
19
|
-
import type { JscpdClient } from "../clients/jscpd-client.js";
|
|
20
|
-
import type { KnipClient } from "../clients/knip-client.js";
|
|
21
|
-
import { validateProductionReadiness } from "../clients/production-readiness.js";
|
|
22
|
-
import {
|
|
23
|
-
buildProjectIndex,
|
|
24
|
-
type ProjectIndex,
|
|
25
|
-
} from "../clients/project-index.js";
|
|
26
|
-
import {
|
|
27
|
-
detectProjectMetadata,
|
|
28
|
-
getAvailableCommands,
|
|
29
|
-
} from "../clients/project-metadata.js";
|
|
30
|
-
import { RunnerTracker } from "../clients/runner-tracker.js";
|
|
31
|
-
import { safeSpawn } from "../clients/safe-spawn.js";
|
|
32
|
-
import {
|
|
33
|
-
collectSourceFiles,
|
|
34
|
-
getFilterStats,
|
|
35
|
-
} from "../clients/source-filter.js";
|
|
36
|
-
import { calculateSimilarity } from "../clients/state-matrix.js";
|
|
37
|
-
import type { TodoScanner } from "../clients/todo-scanner.js";
|
|
38
|
-
import { TreeSitterClient } from "../clients/tree-sitter-client.js";
|
|
39
|
-
import { queryLoader } from "../clients/tree-sitter-query-loader.js";
|
|
40
|
-
import type { TypeCoverageClient } from "../clients/type-coverage-client.js";
|
|
41
|
-
// Side-effect import: registers all fact providers and fact rules
|
|
42
|
-
import "../clients/dispatch/integration.js";
|
|
43
|
-
|
|
44
|
-
const ROOT_MARKERS = [
|
|
45
|
-
"package.json",
|
|
46
|
-
"tsconfig.json",
|
|
47
|
-
".git",
|
|
48
|
-
"Cargo.toml",
|
|
49
|
-
"go.mod",
|
|
50
|
-
"pyproject.toml",
|
|
51
|
-
];
|
|
52
|
-
|
|
53
|
-
function hasRootMarker(dir: string): boolean {
|
|
54
|
-
return ROOT_MARKERS.some((m) => nodeFs.existsSync(path.join(dir, m)));
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
function resolveProjectRoot(startDir: string): string {
|
|
58
|
-
// Walk up: find nearest ancestor with a root marker
|
|
59
|
-
let dir = startDir;
|
|
60
|
-
const fsRoot = path.parse(dir).root;
|
|
61
|
-
while (dir !== fsRoot) {
|
|
62
|
-
if (hasRootMarker(dir)) return dir;
|
|
63
|
-
const parent = path.dirname(dir);
|
|
64
|
-
if (parent === dir) break;
|
|
65
|
-
dir = parent;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
// Walk down one level: if exactly one immediate subdir has a root marker, use it
|
|
69
|
-
try {
|
|
70
|
-
const entries = nodeFs.readdirSync(startDir, { withFileTypes: true });
|
|
71
|
-
const candidates = entries
|
|
72
|
-
.filter((e) => e.isDirectory())
|
|
73
|
-
.map((e) => path.join(startDir, e.name))
|
|
74
|
-
.filter(hasRootMarker);
|
|
75
|
-
if (candidates.length === 1) return candidates[0];
|
|
76
|
-
} catch {
|
|
77
|
-
// unreadable dir โ fall through
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
return startDir;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
// Module-level singleton โ web-tree-sitter WASM must only be initialized once per process
|
|
84
|
-
let _sharedTreeSitterClient: TreeSitterClient | null = null;
|
|
85
|
-
function getSharedTreeSitterClient(): TreeSitterClient {
|
|
86
|
-
if (!_sharedTreeSitterClient) {
|
|
87
|
-
_sharedTreeSitterClient = new TreeSitterClient();
|
|
88
|
-
}
|
|
89
|
-
return _sharedTreeSitterClient;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
const EXT_TO_LANG: Record<string, string> = {
|
|
93
|
-
".ts": "typescript",
|
|
94
|
-
".mts": "typescript",
|
|
95
|
-
".cts": "typescript",
|
|
96
|
-
".tsx": "typescript",
|
|
97
|
-
".js": "javascript",
|
|
98
|
-
".mjs": "javascript",
|
|
99
|
-
".cjs": "javascript",
|
|
100
|
-
".jsx": "javascript",
|
|
101
|
-
".py": "python",
|
|
102
|
-
".go": "go",
|
|
103
|
-
".rs": "rust",
|
|
104
|
-
".rb": "ruby",
|
|
105
|
-
};
|
|
106
|
-
|
|
107
|
-
const getExtensionDir = () => {
|
|
108
|
-
if (typeof __dirname !== "undefined") {
|
|
109
|
-
return __dirname;
|
|
110
|
-
}
|
|
111
|
-
return ".";
|
|
112
|
-
};
|
|
113
|
-
|
|
114
|
-
/**
|
|
115
|
-
* Centralized test file exclusion for booboo runners.
|
|
116
|
-
* Mirrors the dispatch system's skipTestFiles behavior.
|
|
117
|
-
*/
|
|
118
|
-
function shouldIncludeFile(filePath: string): boolean {
|
|
119
|
-
return !isTestFile(filePath);
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
export async function handleBooboo(
|
|
123
|
-
args: string,
|
|
124
|
-
ctx: ExtensionContext,
|
|
125
|
-
clients: {
|
|
126
|
-
astGrep: AstGrepClient;
|
|
127
|
-
complexity: ComplexityClient;
|
|
128
|
-
todo: TodoScanner;
|
|
129
|
-
knip: KnipClient;
|
|
130
|
-
jscpd: JscpdClient;
|
|
131
|
-
typeCoverage: TypeCoverageClient;
|
|
132
|
-
depChecker: DependencyChecker;
|
|
133
|
-
},
|
|
134
|
-
pi: ExtensionAPI,
|
|
135
|
-
) {
|
|
136
|
-
const requestedPath = args.trim() || ctx.cwd || process.cwd();
|
|
137
|
-
const targetPath = resolveProjectRoot(path.resolve(requestedPath));
|
|
138
|
-
const reviewRoot = targetPath;
|
|
139
|
-
|
|
140
|
-
// Build --globs exclusion args for sg scan from centralized .gitignore matching.
|
|
141
|
-
const sgExcludeGlobs = getProjectIgnoreGlobs(targetPath).flatMap((glob) => [
|
|
142
|
-
"--globs",
|
|
143
|
-
`!${glob}`,
|
|
144
|
-
]);
|
|
145
|
-
|
|
146
|
-
const categoryKey = (name: string) => name.toLowerCase().replace(/\s+/g, "-");
|
|
147
|
-
|
|
148
|
-
// Tunable thresholds โ adjust these to reduce false positives across all projects
|
|
149
|
-
const FACT_SEVERITY_FILTER = new Set(["error", "warning"]);
|
|
150
|
-
const MIN_TREE_SITTER_HITS_PER_RULE = 3;
|
|
151
|
-
|
|
152
|
-
// Detect project metadata for richer reporting
|
|
153
|
-
const projectMeta = detectProjectMetadata(targetPath);
|
|
154
|
-
const langs = new Set(projectMeta.languages);
|
|
155
|
-
|
|
156
|
-
// No noisy notification at start - just run the review silently
|
|
157
|
-
|
|
158
|
-
// Detect project type once for all runners
|
|
159
|
-
const isTsProject = nodeFs.existsSync(path.join(targetPath, "tsconfig.json"));
|
|
160
|
-
|
|
161
|
-
// Collect source files once with unified artifact filtering
|
|
162
|
-
// This ensures all scanners work on the same deduplicated file set
|
|
163
|
-
const sourceFiles = collectSourceFiles(targetPath);
|
|
164
|
-
const allFiles = collectSourceFiles(targetPath, {
|
|
165
|
-
extensions: [
|
|
166
|
-
".ts",
|
|
167
|
-
".tsx",
|
|
168
|
-
".js",
|
|
169
|
-
".jsx",
|
|
170
|
-
".mjs",
|
|
171
|
-
".cjs",
|
|
172
|
-
".py",
|
|
173
|
-
".go",
|
|
174
|
-
".rs",
|
|
175
|
-
".rb",
|
|
176
|
-
],
|
|
177
|
-
});
|
|
178
|
-
const filterStats = getFilterStats(allFiles, sourceFiles);
|
|
179
|
-
|
|
180
|
-
if (filterStats.skipped > 0) {
|
|
181
|
-
const byTypeStr = Object.entries(filterStats.byType)
|
|
182
|
-
.map(([ext, count]) => `${count} ${ext}`)
|
|
183
|
-
.join(", ");
|
|
184
|
-
// biome-ignore lint/suspicious/noConsole: CLI output
|
|
185
|
-
console.log(
|
|
186
|
-
`[lens-booboo] Filtered ${filterStats.skipped} build artifacts (${byTypeStr}), scanning ${filterStats.kept} source files`,
|
|
187
|
-
);
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
// Get available commands for the project
|
|
191
|
-
const availableCommands = getAvailableCommands(projectMeta);
|
|
192
|
-
|
|
193
|
-
// Load false positives from fix session to filter them out
|
|
194
|
-
const sessionFile = path.join(reviewRoot, ".pi-lens", "fix-session.json");
|
|
195
|
-
let falsePositives: string[] = [];
|
|
196
|
-
try {
|
|
197
|
-
const sessionData = JSON.parse(
|
|
198
|
-
nodeFs.readFileSync(sessionFile, "utf-8") || "{}",
|
|
199
|
-
);
|
|
200
|
-
falsePositives = sessionData.falsePositives || [];
|
|
201
|
-
} catch {
|
|
202
|
-
// No session file yet
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
// Helper to check if an issue is marked as false positive
|
|
206
|
-
const isFalsePositive = (
|
|
207
|
-
category: string,
|
|
208
|
-
file: string,
|
|
209
|
-
line?: number,
|
|
210
|
-
): boolean => {
|
|
211
|
-
const fpKey =
|
|
212
|
-
line !== undefined
|
|
213
|
-
? `${category}:${file}:${line}`
|
|
214
|
-
: `${category}:${file}`;
|
|
215
|
-
return falsePositives.some(
|
|
216
|
-
(fp) => fp === fpKey || fp.startsWith(`${category}:${file}`),
|
|
217
|
-
);
|
|
218
|
-
};
|
|
219
|
-
|
|
220
|
-
// Summary counts for terminal display
|
|
221
|
-
const summaryItems: {
|
|
222
|
-
category: string;
|
|
223
|
-
count: number;
|
|
224
|
-
severity: "๐ด" | "๐ก" | "๐ข" | "โน๏ธ";
|
|
225
|
-
fixable: boolean;
|
|
226
|
-
}[] = [];
|
|
227
|
-
const fullReport: string[] = [];
|
|
228
|
-
const timestamp = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19);
|
|
229
|
-
const reviewDir = path.join(reviewRoot, ".pi-lens", "reviews");
|
|
230
|
-
|
|
231
|
-
// Initialize runner tracker (no per-runner progress to avoid UI overwriting)
|
|
232
|
-
const tracker = new RunnerTracker();
|
|
233
|
-
|
|
234
|
-
// Helper to format elapsed time
|
|
235
|
-
const formatElapsed = (ms: number): string =>
|
|
236
|
-
ms < 1000 ? `${ms}ms` : `${(ms / 1000).toFixed(1)}s`;
|
|
237
|
-
|
|
238
|
-
// Runner 1: Design smells via ast-grep
|
|
239
|
-
await tracker.run("ast-grep (design smells)", async () => {
|
|
240
|
-
if (!(await clients.astGrep.ensureAvailable())) {
|
|
241
|
-
return { findings: 0, status: "skipped" };
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
const configPath = path.join(
|
|
245
|
-
getExtensionDir(),
|
|
246
|
-
"..",
|
|
247
|
-
"rules",
|
|
248
|
-
"ast-grep-rules",
|
|
249
|
-
".sgconfig.yml",
|
|
250
|
-
);
|
|
251
|
-
|
|
252
|
-
try {
|
|
253
|
-
const result = safeSpawn(
|
|
254
|
-
"npx",
|
|
255
|
-
[
|
|
256
|
-
"sg",
|
|
257
|
-
"scan",
|
|
258
|
-
"--config",
|
|
259
|
-
configPath,
|
|
260
|
-
"--json",
|
|
261
|
-
"--globs",
|
|
262
|
-
"!**/*.test.ts",
|
|
263
|
-
"--globs",
|
|
264
|
-
"!**/*.spec.ts",
|
|
265
|
-
"--globs",
|
|
266
|
-
"!**/*.poc.test.ts",
|
|
267
|
-
"--globs",
|
|
268
|
-
"!**/test-utils.ts",
|
|
269
|
-
"--globs",
|
|
270
|
-
"!**/test-*.ts",
|
|
271
|
-
"--globs",
|
|
272
|
-
"!**/__tests__/**",
|
|
273
|
-
"--globs",
|
|
274
|
-
"!**/tests/**",
|
|
275
|
-
"--globs",
|
|
276
|
-
"!**/.pi-lens/**",
|
|
277
|
-
"--globs",
|
|
278
|
-
"!**/.pi/**",
|
|
279
|
-
"--globs",
|
|
280
|
-
"!**/node_modules/**",
|
|
281
|
-
"--globs",
|
|
282
|
-
"!**/vendor/**",
|
|
283
|
-
"--globs",
|
|
284
|
-
"!**/third_party/**",
|
|
285
|
-
"--globs",
|
|
286
|
-
"!**/third-party/**",
|
|
287
|
-
"--globs",
|
|
288
|
-
"!**/.git/**",
|
|
289
|
-
"--globs",
|
|
290
|
-
"!**/.ruff_cache/**",
|
|
291
|
-
...sgExcludeGlobs,
|
|
292
|
-
targetPath,
|
|
293
|
-
],
|
|
294
|
-
{
|
|
295
|
-
timeout: 30000,
|
|
296
|
-
},
|
|
297
|
-
);
|
|
298
|
-
|
|
299
|
-
const output = result.stdout || result.stderr || "";
|
|
300
|
-
if (output.trim() && result.status !== undefined) {
|
|
301
|
-
const issues: Array<{
|
|
302
|
-
file: string;
|
|
303
|
-
line: number;
|
|
304
|
-
rule: string;
|
|
305
|
-
message: string;
|
|
306
|
-
}> = [];
|
|
307
|
-
|
|
308
|
-
const parseItems = (raw: string): Record<string, any>[] => {
|
|
309
|
-
const trimmed = raw.trim();
|
|
310
|
-
if (trimmed.startsWith("[")) {
|
|
311
|
-
try {
|
|
312
|
-
return JSON.parse(trimmed);
|
|
313
|
-
} catch {
|
|
314
|
-
return [];
|
|
315
|
-
}
|
|
316
|
-
}
|
|
317
|
-
return raw.split("\n").flatMap((l: string) => {
|
|
318
|
-
try {
|
|
319
|
-
return [JSON.parse(l)];
|
|
320
|
-
} catch {
|
|
321
|
-
return [];
|
|
322
|
-
}
|
|
323
|
-
});
|
|
324
|
-
};
|
|
325
|
-
|
|
326
|
-
for (const item of parseItems(output)) {
|
|
327
|
-
const ruleId =
|
|
328
|
-
item.ruleId || item.rule?.title || item.name || "unknown";
|
|
329
|
-
const ruleDesc = clients.astGrep.getRuleDescription?.(ruleId);
|
|
330
|
-
const message = ruleDesc?.message || item.message || ruleId;
|
|
331
|
-
const lineNum =
|
|
332
|
-
item.labels?.[0]?.range?.start?.line ||
|
|
333
|
-
item.spans?.[0]?.range?.start?.line ||
|
|
334
|
-
item.range?.start?.line ||
|
|
335
|
-
0;
|
|
336
|
-
|
|
337
|
-
issues.push({
|
|
338
|
-
file: item.file || item.path || targetPath,
|
|
339
|
-
line: lineNum + 1,
|
|
340
|
-
rule: ruleId,
|
|
341
|
-
message: message,
|
|
342
|
-
});
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
const filteredIssues = issues.filter(
|
|
346
|
-
(issue) =>
|
|
347
|
-
!isFalsePositive(categoryKey("ast-grep"), issue.file, issue.line),
|
|
348
|
-
);
|
|
349
|
-
|
|
350
|
-
if (filteredIssues.length > 0) {
|
|
351
|
-
summaryItems.push({
|
|
352
|
-
category: "ast-grep",
|
|
353
|
-
count: filteredIssues.length,
|
|
354
|
-
severity: filteredIssues.length > 10 ? "๐ด" : "๐ก",
|
|
355
|
-
fixable: true,
|
|
356
|
-
});
|
|
357
|
-
|
|
358
|
-
let fullSection = `## ast-grep (Structural Issues)\n\n**${filteredIssues.length} issue(s) found**\n\n`;
|
|
359
|
-
fullSection +=
|
|
360
|
-
"| Line | Rule | Message |\n|------|------|--------|\n";
|
|
361
|
-
for (const issue of filteredIssues) {
|
|
362
|
-
fullSection += `| ${issue.line} | ${issue.rule} | ${issue.message} |\n`;
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
fullSection += "\n### ๐ก How to Fix\n\n";
|
|
366
|
-
const seenRules = new Set<string>();
|
|
367
|
-
for (const issue of filteredIssues.slice(0, 5)) {
|
|
368
|
-
if (seenRules.has(issue.rule)) continue;
|
|
369
|
-
seenRules.add(issue.rule);
|
|
370
|
-
const ruleDesc = clients.astGrep.getRuleDescription?.(issue.rule);
|
|
371
|
-
if (ruleDesc?.note || ruleDesc?.fix) {
|
|
372
|
-
fullSection += `**${issue.rule}:**\n`;
|
|
373
|
-
if (ruleDesc.note) fullSection += `${ruleDesc.note}\n\n`;
|
|
374
|
-
if (ruleDesc.fix)
|
|
375
|
-
fullSection += `Suggested fix:\n\`\`\`typescript\n${ruleDesc.fix}\n\`\`\`\n\n`;
|
|
376
|
-
}
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
fullReport.push(fullSection);
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
return { findings: filteredIssues.length, status: "done" };
|
|
383
|
-
}
|
|
384
|
-
return { findings: 0, status: "done" };
|
|
385
|
-
} catch {
|
|
386
|
-
return { findings: 0, status: "error" };
|
|
387
|
-
}
|
|
388
|
-
});
|
|
389
|
-
|
|
390
|
-
// Runner 2: Similar functions
|
|
391
|
-
await tracker.run("ast-grep (similar functions)", async () => {
|
|
392
|
-
if (!(await clients.astGrep.ensureAvailable())) {
|
|
393
|
-
return { findings: 0, status: "skipped" };
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
const similarGroups = await clients.astGrep.findSimilarFunctions(
|
|
397
|
-
targetPath,
|
|
398
|
-
"typescript",
|
|
399
|
-
);
|
|
400
|
-
|
|
401
|
-
// Filter out test files using centralized exclusion
|
|
402
|
-
const filteredGroups = similarGroups
|
|
403
|
-
.map((group) => ({
|
|
404
|
-
...group,
|
|
405
|
-
functions: group.functions.filter((fn) => shouldIncludeFile(fn.file)),
|
|
406
|
-
}))
|
|
407
|
-
.filter((group) => group.functions.length > 1); // Need at least 2 non-test functions
|
|
408
|
-
|
|
409
|
-
if (filteredGroups.length > 0) {
|
|
410
|
-
summaryItems.push({
|
|
411
|
-
category: "Similar Functions",
|
|
412
|
-
count: filteredGroups.length,
|
|
413
|
-
severity: "๐ก",
|
|
414
|
-
fixable: true,
|
|
415
|
-
});
|
|
416
|
-
|
|
417
|
-
let fullSection = `## Similar Functions\n\n**${filteredGroups.length} group(s) of structurally similar functions**\n\n`;
|
|
418
|
-
for (const group of filteredGroups) {
|
|
419
|
-
fullSection += `### Pattern: ${group.functions.map((f) => f.name).join(", ")}\n\n`;
|
|
420
|
-
fullSection +=
|
|
421
|
-
"| Function | File | Line |\n|----------|------|------|\n";
|
|
422
|
-
for (const fn of group.functions) {
|
|
423
|
-
fullSection += `| ${fn.name} | ${fn.file} | ${fn.line} |\n`;
|
|
424
|
-
}
|
|
425
|
-
fullSection += "\n";
|
|
426
|
-
}
|
|
427
|
-
fullReport.push(fullSection);
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
return { findings: filteredGroups.length, status: "done" };
|
|
431
|
-
});
|
|
432
|
-
|
|
433
|
-
// Runner 3: Semantic similarity
|
|
434
|
-
await tracker.run("semantic similarity (Amain)", async () => {
|
|
435
|
-
try {
|
|
436
|
-
const absoluteFiles = collectSourceFiles(targetPath, {
|
|
437
|
-
extensions: [".ts"],
|
|
438
|
-
}).filter(shouldIncludeFile);
|
|
439
|
-
|
|
440
|
-
if (absoluteFiles.length === 0) {
|
|
441
|
-
return { findings: 0, status: "done" };
|
|
442
|
-
}
|
|
443
|
-
const index = await buildProjectIndex(targetPath, absoluteFiles);
|
|
444
|
-
const topPairs = findTopSimilarPairs(index, 10);
|
|
445
|
-
|
|
446
|
-
if (topPairs.length > 0) {
|
|
447
|
-
summaryItems.push({
|
|
448
|
-
category: "Semantic Duplicates",
|
|
449
|
-
count: topPairs.length,
|
|
450
|
-
severity: "๐ก",
|
|
451
|
-
fixable: true,
|
|
452
|
-
});
|
|
453
|
-
|
|
454
|
-
let fullSection = `## Semantic Duplicates (Amain Algorithm)\n\n`;
|
|
455
|
-
fullSection += `**${topPairs.length} pair(s) with >=${(SEMANTIC_SIMILARITY_THRESHOLD * 100).toFixed(0)}% semantic similarity**\n\n`;
|
|
456
|
-
fullSection +=
|
|
457
|
-
"Functions with different names/variables but similar logic structures.\n\n";
|
|
458
|
-
|
|
459
|
-
for (const pair of topPairs) {
|
|
460
|
-
fullSection += `### ${pair.func1} โ ${pair.func2}\n\n`;
|
|
461
|
-
fullSection += `- Similarity: **${(pair.similarity * 100).toFixed(1)}%**\n`;
|
|
462
|
-
fullSection += `- Consider consolidating or extracting shared logic\n\n`;
|
|
463
|
-
}
|
|
464
|
-
fullReport.push(fullSection);
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
return { findings: topPairs.length, status: "done" };
|
|
468
|
-
} catch (err) {
|
|
469
|
-
console.error("[booboo] Semantic similarity analysis failed:", err);
|
|
470
|
-
return { findings: 0, status: "error" };
|
|
471
|
-
}
|
|
472
|
-
});
|
|
473
|
-
|
|
474
|
-
// Runner 4: Complexity metrics
|
|
475
|
-
await tracker.run("complexity metrics", async () => {
|
|
476
|
-
const results: import("../clients/complexity-client.js").FileComplexity[] =
|
|
477
|
-
[];
|
|
478
|
-
const aiSlopIssues: string[] = [];
|
|
479
|
-
// Use pre-collected sourceFiles (already filtered for artifacts)
|
|
480
|
-
const files = sourceFiles.filter(shouldIncludeFile);
|
|
481
|
-
|
|
482
|
-
for (const fullPath of files) {
|
|
483
|
-
if (clients.complexity.isSupportedFile(fullPath)) {
|
|
484
|
-
const metrics = clients.complexity.analyzeFile(fullPath);
|
|
485
|
-
if (metrics) {
|
|
486
|
-
results.push(metrics);
|
|
487
|
-
// AI slop check - already filtered by shouldIncludeFile above
|
|
488
|
-
const warnings = clients.complexity
|
|
489
|
-
.checkThresholds(metrics)
|
|
490
|
-
.filter((w) => !w.includes("entropy") && !w.includes("AI-style"));
|
|
491
|
-
if (warnings.length > 0) {
|
|
492
|
-
aiSlopIssues.push(` ${metrics.filePath}:`);
|
|
493
|
-
for (const w of warnings) {
|
|
494
|
-
aiSlopIssues.push(` โ ${w}`);
|
|
495
|
-
}
|
|
496
|
-
}
|
|
497
|
-
}
|
|
498
|
-
}
|
|
499
|
-
}
|
|
500
|
-
|
|
501
|
-
if (results.length > 0) {
|
|
502
|
-
const avgMI =
|
|
503
|
-
results.reduce((a, b) => a + b.maintainabilityIndex, 0) /
|
|
504
|
-
results.length;
|
|
505
|
-
const avgCognitive =
|
|
506
|
-
results.reduce((a, b) => a + b.cognitiveComplexity, 0) / results.length;
|
|
507
|
-
const avgCyclomatic =
|
|
508
|
-
results.reduce((a, b) => a + b.cyclomaticComplexity, 0) /
|
|
509
|
-
results.length;
|
|
510
|
-
const maxNesting = Math.max(...results.map((r) => r.maxNestingDepth));
|
|
511
|
-
const maxCognitive = Math.max(
|
|
512
|
-
...results.map((r) => r.cognitiveComplexity),
|
|
513
|
-
);
|
|
514
|
-
const minMI = Math.min(...results.map((r) => r.maintainabilityIndex));
|
|
515
|
-
|
|
516
|
-
// Only flag files with EXTREME issues (tuned to reduce false positives)
|
|
517
|
-
// MI < 20 is "critically unmaintainable" (was < 40, too aggressive)
|
|
518
|
-
const severeLowMI = results
|
|
519
|
-
.filter((r) => r.maintainabilityIndex < 20 && !isTestFile(r.filePath))
|
|
520
|
-
.sort((a, b) => a.maintainabilityIndex - b.maintainabilityIndex);
|
|
521
|
-
// Cognitive > 80 is extreme (was > 30, flagged too many files)
|
|
522
|
-
const veryHighCognitive = results
|
|
523
|
-
.filter((r) => r.cognitiveComplexity > 80 && !isTestFile(r.filePath))
|
|
524
|
-
.sort((a, b) => b.cognitiveComplexity - a.cognitiveComplexity);
|
|
525
|
-
// Deep nesting > 8 levels is extreme (was > 5, normal code hits this)
|
|
526
|
-
const deepNesting = results
|
|
527
|
-
.filter((r) => r.maxNestingDepth > 8 && !isTestFile(r.filePath))
|
|
528
|
-
.sort((a, b) => b.maxNestingDepth - a.maxNestingDepth);
|
|
529
|
-
|
|
530
|
-
let findings = 0;
|
|
531
|
-
|
|
532
|
-
if (severeLowMI.length > 0) {
|
|
533
|
-
findings += severeLowMI.length;
|
|
534
|
-
summaryItems.push({
|
|
535
|
-
category: "Low Maintainability",
|
|
536
|
-
count: severeLowMI.length,
|
|
537
|
-
severity: "๐ด",
|
|
538
|
-
fixable: false,
|
|
539
|
-
});
|
|
540
|
-
}
|
|
541
|
-
if (veryHighCognitive.length > 0) {
|
|
542
|
-
findings += veryHighCognitive.length;
|
|
543
|
-
summaryItems.push({
|
|
544
|
-
category: "Very High Complexity",
|
|
545
|
-
count: veryHighCognitive.length,
|
|
546
|
-
severity: "๐ด",
|
|
547
|
-
fixable: true,
|
|
548
|
-
});
|
|
549
|
-
}
|
|
550
|
-
if (deepNesting.length > 0) {
|
|
551
|
-
findings += deepNesting.length;
|
|
552
|
-
summaryItems.push({
|
|
553
|
-
category: "Deep Nesting",
|
|
554
|
-
count: deepNesting.length,
|
|
555
|
-
severity: "๐ก",
|
|
556
|
-
fixable: true,
|
|
557
|
-
});
|
|
558
|
-
}
|
|
559
|
-
if (aiSlopIssues.length > 0) {
|
|
560
|
-
findings += Math.floor(aiSlopIssues.length / 2);
|
|
561
|
-
summaryItems.push({
|
|
562
|
-
category: "AI Slop",
|
|
563
|
-
count: Math.floor(aiSlopIssues.length / 2),
|
|
564
|
-
severity: "๐ก",
|
|
565
|
-
fixable: true,
|
|
566
|
-
});
|
|
567
|
-
}
|
|
568
|
-
|
|
569
|
-
let fullSection = `## Complexity Metrics\n\n**${results.length} file(s) scanned**\n\n`;
|
|
570
|
-
fullSection += `### Summary\n\n| Metric | Value |\n|--------|-------|\n`;
|
|
571
|
-
fullSection += `| Avg Maintainability Index | ${avgMI.toFixed(1)} |\n`;
|
|
572
|
-
fullSection += `| Min Maintainability Index | ${minMI.toFixed(1)} |\n`;
|
|
573
|
-
fullSection += `| Avg Cognitive Complexity | ${avgCognitive.toFixed(1)} |\n`;
|
|
574
|
-
fullSection += `| Max Cognitive Complexity | ${maxCognitive} |\n`;
|
|
575
|
-
fullSection += `| Avg Cyclomatic Complexity | ${avgCyclomatic.toFixed(1)} |\n`;
|
|
576
|
-
fullSection += `| Max Nesting Depth | ${maxNesting} |\n`;
|
|
577
|
-
fullSection += `| Total Files | ${results.length} |\n\n`;
|
|
578
|
-
|
|
579
|
-
// Report severe issues (thresholds match findings count)
|
|
580
|
-
if (severeLowMI.length > 0) {
|
|
581
|
-
fullSection += `### Low Maintainability (MI < 20)\n\n| File | MI | Cognitive | Cyclomatic | Nesting |\n|------|-----|-----------|------------|--------|\n`;
|
|
582
|
-
for (const f of severeLowMI) {
|
|
583
|
-
fullSection += `| ${f.filePath} | ${f.maintainabilityIndex.toFixed(1)} | ${f.cognitiveComplexity} | ${f.cyclomaticComplexity} | ${f.maxNestingDepth} |\n`;
|
|
584
|
-
}
|
|
585
|
-
fullSection += "\n";
|
|
586
|
-
}
|
|
587
|
-
|
|
588
|
-
if (veryHighCognitive.length > 0) {
|
|
589
|
-
fullSection += `### Very High Cognitive Complexity (> 80)\n\n| File | Cognitive | MI | Cyclomatic | Nesting |\n|------|-----------|-----|------------|--------|\n`;
|
|
590
|
-
for (const f of veryHighCognitive) {
|
|
591
|
-
fullSection += `| ${f.filePath} | ${f.cognitiveComplexity} | ${f.maintainabilityIndex.toFixed(1)} | ${f.cyclomaticComplexity} | ${f.maxNestingDepth} |\n`;
|
|
592
|
-
}
|
|
593
|
-
fullSection += "\n";
|
|
594
|
-
}
|
|
595
|
-
|
|
596
|
-
if (deepNesting.length > 0) {
|
|
597
|
-
fullSection += `### Deep Nesting (> 8 levels)\n\n| File | Nesting | Cognitive | MI |\n|------|---------|-----------|-----|\n`;
|
|
598
|
-
for (const f of deepNesting) {
|
|
599
|
-
fullSection += `| ${f.filePath} | ${f.maxNestingDepth} | ${f.cognitiveComplexity} | ${f.maintainabilityIndex.toFixed(1)} |\n`;
|
|
600
|
-
}
|
|
601
|
-
fullSection += "\n";
|
|
602
|
-
}
|
|
603
|
-
|
|
604
|
-
if (aiSlopIssues.length > 0) {
|
|
605
|
-
fullSection += `### AI Slop Indicators\n\n`;
|
|
606
|
-
for (const issue of aiSlopIssues) {
|
|
607
|
-
fullSection += `${issue}\n`;
|
|
608
|
-
}
|
|
609
|
-
fullSection += "\n";
|
|
610
|
-
}
|
|
611
|
-
|
|
612
|
-
fullReport.push(fullSection);
|
|
613
|
-
return { findings, status: "done" };
|
|
614
|
-
}
|
|
615
|
-
|
|
616
|
-
return { findings: 0, status: "done" };
|
|
617
|
-
});
|
|
618
|
-
|
|
619
|
-
// Runner 4: Tree-sitter patterns โ language-aware, driven by .yml rule files
|
|
620
|
-
// Uses the same queryLoader + singleton client as the per-write dispatch runner.
|
|
621
|
-
// Covers all languages: TypeScript, JavaScript, Python, Go, Rust, Ruby.
|
|
622
|
-
await tracker.run("tree-sitter patterns", async () => {
|
|
623
|
-
const client = getSharedTreeSitterClient();
|
|
624
|
-
if (!client.isAvailable()) return { findings: 0, status: "skipped" };
|
|
625
|
-
|
|
626
|
-
const initialized = await client.init();
|
|
627
|
-
if (!initialized) return { findings: 0, status: "skipped" };
|
|
628
|
-
|
|
629
|
-
await queryLoader.loadQueries(targetPath);
|
|
630
|
-
const allQueries = queryLoader.getAllQueries();
|
|
631
|
-
if (allQueries.length === 0) return { findings: 0, status: "skipped" };
|
|
632
|
-
|
|
633
|
-
// Deduplicate structural rules that fire per nesting level
|
|
634
|
-
const DEDUP_PER_FILE = new Set(["deep-promise-chain", "deep-nesting"]);
|
|
635
|
-
|
|
636
|
-
interface TSIssue {
|
|
637
|
-
file: string;
|
|
638
|
-
line: number;
|
|
639
|
-
ruleId: string;
|
|
640
|
-
severity: string;
|
|
641
|
-
message: string;
|
|
642
|
-
}
|
|
643
|
-
|
|
644
|
-
const byRule = new Map<string, TSIssue[]>();
|
|
645
|
-
let findings = 0;
|
|
646
|
-
|
|
647
|
-
for (const filePath of allFiles) {
|
|
648
|
-
if (isTestFile(filePath)) continue;
|
|
649
|
-
const ext = filePath.slice(filePath.lastIndexOf("."));
|
|
650
|
-
const langId = EXT_TO_LANG[ext];
|
|
651
|
-
if (!langId) continue;
|
|
652
|
-
|
|
653
|
-
const langQueries = allQueries.filter(
|
|
654
|
-
(q) =>
|
|
655
|
-
q.language === langId ||
|
|
656
|
-
(langId === "javascript" && q.language === "typescript"),
|
|
657
|
-
);
|
|
658
|
-
if (langQueries.length === 0) continue;
|
|
659
|
-
|
|
660
|
-
for (const query of langQueries) {
|
|
661
|
-
let matches;
|
|
662
|
-
try {
|
|
663
|
-
matches = await client.runQueryOnFile(query, filePath, langId, {
|
|
664
|
-
maxResults: 20,
|
|
665
|
-
});
|
|
666
|
-
} catch {
|
|
667
|
-
continue;
|
|
668
|
-
}
|
|
669
|
-
if (!matches?.length) continue;
|
|
670
|
-
|
|
671
|
-
const relFile = path.relative(targetPath, filePath);
|
|
672
|
-
const bucket = byRule.get(query.id) ?? [];
|
|
673
|
-
|
|
674
|
-
if (DEDUP_PER_FILE.has(query.id)) {
|
|
675
|
-
if (!bucket.some((h) => h.file === relFile)) {
|
|
676
|
-
bucket.push({
|
|
677
|
-
file: relFile,
|
|
678
|
-
line: matches[0].line ?? 1,
|
|
679
|
-
ruleId: query.id,
|
|
680
|
-
severity: query.severity,
|
|
681
|
-
message: query.message,
|
|
682
|
-
});
|
|
683
|
-
findings++;
|
|
684
|
-
}
|
|
685
|
-
} else {
|
|
686
|
-
for (const m of matches) {
|
|
687
|
-
bucket.push({
|
|
688
|
-
file: relFile,
|
|
689
|
-
line: m.line ?? 1,
|
|
690
|
-
ruleId: query.id,
|
|
691
|
-
severity: query.severity,
|
|
692
|
-
message: query.message,
|
|
693
|
-
});
|
|
694
|
-
findings++;
|
|
695
|
-
}
|
|
696
|
-
}
|
|
697
|
-
byRule.set(query.id, bucket);
|
|
698
|
-
}
|
|
699
|
-
}
|
|
700
|
-
|
|
701
|
-
// Suppress rules with fewer than N hits (false positives from one-off matches)
|
|
702
|
-
for (const [ruleId, bucket] of byRule) {
|
|
703
|
-
if (bucket.length < MIN_TREE_SITTER_HITS_PER_RULE) {
|
|
704
|
-
byRule.delete(ruleId);
|
|
705
|
-
findings -= bucket.length;
|
|
706
|
-
}
|
|
707
|
-
}
|
|
708
|
-
|
|
709
|
-
if (findings === 0) return { findings: 0, status: "done" };
|
|
710
|
-
|
|
711
|
-
const errorCount = [...byRule.values()]
|
|
712
|
-
.flat()
|
|
713
|
-
.filter((i) => i.severity === "error").length;
|
|
714
|
-
summaryItems.push({
|
|
715
|
-
category: "Tree-sitter Patterns",
|
|
716
|
-
count: findings,
|
|
717
|
-
severity: errorCount > 0 ? "๐ด" : "๐ก",
|
|
718
|
-
fixable: true,
|
|
719
|
-
});
|
|
720
|
-
|
|
721
|
-
// Sort rules by hit count descending
|
|
722
|
-
const sorted = [...byRule.entries()].sort(
|
|
723
|
-
(a, b) => b[1].length - a[1].length,
|
|
724
|
-
);
|
|
725
|
-
let fullSection = `## Tree-sitter Patterns\n\n**${findings} issue(s) across ${byRule.size} rule(s)**\n\n`;
|
|
726
|
-
for (const [ruleId, issues] of sorted) {
|
|
727
|
-
const sev = issues[0].severity === "error" ? "๐ด" : "๐ก";
|
|
728
|
-
fullSection += `### ${sev} ${ruleId} (${issues.length})\n\n`;
|
|
729
|
-
fullSection += `${issues[0].message}\n\n`;
|
|
730
|
-
fullSection += "| File | Line |\n|------|------|\n";
|
|
731
|
-
for (const issue of issues.slice(0, 10)) {
|
|
732
|
-
fullSection += `| ${issue.file} | ${issue.line} |\n`;
|
|
733
|
-
}
|
|
734
|
-
if (issues.length > 10)
|
|
735
|
-
fullSection += `| ... | +${issues.length - 10} more |\n`;
|
|
736
|
-
fullSection += "\n";
|
|
737
|
-
}
|
|
738
|
-
fullReport.push(fullSection);
|
|
739
|
-
|
|
740
|
-
return { findings, status: "done" };
|
|
741
|
-
});
|
|
742
|
-
|
|
743
|
-
// Runner 4b: Fact rules โ semantic analysis over TS/JS files
|
|
744
|
-
// Runs all registered fact rules (error-obscuring, async-noise, unsafe-boundary, etc.)
|
|
745
|
-
// using the same provider/rule pipeline as the per-write dispatch system.
|
|
746
|
-
await tracker.run("fact rules", async () => {
|
|
747
|
-
const boobooFacts = new FactStore();
|
|
748
|
-
const tsFiles = allFiles.filter((f) => /\.tsx?$/.test(f) && !isTestFile(f));
|
|
749
|
-
if (tsFiles.length === 0) return { findings: 0, status: "skipped" };
|
|
750
|
-
|
|
751
|
-
interface FactIssue {
|
|
752
|
-
file: string;
|
|
753
|
-
line: number;
|
|
754
|
-
ruleId: string;
|
|
755
|
-
severity: string;
|
|
756
|
-
message: string;
|
|
757
|
-
}
|
|
758
|
-
const byRule = new Map<string, FactIssue[]>();
|
|
759
|
-
let findings = 0;
|
|
760
|
-
|
|
761
|
-
for (const filePath of tsFiles) {
|
|
762
|
-
boobooFacts.clearFileFactsFor(filePath);
|
|
763
|
-
const ctx = createDispatchContext(
|
|
764
|
-
filePath,
|
|
765
|
-
targetPath,
|
|
766
|
-
pi,
|
|
767
|
-
boobooFacts,
|
|
768
|
-
false,
|
|
769
|
-
);
|
|
770
|
-
|
|
771
|
-
try {
|
|
772
|
-
await runProviders(ctx);
|
|
773
|
-
} catch {
|
|
774
|
-
continue;
|
|
775
|
-
}
|
|
776
|
-
|
|
777
|
-
const diagnostics = evaluateRules(ctx).filter((d) =>
|
|
778
|
-
FACT_SEVERITY_FILTER.has(d.severity ?? "warning"),
|
|
779
|
-
);
|
|
780
|
-
for (const diag of diagnostics) {
|
|
781
|
-
const relFile = path.relative(targetPath, filePath);
|
|
782
|
-
const bucket = byRule.get(diag.rule ?? diag.id) ?? [];
|
|
783
|
-
bucket.push({
|
|
784
|
-
file: relFile,
|
|
785
|
-
line: diag.line ?? 1,
|
|
786
|
-
ruleId: diag.rule ?? diag.id,
|
|
787
|
-
severity: diag.severity ?? "warning",
|
|
788
|
-
message: diag.message ?? "",
|
|
789
|
-
});
|
|
790
|
-
byRule.set(diag.rule ?? diag.id, bucket);
|
|
791
|
-
findings++;
|
|
792
|
-
}
|
|
793
|
-
}
|
|
794
|
-
|
|
795
|
-
if (findings === 0) return { findings: 0, status: "done" };
|
|
796
|
-
|
|
797
|
-
const errorCount = [...byRule.values()]
|
|
798
|
-
.flat()
|
|
799
|
-
.filter((i) => i.severity === "error").length;
|
|
800
|
-
summaryItems.push({
|
|
801
|
-
category: "Fact Rules",
|
|
802
|
-
count: findings,
|
|
803
|
-
severity: errorCount > 0 ? "๐ด" : "๐ก",
|
|
804
|
-
fixable: true,
|
|
805
|
-
});
|
|
806
|
-
|
|
807
|
-
const sorted = [...byRule.entries()].sort(
|
|
808
|
-
(a, b) => b[1].length - a[1].length,
|
|
809
|
-
);
|
|
810
|
-
let fullSection = `## Fact Rules (Semantic Analysis)\n\n**${findings} issue(s) across ${byRule.size} rule(s)**\n\n`;
|
|
811
|
-
for (const [ruleId, issues] of sorted) {
|
|
812
|
-
const sev = issues[0].severity === "error" ? "๐ด" : "๐ก";
|
|
813
|
-
fullSection += `### ${sev} ${ruleId} (${issues.length})\n\n`;
|
|
814
|
-
fullSection += `${issues[0].message}\n\n`;
|
|
815
|
-
fullSection += "| File | Line |\n|------|------|\n";
|
|
816
|
-
for (const issue of issues.slice(0, 10)) {
|
|
817
|
-
fullSection += `| ${issue.file} | ${issue.line} |\n`;
|
|
818
|
-
}
|
|
819
|
-
if (issues.length > 10)
|
|
820
|
-
fullSection += `| ... | +${issues.length - 10} more |\n`;
|
|
821
|
-
fullSection += "\n";
|
|
822
|
-
}
|
|
823
|
-
fullReport.push(fullSection);
|
|
824
|
-
|
|
825
|
-
return { findings, status: "done" };
|
|
826
|
-
});
|
|
827
|
-
|
|
828
|
-
// Runner 5: TODOs (cache test edit)
|
|
829
|
-
await tracker.run("TODO scanner", async () => {
|
|
830
|
-
const todoResult = clients.todo.scanDirectory(targetPath);
|
|
831
|
-
|
|
832
|
-
if (todoResult.items.length > 0) {
|
|
833
|
-
summaryItems.push({
|
|
834
|
-
category: "TODOs",
|
|
835
|
-
count: todoResult.items.length,
|
|
836
|
-
severity: "โน๏ธ",
|
|
837
|
-
fixable: false,
|
|
838
|
-
});
|
|
839
|
-
|
|
840
|
-
let fullSection = `## TODOs / Annotations\n\n`;
|
|
841
|
-
fullSection += `**${todoResult.items.length} annotation(s) found**\n\n`;
|
|
842
|
-
fullSection +=
|
|
843
|
-
"| Type | File | Line | Text |\n|------|------|------|------|\n";
|
|
844
|
-
for (const item of todoResult.items) {
|
|
845
|
-
fullSection += `| ${item.type} | ${item.file} | ${item.line} | ${item.message} |\n`;
|
|
846
|
-
}
|
|
847
|
-
fullSection += "\n";
|
|
848
|
-
fullReport.push(fullSection);
|
|
849
|
-
}
|
|
850
|
-
|
|
851
|
-
return { findings: todoResult.items.length, status: "done" };
|
|
852
|
-
});
|
|
853
|
-
|
|
854
|
-
// Runner 6: Dead code (JS/TS only โ Knip is a JS/TS tool)
|
|
855
|
-
await tracker.run("dead code (Knip)", async () => {
|
|
856
|
-
if (!langs.has("javascript") && !langs.has("typescript")) {
|
|
857
|
-
return { findings: 0, status: "skipped" };
|
|
858
|
-
}
|
|
859
|
-
if (!(await clients.knip.ensureAvailable())) {
|
|
860
|
-
return { findings: 0, status: "skipped" };
|
|
861
|
-
}
|
|
862
|
-
|
|
863
|
-
const knipResult = await clients.knip.analyze(
|
|
864
|
-
targetPath,
|
|
865
|
-
getKnipIgnorePatterns(),
|
|
866
|
-
);
|
|
867
|
-
|
|
868
|
-
// Filter out test file issues as additional safeguard
|
|
869
|
-
const filteredIssues = knipResult.issues.filter(
|
|
870
|
-
(issue) => !issue.file || shouldIncludeFile(issue.file),
|
|
871
|
-
);
|
|
872
|
-
|
|
873
|
-
if (filteredIssues.length > 0) {
|
|
874
|
-
summaryItems.push({
|
|
875
|
-
category: "Dead Code",
|
|
876
|
-
count: filteredIssues.length,
|
|
877
|
-
severity: "๐ก",
|
|
878
|
-
fixable: true,
|
|
879
|
-
});
|
|
880
|
-
|
|
881
|
-
let fullSection = `## Dead Code (Knip)\n\n`;
|
|
882
|
-
fullSection += `**${filteredIssues.length} issue(s) found**\n\n`;
|
|
883
|
-
fullSection += "| Type | Name | File |\n|------|------|------|\n";
|
|
884
|
-
for (const issue of filteredIssues) {
|
|
885
|
-
fullSection += `| ${issue.type} | ${issue.name} | ${issue.file ?? ""} |\n`;
|
|
886
|
-
}
|
|
887
|
-
fullSection += "\n";
|
|
888
|
-
fullReport.push(fullSection);
|
|
889
|
-
}
|
|
890
|
-
|
|
891
|
-
return { findings: filteredIssues.length, status: "done" };
|
|
892
|
-
});
|
|
893
|
-
|
|
894
|
-
// Runner 7: Duplicate code
|
|
895
|
-
await tracker.run("duplicate code (jscpd)", async () => {
|
|
896
|
-
if (!(await clients.jscpd.ensureAvailable())) {
|
|
897
|
-
return { findings: 0, status: "skipped" };
|
|
898
|
-
}
|
|
899
|
-
|
|
900
|
-
// In TS projects, exclude .js files (they're compiled artifacts)
|
|
901
|
-
const jscpdResult = await clients.jscpd.scan(
|
|
902
|
-
targetPath,
|
|
903
|
-
5,
|
|
904
|
-
50,
|
|
905
|
-
isTsProject,
|
|
906
|
-
);
|
|
907
|
-
|
|
908
|
-
// Filter out test file duplicates using centralized exclusion
|
|
909
|
-
const filteredClones = jscpdResult.clones.filter(
|
|
910
|
-
(dup) => shouldIncludeFile(dup.fileA) && shouldIncludeFile(dup.fileB),
|
|
911
|
-
);
|
|
912
|
-
|
|
913
|
-
if (filteredClones.length > 0) {
|
|
914
|
-
summaryItems.push({
|
|
915
|
-
category: "Duplicates",
|
|
916
|
-
count: filteredClones.length,
|
|
917
|
-
severity: "๐ก",
|
|
918
|
-
fixable: true,
|
|
919
|
-
});
|
|
920
|
-
|
|
921
|
-
let fullSection = `## Code Duplication (jscpd)\n\n`;
|
|
922
|
-
fullSection += `**${filteredClones.length} duplicate block(s) found** (${jscpdResult.duplicatedLines}/${jscpdResult.totalLines} lines, ${jscpdResult.percentage.toFixed(1)}%)\n\n`;
|
|
923
|
-
fullSection +=
|
|
924
|
-
"| File A | Line A | File B | Line B | Lines | Tokens |\n|--------|--------|--------|--------|-------|--------|\n";
|
|
925
|
-
for (const dup of filteredClones) {
|
|
926
|
-
fullSection += `| ${dup.fileA} | ${dup.startA} | ${dup.fileB} | ${dup.startB} | ${dup.lines} | ${dup.tokens} |\n`;
|
|
927
|
-
}
|
|
928
|
-
fullSection += "\n";
|
|
929
|
-
fullReport.push(fullSection);
|
|
930
|
-
}
|
|
931
|
-
|
|
932
|
-
return { findings: filteredClones.length, status: "done" };
|
|
933
|
-
});
|
|
934
|
-
|
|
935
|
-
// Runner 8: Type coverage
|
|
936
|
-
await tracker.run("type coverage", async () => {
|
|
937
|
-
if (!langs.has("typescript")) {
|
|
938
|
-
return { findings: 0, status: "skipped" };
|
|
939
|
-
}
|
|
940
|
-
if (!clients.typeCoverage.isAvailable()) {
|
|
941
|
-
return { findings: 0, status: "skipped" };
|
|
942
|
-
}
|
|
943
|
-
|
|
944
|
-
const tcResult = clients.typeCoverage.scan(targetPath);
|
|
945
|
-
|
|
946
|
-
if (tcResult.percentage < 100) {
|
|
947
|
-
// Filter out test file locations using centralized exclusion
|
|
948
|
-
const filteredLocations = tcResult.untypedLocations.filter((u) =>
|
|
949
|
-
shouldIncludeFile(u.file),
|
|
950
|
-
);
|
|
951
|
-
|
|
952
|
-
const filesWithLowCoverage = new Set(
|
|
953
|
-
filteredLocations
|
|
954
|
-
.filter(() => tcResult.percentage < 90)
|
|
955
|
-
.map((u) => u.file),
|
|
956
|
-
).size;
|
|
957
|
-
|
|
958
|
-
summaryItems.push({
|
|
959
|
-
category: "Type Coverage",
|
|
960
|
-
count: filesWithLowCoverage || 1,
|
|
961
|
-
severity: tcResult.percentage < 90 ? "๐ก" : "โน๏ธ",
|
|
962
|
-
fixable: false,
|
|
963
|
-
});
|
|
964
|
-
|
|
965
|
-
let fullSection = `## Type Coverage\n\n**${tcResult.percentage.toFixed(1)}% typed** (${tcResult.typed}/${tcResult.total} identifiers)\n\n`;
|
|
966
|
-
fullSection +=
|
|
967
|
-
"Type coverage highlights identifiers that resolve to `any` (implicit or explicit). Inferred non-`any` types are treated as typed.\n\n";
|
|
968
|
-
const byFile: Record<string, number> = {};
|
|
969
|
-
for (const u of filteredLocations) {
|
|
970
|
-
byFile[u.file] = (byFile[u.file] || 0) + 1;
|
|
971
|
-
}
|
|
972
|
-
const sortedFiles = Object.entries(byFile)
|
|
973
|
-
.filter(([file]) => shouldIncludeFile(file))
|
|
974
|
-
.sort((a, b) => b[1] - a[1])
|
|
975
|
-
.slice(0, 10);
|
|
976
|
-
|
|
977
|
-
if (sortedFiles.length > 0) {
|
|
978
|
-
fullSection += `### Top Files by Any-Typed Identifier Count\n\n| File | Any-Typed Count |\n|------|-----------------|\n`;
|
|
979
|
-
for (const [file, count] of sortedFiles) {
|
|
980
|
-
fullSection += `| ${file} | ${count} |\n`;
|
|
981
|
-
}
|
|
982
|
-
if (Object.keys(byFile).length > 10) {
|
|
983
|
-
fullSection += `| ... | +${Object.keys(byFile).length - 10} more files |\n`;
|
|
984
|
-
}
|
|
985
|
-
}
|
|
986
|
-
fullSection += "\n";
|
|
987
|
-
fullReport.push(fullSection);
|
|
988
|
-
|
|
989
|
-
return { findings: filesWithLowCoverage || 1, status: "done" };
|
|
990
|
-
}
|
|
991
|
-
|
|
992
|
-
return { findings: 0, status: "done" };
|
|
993
|
-
});
|
|
994
|
-
|
|
995
|
-
// Runner 9: Circular deps (JS/TS only โ Madge is a JS/TS tool)
|
|
996
|
-
await tracker.run("circular deps (Madge)", async () => {
|
|
997
|
-
if (!langs.has("javascript") && !langs.has("typescript")) {
|
|
998
|
-
return { findings: 0, status: "skipped" };
|
|
999
|
-
}
|
|
1000
|
-
if (!(await clients.depChecker.ensureAvailable())) {
|
|
1001
|
-
return { findings: 0, status: "skipped" };
|
|
1002
|
-
}
|
|
1003
|
-
|
|
1004
|
-
const { circular } = await clients.depChecker.scanProject(targetPath);
|
|
1005
|
-
|
|
1006
|
-
// Filter out circular deps involving only test files using centralized exclusion
|
|
1007
|
-
const filteredCircular = circular.filter((dep) => {
|
|
1008
|
-
// Keep if ANY file in the chain is not a test file
|
|
1009
|
-
return dep.path.some((file) => shouldIncludeFile(file));
|
|
1010
|
-
});
|
|
1011
|
-
|
|
1012
|
-
if (filteredCircular.length > 0) {
|
|
1013
|
-
summaryItems.push({
|
|
1014
|
-
category: "Circular Deps",
|
|
1015
|
-
count: filteredCircular.length,
|
|
1016
|
-
severity: "๐ด",
|
|
1017
|
-
fixable: false,
|
|
1018
|
-
});
|
|
1019
|
-
|
|
1020
|
-
let fullSection = `## Circular Dependencies (Madge)\n\n`;
|
|
1021
|
-
fullSection += `**${filteredCircular.length} circular chain(s) found**\n\n`;
|
|
1022
|
-
for (const dep of filteredCircular) {
|
|
1023
|
-
fullSection += `- ${dep.path.join(" โ ")}\n`;
|
|
1024
|
-
}
|
|
1025
|
-
fullReport.push(`${fullSection}\n`);
|
|
1026
|
-
}
|
|
1027
|
-
|
|
1028
|
-
return { findings: filteredCircular.length, status: "done" };
|
|
1029
|
-
});
|
|
1030
|
-
|
|
1031
|
-
// Runner 11: Production Readiness (inspired by pi-validate)
|
|
1032
|
-
await tracker.run("production readiness", async () => {
|
|
1033
|
-
const readiness = validateProductionReadiness(targetPath);
|
|
1034
|
-
|
|
1035
|
-
// Add to summary if not perfect
|
|
1036
|
-
if (readiness.overallScore < 100) {
|
|
1037
|
-
const severity =
|
|
1038
|
-
readiness.grade === "A"
|
|
1039
|
-
? "๐ข"
|
|
1040
|
-
: readiness.grade === "B"
|
|
1041
|
-
? "๐ข"
|
|
1042
|
-
: readiness.grade === "C"
|
|
1043
|
-
? "๐ก"
|
|
1044
|
-
: "๐ ";
|
|
1045
|
-
|
|
1046
|
-
// Count issues across all categories
|
|
1047
|
-
const totalIssues_ = Object.values(readiness.categories).reduce(
|
|
1048
|
-
(sum, cat) => sum + cat.issues.length,
|
|
1049
|
-
0,
|
|
1050
|
-
);
|
|
1051
|
-
|
|
1052
|
-
if (totalIssues_ > 0) {
|
|
1053
|
-
summaryItems.push({
|
|
1054
|
-
category: "Production Readiness",
|
|
1055
|
-
count: totalIssues_,
|
|
1056
|
-
severity: severity as "๐ด" | "๐ก" | "๐ข" | "โน๏ธ",
|
|
1057
|
-
fixable: true,
|
|
1058
|
-
});
|
|
1059
|
-
}
|
|
1060
|
-
}
|
|
1061
|
-
|
|
1062
|
-
// Add to full report
|
|
1063
|
-
let section = `## Production Readiness\n\n`;
|
|
1064
|
-
section += `**Score:** ${readiness.overallScore}/100 **Grade:** ${readiness.grade}\n\n`;
|
|
1065
|
-
|
|
1066
|
-
for (const [key, cat] of Object.entries(readiness.categories)) {
|
|
1067
|
-
section += `### ${key.charAt(0).toUpperCase() + key.slice(1)} (${cat.score}/100)\n\n`;
|
|
1068
|
-
if (cat.details.length > 0) {
|
|
1069
|
-
for (const detail of cat.details) {
|
|
1070
|
-
section += `- ${detail}\n`;
|
|
1071
|
-
}
|
|
1072
|
-
}
|
|
1073
|
-
if (cat.issues.length > 0) {
|
|
1074
|
-
for (const issue of cat.issues) {
|
|
1075
|
-
section += `- โ ๏ธ ${issue}\n`;
|
|
1076
|
-
}
|
|
1077
|
-
}
|
|
1078
|
-
if (cat.details.length === 0 && cat.issues.length === 0) {
|
|
1079
|
-
section += `- โ
No issues\n`;
|
|
1080
|
-
}
|
|
1081
|
-
section += "\n";
|
|
1082
|
-
}
|
|
1083
|
-
|
|
1084
|
-
fullReport.push(section);
|
|
1085
|
-
|
|
1086
|
-
// Add metadata to report
|
|
1087
|
-
const criticalIssues = [];
|
|
1088
|
-
for (const [key, cat] of Object.entries(readiness.categories)) {
|
|
1089
|
-
for (const issue of cat.issues) {
|
|
1090
|
-
// Flag critical issues
|
|
1091
|
-
if (key === "code" && issue.includes("debugger")) {
|
|
1092
|
-
criticalIssues.push(`[CRITICAL] ${issue}`);
|
|
1093
|
-
} else if (key === "tests" && cat.score < 50) {
|
|
1094
|
-
criticalIssues.push(`[CRITICAL] No tests found`);
|
|
1095
|
-
}
|
|
1096
|
-
}
|
|
1097
|
-
}
|
|
1098
|
-
|
|
1099
|
-
return {
|
|
1100
|
-
findings: Object.values(readiness.categories).reduce(
|
|
1101
|
-
(sum, cat) => sum + cat.issues.length,
|
|
1102
|
-
0,
|
|
1103
|
-
),
|
|
1104
|
-
status: "done",
|
|
1105
|
-
};
|
|
1106
|
-
});
|
|
1107
|
-
|
|
1108
|
-
// Runner 12: Compiler checks (language-aware)
|
|
1109
|
-
// Runs the project's native type-checker/compiler for whole-workspace type errors.
|
|
1110
|
-
// Each language uses its canonical batch tool โ these catch cross-file breakage
|
|
1111
|
-
// that per-file LSP checks miss (e.g. broken imports, declaration emit errors).
|
|
1112
|
-
await tracker.run("compiler checks", async () => {
|
|
1113
|
-
interface CompilerIssue {
|
|
1114
|
-
file: string;
|
|
1115
|
-
line: number;
|
|
1116
|
-
col: number;
|
|
1117
|
-
severity: string;
|
|
1118
|
-
code: string;
|
|
1119
|
-
message: string;
|
|
1120
|
-
compiler: string;
|
|
1121
|
-
}
|
|
1122
|
-
|
|
1123
|
-
const issues: CompilerIssue[] = [];
|
|
1124
|
-
|
|
1125
|
-
// TypeScript: tsc --noEmit
|
|
1126
|
-
if (
|
|
1127
|
-
langs.has("typescript") &&
|
|
1128
|
-
nodeFs.existsSync(path.join(targetPath, "tsconfig.json"))
|
|
1129
|
-
) {
|
|
1130
|
-
const result = safeSpawn(
|
|
1131
|
-
"npx",
|
|
1132
|
-
["tsc", "--noEmit", "--pretty", "false"],
|
|
1133
|
-
{
|
|
1134
|
-
cwd: targetPath,
|
|
1135
|
-
timeout: 60_000,
|
|
1136
|
-
},
|
|
1137
|
-
);
|
|
1138
|
-
const output = (result.stdout || "") + (result.stderr || "");
|
|
1139
|
-
// tsc --pretty false format: "file(line,col): error TS####: message"
|
|
1140
|
-
const tscRe =
|
|
1141
|
-
/^(.+?)\((\d+),(\d+)\):\s+(error|warning)\s+(TS\d+):\s+(.+)$/gm;
|
|
1142
|
-
for (const m of output.matchAll(tscRe)) {
|
|
1143
|
-
const [, file, line, col, sev, code, msg] = m;
|
|
1144
|
-
const absFile = path.isAbsolute(file)
|
|
1145
|
-
? file
|
|
1146
|
-
: path.join(targetPath, file);
|
|
1147
|
-
if (shouldIncludeFile(absFile)) {
|
|
1148
|
-
issues.push({
|
|
1149
|
-
file: path.relative(targetPath, absFile),
|
|
1150
|
-
line: parseInt(line, 10),
|
|
1151
|
-
col: parseInt(col, 10),
|
|
1152
|
-
severity: sev,
|
|
1153
|
-
code,
|
|
1154
|
-
message: msg.trim(),
|
|
1155
|
-
compiler: "tsc",
|
|
1156
|
-
});
|
|
1157
|
-
}
|
|
1158
|
-
}
|
|
1159
|
-
}
|
|
1160
|
-
|
|
1161
|
-
// Go: go vet ./...
|
|
1162
|
-
if (langs.has("go") && nodeFs.existsSync(path.join(targetPath, "go.mod"))) {
|
|
1163
|
-
const result = safeSpawn("go", ["vet", "./..."], {
|
|
1164
|
-
cwd: targetPath,
|
|
1165
|
-
timeout: 60_000,
|
|
1166
|
-
});
|
|
1167
|
-
const output = (result.stderr || "") + (result.stdout || "");
|
|
1168
|
-
// go vet format: "file.go:line:col: message" or "file.go:line: message"
|
|
1169
|
-
const goRe = /^(.+\.go):(\d+)(?::(\d+))?:\s+(.+)$/gm;
|
|
1170
|
-
for (const m of output.matchAll(goRe)) {
|
|
1171
|
-
const [, file, line, col, msg] = m;
|
|
1172
|
-
const absFile = path.isAbsolute(file)
|
|
1173
|
-
? file
|
|
1174
|
-
: path.join(targetPath, file);
|
|
1175
|
-
issues.push({
|
|
1176
|
-
file: path.relative(targetPath, absFile),
|
|
1177
|
-
line: parseInt(line, 10),
|
|
1178
|
-
col: col ? parseInt(col, 10) : 1,
|
|
1179
|
-
severity: "error",
|
|
1180
|
-
code: "go-vet",
|
|
1181
|
-
message: msg.trim(),
|
|
1182
|
-
compiler: "go vet",
|
|
1183
|
-
});
|
|
1184
|
-
}
|
|
1185
|
-
}
|
|
1186
|
-
|
|
1187
|
-
// Rust: cargo check --message-format=json
|
|
1188
|
-
if (
|
|
1189
|
-
langs.has("rust") &&
|
|
1190
|
-
nodeFs.existsSync(path.join(targetPath, "Cargo.toml"))
|
|
1191
|
-
) {
|
|
1192
|
-
const result = safeSpawn(
|
|
1193
|
-
"cargo",
|
|
1194
|
-
["check", "--message-format=json", "--quiet"],
|
|
1195
|
-
{
|
|
1196
|
-
cwd: targetPath,
|
|
1197
|
-
timeout: 120_000,
|
|
1198
|
-
},
|
|
1199
|
-
);
|
|
1200
|
-
const output = result.stdout || "";
|
|
1201
|
-
for (const line of output.split("\n")) {
|
|
1202
|
-
if (!line.trim()) continue;
|
|
1203
|
-
try {
|
|
1204
|
-
const msg = JSON.parse(line);
|
|
1205
|
-
if (msg.reason !== "compiler-message") continue;
|
|
1206
|
-
const inner = msg.message;
|
|
1207
|
-
if (!inner || !["error", "warning"].includes(inner.level)) continue;
|
|
1208
|
-
const span =
|
|
1209
|
-
inner.spans?.find((s: { is_primary: boolean }) => s.is_primary) ??
|
|
1210
|
-
inner.spans?.[0];
|
|
1211
|
-
if (!span) continue;
|
|
1212
|
-
const absFile = span.file_name
|
|
1213
|
-
? path.isAbsolute(span.file_name)
|
|
1214
|
-
? span.file_name
|
|
1215
|
-
: path.join(targetPath, span.file_name)
|
|
1216
|
-
: targetPath;
|
|
1217
|
-
issues.push({
|
|
1218
|
-
file: path.relative(targetPath, absFile),
|
|
1219
|
-
line: span.line_start ?? 1,
|
|
1220
|
-
col: span.column_start ?? 1,
|
|
1221
|
-
severity: inner.level,
|
|
1222
|
-
code: inner.code?.code ?? "cargo",
|
|
1223
|
-
message: inner.message,
|
|
1224
|
-
compiler: "cargo check",
|
|
1225
|
-
});
|
|
1226
|
-
} catch {
|
|
1227
|
-
// non-JSON line
|
|
1228
|
-
}
|
|
1229
|
-
}
|
|
1230
|
-
}
|
|
1231
|
-
|
|
1232
|
-
// Python: pyright --outputjson (preferred) or mypy
|
|
1233
|
-
if (langs.has("python")) {
|
|
1234
|
-
const hasPyright =
|
|
1235
|
-
safeSpawn("pyright", ["--version"], { timeout: 5_000 }).status === 0;
|
|
1236
|
-
if (hasPyright) {
|
|
1237
|
-
const result = safeSpawn("pyright", ["--outputjson", "."], {
|
|
1238
|
-
cwd: targetPath,
|
|
1239
|
-
timeout: 60_000,
|
|
1240
|
-
});
|
|
1241
|
-
const output = result.stdout || "";
|
|
1242
|
-
try {
|
|
1243
|
-
const json = JSON.parse(output);
|
|
1244
|
-
for (const diag of json?.generalDiagnostics ?? []) {
|
|
1245
|
-
if (!["error", "warning"].includes(diag.severity)) continue;
|
|
1246
|
-
const absFile = diag.file
|
|
1247
|
-
? path.isAbsolute(diag.file)
|
|
1248
|
-
? diag.file
|
|
1249
|
-
: path.join(targetPath, diag.file)
|
|
1250
|
-
: targetPath;
|
|
1251
|
-
if (shouldIncludeFile(absFile)) {
|
|
1252
|
-
issues.push({
|
|
1253
|
-
file: path.relative(targetPath, absFile),
|
|
1254
|
-
line: (diag.range?.start?.line ?? 0) + 1,
|
|
1255
|
-
col: (diag.range?.start?.character ?? 0) + 1,
|
|
1256
|
-
severity: diag.severity,
|
|
1257
|
-
code: diag.rule ?? "pyright",
|
|
1258
|
-
message: diag.message,
|
|
1259
|
-
compiler: "pyright",
|
|
1260
|
-
});
|
|
1261
|
-
}
|
|
1262
|
-
}
|
|
1263
|
-
} catch {
|
|
1264
|
-
// pyright didn't produce valid JSON
|
|
1265
|
-
}
|
|
1266
|
-
}
|
|
1267
|
-
}
|
|
1268
|
-
|
|
1269
|
-
// Ruby: rubocop --format json
|
|
1270
|
-
if (
|
|
1271
|
-
langs.has("ruby") &&
|
|
1272
|
-
nodeFs.existsSync(path.join(targetPath, "Gemfile"))
|
|
1273
|
-
) {
|
|
1274
|
-
const hasRubocop =
|
|
1275
|
-
safeSpawn("rubocop", ["--version"], { timeout: 5_000 }).status === 0;
|
|
1276
|
-
if (hasRubocop) {
|
|
1277
|
-
const result = safeSpawn(
|
|
1278
|
-
"rubocop",
|
|
1279
|
-
[
|
|
1280
|
-
"--format",
|
|
1281
|
-
"json",
|
|
1282
|
-
"--no-color",
|
|
1283
|
-
"--display-only-fail-level-offenses",
|
|
1284
|
-
],
|
|
1285
|
-
{ cwd: targetPath, timeout: 60_000 },
|
|
1286
|
-
);
|
|
1287
|
-
const output = result.stdout || "";
|
|
1288
|
-
try {
|
|
1289
|
-
const json = JSON.parse(output);
|
|
1290
|
-
for (const fileResult of json?.files ?? []) {
|
|
1291
|
-
const absFile = path.isAbsolute(fileResult.path)
|
|
1292
|
-
? fileResult.path
|
|
1293
|
-
: path.join(targetPath, fileResult.path);
|
|
1294
|
-
if (!shouldIncludeFile(absFile)) continue;
|
|
1295
|
-
for (const offense of fileResult.offenses ?? []) {
|
|
1296
|
-
const sev =
|
|
1297
|
-
offense.severity === "error" || offense.severity === "fatal"
|
|
1298
|
-
? "error"
|
|
1299
|
-
: "warning";
|
|
1300
|
-
issues.push({
|
|
1301
|
-
file: path.relative(targetPath, absFile),
|
|
1302
|
-
line: offense.location?.line ?? 1,
|
|
1303
|
-
col: offense.location?.column ?? 1,
|
|
1304
|
-
severity: sev,
|
|
1305
|
-
code: offense.cop_name ?? "rubocop",
|
|
1306
|
-
message: offense.message ?? "",
|
|
1307
|
-
compiler: "rubocop",
|
|
1308
|
-
});
|
|
1309
|
-
}
|
|
1310
|
-
}
|
|
1311
|
-
} catch {
|
|
1312
|
-
// rubocop didn't produce valid JSON
|
|
1313
|
-
}
|
|
1314
|
-
}
|
|
1315
|
-
}
|
|
1316
|
-
|
|
1317
|
-
// Java: mvn compile (preferred) or javac on discovered .java files
|
|
1318
|
-
if (nodeFs.existsSync(path.join(targetPath, "pom.xml"))) {
|
|
1319
|
-
const result = safeSpawn(
|
|
1320
|
-
"mvn",
|
|
1321
|
-
["compile", "-q", "-B", "--no-transfer-progress"],
|
|
1322
|
-
{
|
|
1323
|
-
cwd: targetPath,
|
|
1324
|
-
timeout: 120_000,
|
|
1325
|
-
},
|
|
1326
|
-
);
|
|
1327
|
-
const output = (result.stdout || "") + (result.stderr || "");
|
|
1328
|
-
const mvnRe = /^\[ERROR\]\s+([^\s[]+\.java):\[(\d+),(\d+)\]\s+([^\n]+)/gm;
|
|
1329
|
-
for (const m of output.matchAll(mvnRe)) {
|
|
1330
|
-
const [, file, line, col, msg] = m;
|
|
1331
|
-
const absFile = path.isAbsolute(file)
|
|
1332
|
-
? file
|
|
1333
|
-
: path.join(targetPath, file);
|
|
1334
|
-
if (shouldIncludeFile(absFile)) {
|
|
1335
|
-
issues.push({
|
|
1336
|
-
file: path.relative(targetPath, absFile),
|
|
1337
|
-
line: parseInt(line, 10),
|
|
1338
|
-
col: parseInt(col, 10),
|
|
1339
|
-
severity: "error",
|
|
1340
|
-
code: "javac",
|
|
1341
|
-
message: msg.trim(),
|
|
1342
|
-
compiler: "mvn compile",
|
|
1343
|
-
});
|
|
1344
|
-
}
|
|
1345
|
-
}
|
|
1346
|
-
} else if (
|
|
1347
|
-
nodeFs.existsSync(path.join(targetPath, "build.gradle")) ||
|
|
1348
|
-
nodeFs.existsSync(path.join(targetPath, "build.gradle.kts"))
|
|
1349
|
-
) {
|
|
1350
|
-
const gradleCmd = nodeFs.existsSync(path.join(targetPath, "gradlew"))
|
|
1351
|
-
? "./gradlew"
|
|
1352
|
-
: "gradle";
|
|
1353
|
-
const result = safeSpawn(gradleCmd, ["compileJava", "--quiet"], {
|
|
1354
|
-
cwd: targetPath,
|
|
1355
|
-
timeout: 120_000,
|
|
1356
|
-
});
|
|
1357
|
-
const output = (result.stdout || "") + (result.stderr || "");
|
|
1358
|
-
const gradleRe =
|
|
1359
|
-
/^([^\s:]+\.(?:java|kt)):\s*(\d+):\s*(?:error|warning):\s+([^\n]+)/gm;
|
|
1360
|
-
for (const m of output.matchAll(gradleRe)) {
|
|
1361
|
-
const [, file, line, msg] = m;
|
|
1362
|
-
const absFile = path.isAbsolute(file)
|
|
1363
|
-
? file
|
|
1364
|
-
: path.join(targetPath, file);
|
|
1365
|
-
if (shouldIncludeFile(absFile)) {
|
|
1366
|
-
issues.push({
|
|
1367
|
-
file: path.relative(targetPath, absFile),
|
|
1368
|
-
line: parseInt(line, 10),
|
|
1369
|
-
col: 1,
|
|
1370
|
-
severity: "error",
|
|
1371
|
-
code: "gradle",
|
|
1372
|
-
message: msg.trim(),
|
|
1373
|
-
compiler: "gradle compileJava",
|
|
1374
|
-
});
|
|
1375
|
-
}
|
|
1376
|
-
}
|
|
1377
|
-
}
|
|
1378
|
-
|
|
1379
|
-
// C#: dotnet build
|
|
1380
|
-
if (
|
|
1381
|
-
nodeFs
|
|
1382
|
-
.readdirSync(targetPath)
|
|
1383
|
-
.some((e) => /\.(sln|slnx|csproj)$/i.test(e))
|
|
1384
|
-
) {
|
|
1385
|
-
const hasDotnet =
|
|
1386
|
-
safeSpawn("dotnet", ["--version"], { timeout: 5_000 }).status === 0;
|
|
1387
|
-
if (hasDotnet) {
|
|
1388
|
-
const result = safeSpawn(
|
|
1389
|
-
"dotnet",
|
|
1390
|
-
["build", "--no-incremental", "-v", "minimal"],
|
|
1391
|
-
{ cwd: targetPath, timeout: 120_000 },
|
|
1392
|
-
);
|
|
1393
|
-
const output = (result.stdout || "") + (result.stderr || "");
|
|
1394
|
-
const csRe =
|
|
1395
|
-
/^([^\s(]+\.cs)\((\d+),(\d+)\):\s+(error|warning)\s+([A-Z]+\d+):\s+([^[]+)/gm;
|
|
1396
|
-
for (const m of output.matchAll(csRe)) {
|
|
1397
|
-
const [, file, line, col, sev, code, msg] = m;
|
|
1398
|
-
const absFile = path.isAbsolute(file)
|
|
1399
|
-
? file
|
|
1400
|
-
: path.join(targetPath, file);
|
|
1401
|
-
if (shouldIncludeFile(absFile)) {
|
|
1402
|
-
issues.push({
|
|
1403
|
-
file: path.relative(targetPath, absFile),
|
|
1404
|
-
line: parseInt(line, 10),
|
|
1405
|
-
col: parseInt(col, 10),
|
|
1406
|
-
severity: sev,
|
|
1407
|
-
code,
|
|
1408
|
-
message: msg.trim(),
|
|
1409
|
-
compiler: "dotnet build",
|
|
1410
|
-
});
|
|
1411
|
-
}
|
|
1412
|
-
}
|
|
1413
|
-
}
|
|
1414
|
-
}
|
|
1415
|
-
|
|
1416
|
-
// Dart: dart analyze
|
|
1417
|
-
if (nodeFs.existsSync(path.join(targetPath, "pubspec.yaml"))) {
|
|
1418
|
-
const hasDart =
|
|
1419
|
-
safeSpawn("dart", ["--version"], { timeout: 5_000 }).status === 0;
|
|
1420
|
-
if (hasDart) {
|
|
1421
|
-
const result = safeSpawn("dart", ["analyze", "--format=machine"], {
|
|
1422
|
-
cwd: targetPath,
|
|
1423
|
-
timeout: 60_000,
|
|
1424
|
-
});
|
|
1425
|
-
const output = (result.stdout || "") + (result.stderr || "");
|
|
1426
|
-
// severity|type|code|file|line|col|length|message
|
|
1427
|
-
for (const line of output.split(/\r?\n/)) {
|
|
1428
|
-
const parts = line.split("|");
|
|
1429
|
-
if (parts.length < 8) continue;
|
|
1430
|
-
const [sev, , code, file, lineNum, , , ...msgParts] = parts;
|
|
1431
|
-
const absFile = path.isAbsolute(file)
|
|
1432
|
-
? file
|
|
1433
|
-
: path.join(targetPath, file);
|
|
1434
|
-
if (!shouldIncludeFile(absFile)) continue;
|
|
1435
|
-
issues.push({
|
|
1436
|
-
file: path.relative(targetPath, absFile),
|
|
1437
|
-
line: parseInt(lineNum, 10),
|
|
1438
|
-
col: 1,
|
|
1439
|
-
severity: sev.toLowerCase() === "error" ? "error" : "warning",
|
|
1440
|
-
code: code.trim(),
|
|
1441
|
-
message: msgParts.join("|").trim(),
|
|
1442
|
-
compiler: "dart analyze",
|
|
1443
|
-
});
|
|
1444
|
-
}
|
|
1445
|
-
}
|
|
1446
|
-
}
|
|
1447
|
-
|
|
1448
|
-
// Gleam: gleam check
|
|
1449
|
-
if (nodeFs.existsSync(path.join(targetPath, "gleam.toml"))) {
|
|
1450
|
-
const hasGleam =
|
|
1451
|
-
safeSpawn("gleam", ["--version"], { timeout: 5_000 }).status === 0;
|
|
1452
|
-
if (hasGleam) {
|
|
1453
|
-
const result = safeSpawn("gleam", ["check"], {
|
|
1454
|
-
cwd: targetPath,
|
|
1455
|
-
timeout: 60_000,
|
|
1456
|
-
});
|
|
1457
|
-
const output = (result.stdout || "") + (result.stderr || "");
|
|
1458
|
-
const gleamLines = output.split("\n");
|
|
1459
|
-
const gleamHeaderRe = /^([^:]+):(\d+):(\d+)[ \t]*(?:error|warning)/;
|
|
1460
|
-
for (let i = 0; i < gleamLines.length; i++) {
|
|
1461
|
-
const m = gleamHeaderRe.exec(gleamLines[i]);
|
|
1462
|
-
if (!m) continue;
|
|
1463
|
-
const [, file, line, col] = m;
|
|
1464
|
-
const msg = gleamLines[i + 1]?.trim() ?? "";
|
|
1465
|
-
const absFile = path.isAbsolute(file)
|
|
1466
|
-
? file
|
|
1467
|
-
: path.join(targetPath, file);
|
|
1468
|
-
if (shouldIncludeFile(absFile)) {
|
|
1469
|
-
issues.push({
|
|
1470
|
-
file: path.relative(targetPath, absFile),
|
|
1471
|
-
line: parseInt(line, 10),
|
|
1472
|
-
col: parseInt(col, 10),
|
|
1473
|
-
severity: "error",
|
|
1474
|
-
code: "gleam",
|
|
1475
|
-
message: msg,
|
|
1476
|
-
compiler: "gleam check",
|
|
1477
|
-
});
|
|
1478
|
-
}
|
|
1479
|
-
}
|
|
1480
|
-
}
|
|
1481
|
-
}
|
|
1482
|
-
|
|
1483
|
-
// Zig: zig build (or zig check on entry files)
|
|
1484
|
-
if (nodeFs.existsSync(path.join(targetPath, "build.zig"))) {
|
|
1485
|
-
const hasZig =
|
|
1486
|
-
safeSpawn("zig", ["version"], { timeout: 5_000 }).status === 0;
|
|
1487
|
-
if (hasZig) {
|
|
1488
|
-
const result = safeSpawn("zig", ["build", "--summary", "none"], {
|
|
1489
|
-
cwd: targetPath,
|
|
1490
|
-
timeout: 120_000,
|
|
1491
|
-
});
|
|
1492
|
-
const output = (result.stdout || "") + (result.stderr || "");
|
|
1493
|
-
const zigLineRe =
|
|
1494
|
-
/^([^:]+):(\d+):(\d+):[ \t]*(error|warning|note):[ \t]*(.+)/;
|
|
1495
|
-
for (const zigLine of output.split("\n")) {
|
|
1496
|
-
const m = zigLineRe.exec(zigLine);
|
|
1497
|
-
if (!m) continue;
|
|
1498
|
-
const [, file, line, col, sev, msg] = m;
|
|
1499
|
-
const absFile = path.isAbsolute(file)
|
|
1500
|
-
? file
|
|
1501
|
-
: path.join(targetPath, file);
|
|
1502
|
-
if (!absFile.includes("zig-cache") && shouldIncludeFile(absFile)) {
|
|
1503
|
-
issues.push({
|
|
1504
|
-
file: path.relative(targetPath, absFile),
|
|
1505
|
-
line: parseInt(line, 10),
|
|
1506
|
-
col: parseInt(col, 10),
|
|
1507
|
-
severity: sev === "error" ? "error" : "warning",
|
|
1508
|
-
code: "zig",
|
|
1509
|
-
message: msg.trim(),
|
|
1510
|
-
compiler: "zig build",
|
|
1511
|
-
});
|
|
1512
|
-
}
|
|
1513
|
-
}
|
|
1514
|
-
}
|
|
1515
|
-
}
|
|
1516
|
-
|
|
1517
|
-
// Elixir: mix compile
|
|
1518
|
-
if (nodeFs.existsSync(path.join(targetPath, "mix.exs"))) {
|
|
1519
|
-
const hasMix =
|
|
1520
|
-
safeSpawn("mix", ["--version"], { timeout: 5_000 }).status === 0;
|
|
1521
|
-
if (hasMix) {
|
|
1522
|
-
const result = safeSpawn("mix", ["compile", "--warnings-as-errors"], {
|
|
1523
|
-
cwd: targetPath,
|
|
1524
|
-
timeout: 60_000,
|
|
1525
|
-
});
|
|
1526
|
-
const output = (result.stdout || "") + (result.stderr || "");
|
|
1527
|
-
const elixirLines = output.split(/\r?\n/);
|
|
1528
|
-
for (let i = 0; i < elixirLines.length; i++) {
|
|
1529
|
-
const line = elixirLines[i];
|
|
1530
|
-
const prefixMatch = line.match(/^(?:warning|error):\s+/);
|
|
1531
|
-
if (!prefixMatch) continue;
|
|
1532
|
-
const msg = line.slice(prefixMatch[0].length);
|
|
1533
|
-
const nextLine = elixirLines[i + 1];
|
|
1534
|
-
if (!nextLine) continue;
|
|
1535
|
-
const locMatch = nextLine.trim().match(/^([^:]+):(\d+)$/);
|
|
1536
|
-
if (!locMatch) continue;
|
|
1537
|
-
const [, file, lineNum] = locMatch;
|
|
1538
|
-
const absFile = path.isAbsolute(file)
|
|
1539
|
-
? file
|
|
1540
|
-
: path.join(targetPath, file);
|
|
1541
|
-
if (shouldIncludeFile(absFile)) {
|
|
1542
|
-
issues.push({
|
|
1543
|
-
file: path.relative(targetPath, absFile),
|
|
1544
|
-
line: parseInt(lineNum, 10),
|
|
1545
|
-
col: 1,
|
|
1546
|
-
severity: "warning",
|
|
1547
|
-
code: "mix",
|
|
1548
|
-
message: msg.trim(),
|
|
1549
|
-
compiler: "mix compile",
|
|
1550
|
-
});
|
|
1551
|
-
}
|
|
1552
|
-
}
|
|
1553
|
-
}
|
|
1554
|
-
}
|
|
1555
|
-
|
|
1556
|
-
if (issues.length === 0) return { findings: 0, status: "done" };
|
|
1557
|
-
|
|
1558
|
-
// Group by compiler for reporting
|
|
1559
|
-
const byCompiler: Record<string, CompilerIssue[]> = {};
|
|
1560
|
-
for (const issue of issues) {
|
|
1561
|
-
if (!byCompiler[issue.compiler]) byCompiler[issue.compiler] = [];
|
|
1562
|
-
byCompiler[issue.compiler].push(issue);
|
|
1563
|
-
}
|
|
1564
|
-
|
|
1565
|
-
const errorCount = issues.filter((i) => i.severity === "error").length;
|
|
1566
|
-
summaryItems.push({
|
|
1567
|
-
category: "Compiler Errors",
|
|
1568
|
-
count: issues.length,
|
|
1569
|
-
severity: errorCount > 0 ? "๐ด" : "๐ก",
|
|
1570
|
-
fixable: true,
|
|
1571
|
-
});
|
|
1572
|
-
|
|
1573
|
-
let fullSection = `## Compiler Checks\n\n**${issues.length} issue(s) found** (${errorCount} errors)\n\n`;
|
|
1574
|
-
for (const [compiler, compIssues] of Object.entries(byCompiler)) {
|
|
1575
|
-
fullSection += `### ${compiler} (${compIssues.length})\n\n`;
|
|
1576
|
-
fullSection +=
|
|
1577
|
-
"| File | Line | Code | Message |\n|------|------|------|---------|\n";
|
|
1578
|
-
for (const issue of compIssues.slice(0, 30)) {
|
|
1579
|
-
const sev = issue.severity === "error" ? "๐ด" : "๐ก";
|
|
1580
|
-
fullSection += `| ${issue.file} | ${issue.line} | ${sev} ${issue.code} | ${issue.message} |\n`;
|
|
1581
|
-
}
|
|
1582
|
-
if (compIssues.length > 30) {
|
|
1583
|
-
fullSection += `| ... | | | +${compIssues.length - 30} more |\n`;
|
|
1584
|
-
}
|
|
1585
|
-
fullSection += "\n";
|
|
1586
|
-
}
|
|
1587
|
-
fullReport.push(fullSection);
|
|
1588
|
-
|
|
1589
|
-
return { findings: issues.length, status: "done" };
|
|
1590
|
-
});
|
|
1591
|
-
|
|
1592
|
-
// --- Create structured JSON report ---
|
|
1593
|
-
nodeFs.mkdirSync(reviewDir, { recursive: true });
|
|
1594
|
-
const projectName = path.basename(reviewRoot);
|
|
1595
|
-
|
|
1596
|
-
const totalIssues = summaryItems.reduce((sum, s) => sum + s.count, 0);
|
|
1597
|
-
const fixableCount = summaryItems
|
|
1598
|
-
.filter((s) => s.fixable)
|
|
1599
|
-
.reduce((sum, s) => sum + s.count, 0);
|
|
1600
|
-
const refactorNeeded = summaryItems
|
|
1601
|
-
.filter((s) => !s.fixable)
|
|
1602
|
-
.reduce((sum, s) => sum + s.count, 0);
|
|
1603
|
-
|
|
1604
|
-
// Build runner summary
|
|
1605
|
-
const runnerSummary = tracker.getRunners().map((r) => ({
|
|
1606
|
-
name: r.name,
|
|
1607
|
-
status: r.status,
|
|
1608
|
-
findings: r.findings,
|
|
1609
|
-
time: formatElapsed(r.elapsedMs),
|
|
1610
|
-
}));
|
|
1611
|
-
|
|
1612
|
-
const jsonReport = {
|
|
1613
|
-
meta: {
|
|
1614
|
-
timestamp: new Date().toISOString(),
|
|
1615
|
-
project: projectName,
|
|
1616
|
-
path: targetPath,
|
|
1617
|
-
totalIssues,
|
|
1618
|
-
fixableCount,
|
|
1619
|
-
refactorNeeded,
|
|
1620
|
-
// New: runner execution details
|
|
1621
|
-
runners: runnerSummary,
|
|
1622
|
-
totalTime: formatElapsed(
|
|
1623
|
-
runnerSummary.reduce((sum, r) => {
|
|
1624
|
-
const ms = r.time.endsWith("ms")
|
|
1625
|
-
? parseInt(r.time, 10)
|
|
1626
|
-
: parseFloat(r.time) * 1000;
|
|
1627
|
-
return sum + (Number.isNaN(ms) ? 0 : ms);
|
|
1628
|
-
}, 0),
|
|
1629
|
-
),
|
|
1630
|
-
},
|
|
1631
|
-
// New: project metadata
|
|
1632
|
-
project: {
|
|
1633
|
-
type: projectMeta.type,
|
|
1634
|
-
name: projectMeta.name,
|
|
1635
|
-
version: projectMeta.version,
|
|
1636
|
-
packageManager: projectMeta.packageManager,
|
|
1637
|
-
languages: projectMeta.languages,
|
|
1638
|
-
hasTests: projectMeta.hasTests,
|
|
1639
|
-
testFramework: projectMeta.testFramework,
|
|
1640
|
-
hasLinting: projectMeta.hasLinting,
|
|
1641
|
-
linter: projectMeta.linter,
|
|
1642
|
-
hasFormatting: projectMeta.hasFormatting,
|
|
1643
|
-
formatter: projectMeta.formatter,
|
|
1644
|
-
hasTypeScript: projectMeta.hasTypeScript,
|
|
1645
|
-
configFiles: projectMeta.configFiles,
|
|
1646
|
-
scripts: projectMeta.scripts,
|
|
1647
|
-
},
|
|
1648
|
-
// New: available commands for the project
|
|
1649
|
-
commands: availableCommands,
|
|
1650
|
-
byCategory: summaryItems.reduce(
|
|
1651
|
-
(acc, item) => {
|
|
1652
|
-
acc[item.category] = {
|
|
1653
|
-
count: item.count,
|
|
1654
|
-
severity: item.severity,
|
|
1655
|
-
fixable: item.fixable,
|
|
1656
|
-
falsePositivePrefix: `${categoryKey(item.category)}:`,
|
|
1657
|
-
};
|
|
1658
|
-
return acc;
|
|
1659
|
-
},
|
|
1660
|
-
{} as Record<
|
|
1661
|
-
string,
|
|
1662
|
-
{
|
|
1663
|
-
count: number;
|
|
1664
|
-
severity: string;
|
|
1665
|
-
fixable: boolean;
|
|
1666
|
-
falsePositivePrefix: string;
|
|
1667
|
-
}
|
|
1668
|
-
>,
|
|
1669
|
-
),
|
|
1670
|
-
howToMarkFalsePositive: {
|
|
1671
|
-
command: "Ignore via AGENTS.md rules or suppress comments",
|
|
1672
|
-
format: "Add to .claude/rules or use biome/oxlint ignore comments",
|
|
1673
|
-
examples: [
|
|
1674
|
-
"// biome-ignore lint/suspicious/noConsole: intentional debug",
|
|
1675
|
-
"// oxlint-disable-next-line no-console",
|
|
1676
|
-
],
|
|
1677
|
-
},
|
|
1678
|
-
sessionFile: path.join(reviewRoot, ".pi-lens", "fix-session.json"),
|
|
1679
|
-
details: fullReport.join("\n"),
|
|
1680
|
-
};
|
|
1681
|
-
|
|
1682
|
-
const jsonPath = path.join(reviewDir, `booboo-${timestamp}.json`);
|
|
1683
|
-
nodeFs.writeFileSync(jsonPath, JSON.stringify(jsonReport, null, 2), "utf-8");
|
|
1684
|
-
|
|
1685
|
-
// --- Create markdown report ---
|
|
1686
|
-
|
|
1687
|
-
// Build project info section
|
|
1688
|
-
let projectSection = `## Project Info\n\n**Type:** ${projectMeta.type}`;
|
|
1689
|
-
if (projectMeta.name) projectSection += ` | **Name:** ${projectMeta.name}`;
|
|
1690
|
-
if (projectMeta.version)
|
|
1691
|
-
projectSection += ` | **Version:** ${projectMeta.version}`;
|
|
1692
|
-
if (projectMeta.packageManager)
|
|
1693
|
-
projectSection += `\n**Package Manager:** ${projectMeta.packageManager}`;
|
|
1694
|
-
if (projectMeta.languages.length > 0)
|
|
1695
|
-
projectSection += `\n**Languages:** ${projectMeta.languages.join(", ")}`;
|
|
1696
|
-
|
|
1697
|
-
// Tools
|
|
1698
|
-
const tools: string[] = [];
|
|
1699
|
-
if (projectMeta.testFramework) tools.push(`๐งช ${projectMeta.testFramework}`);
|
|
1700
|
-
else if (projectMeta.hasTests) tools.push("๐งช tests");
|
|
1701
|
-
if (projectMeta.linter) tools.push(`๐ ${projectMeta.linter}`);
|
|
1702
|
-
if (projectMeta.formatter) tools.push(`โจ ${projectMeta.formatter}`);
|
|
1703
|
-
if (tools.length > 0) projectSection += `\n**Tools:** ${tools.join(" | ")}`;
|
|
1704
|
-
|
|
1705
|
-
// Available commands
|
|
1706
|
-
if (availableCommands.length > 0) {
|
|
1707
|
-
projectSection += `\n\n### Available Commands\n\n| Action | Command |\n|--------|---------|`;
|
|
1708
|
-
for (const cmd of availableCommands) {
|
|
1709
|
-
projectSection += `\n| ${cmd.action} | \`${cmd.command}\` |`;
|
|
1710
|
-
}
|
|
1711
|
-
}
|
|
1712
|
-
|
|
1713
|
-
const mdReport = `# Code Review: ${projectName}
|
|
1714
|
-
|
|
1715
|
-
**Scanned:** ${jsonReport.meta.timestamp}
|
|
1716
|
-
**Path:** \`${targetPath}\`
|
|
1717
|
-
**Summary:** ${jsonReport.meta.totalIssues} issues | ${jsonReport.meta.fixableCount} fixable | ${jsonReport.meta.refactorNeeded} need refactor
|
|
1718
|
-
**Total Time:** ${jsonReport.meta.totalTime}
|
|
1719
|
-
|
|
1720
|
-
${projectSection}
|
|
1721
|
-
|
|
1722
|
-
## Runner Summary
|
|
1723
|
-
|
|
1724
|
-
| Runner | Status | Findings | Time |
|
|
1725
|
-
|--------|--------|----------|------|
|
|
1726
|
-
${runnerSummary.map((r) => `| ${r.name} | ${r.status} | ${r.findings} | ${r.time} |`).join("\n")}
|
|
1727
|
-
|
|
1728
|
-
---
|
|
1729
|
-
|
|
1730
|
-
${fullReport.join("\n")}`;
|
|
1731
|
-
|
|
1732
|
-
const mdPath = path.join(reviewDir, `booboo-${timestamp}.md`);
|
|
1733
|
-
nodeFs.writeFileSync(mdPath, mdReport, "utf-8");
|
|
1734
|
-
|
|
1735
|
-
// --- Brief terminal summary ---
|
|
1736
|
-
if (summaryItems.length === 0) {
|
|
1737
|
-
ctx.ui.notify("โ Code review clean", "info");
|
|
1738
|
-
} else {
|
|
1739
|
-
const { totalIssues } = jsonReport.meta;
|
|
1740
|
-
|
|
1741
|
-
// Build runner lines for terminal output
|
|
1742
|
-
const runnerLines = tracker
|
|
1743
|
-
.getRunners()
|
|
1744
|
-
.filter((r) => r.findings > 0)
|
|
1745
|
-
.map(
|
|
1746
|
-
(r) =>
|
|
1747
|
-
` ${r.status === "error" ? "โ" : "โ "} ${r.name}: ${r.findings} finding${r.findings !== 1 ? "s" : ""} (${formatElapsed(r.elapsedMs)})`,
|
|
1748
|
-
);
|
|
1749
|
-
|
|
1750
|
-
const summaryLines = [
|
|
1751
|
-
`๐ Code Review: ${totalIssues} issues`,
|
|
1752
|
-
...runnerLines,
|
|
1753
|
-
` โฑ๏ธ Total: ${jsonReport.meta.totalTime}`,
|
|
1754
|
-
`๐ MD: ${mdPath}`,
|
|
1755
|
-
];
|
|
1756
|
-
|
|
1757
|
-
ctx.ui.notify(summaryLines.join("\n"), "info");
|
|
1758
|
-
}
|
|
1759
|
-
}
|
|
1760
|
-
|
|
1761
|
-
// ============================================================================
|
|
1762
|
-
// Semantic Similarity Helper
|
|
1763
|
-
// ============================================================================
|
|
1764
|
-
|
|
1765
|
-
interface SimilarPair {
|
|
1766
|
-
func1: string;
|
|
1767
|
-
func2: string;
|
|
1768
|
-
similarity: number;
|
|
1769
|
-
}
|
|
1770
|
-
|
|
1771
|
-
const SEMANTIC_SIMILARITY_THRESHOLD = 0.98;
|
|
1772
|
-
const MIN_SIMILARITY_TRANSITIONS = 40;
|
|
1773
|
-
const MAX_TRANSITION_RATIO = 1.8;
|
|
1774
|
-
|
|
1775
|
-
/**
|
|
1776
|
-
* Find top N most similar function pairs in the project index
|
|
1777
|
-
* Uses canonical pair ordering to avoid duplicates (A,B) vs (B,A)
|
|
1778
|
-
*/
|
|
1779
|
-
function findTopSimilarPairs(
|
|
1780
|
-
index: ProjectIndex,
|
|
1781
|
-
maxPairs: number,
|
|
1782
|
-
): SimilarPair[] {
|
|
1783
|
-
const entries = Array.from(index.entries.values());
|
|
1784
|
-
const seenPairs = new Set<string>();
|
|
1785
|
-
const pairs: SimilarPair[] = [];
|
|
1786
|
-
|
|
1787
|
-
for (let i = 0; i < entries.length; i++) {
|
|
1788
|
-
for (let j = i + 1; j < entries.length; j++) {
|
|
1789
|
-
const entry1 = entries[i];
|
|
1790
|
-
const entry2 = entries[j];
|
|
1791
|
-
|
|
1792
|
-
// Skip if same file (we want cross-file duplicates)
|
|
1793
|
-
if (entry1.filePath === entry2.filePath) continue;
|
|
1794
|
-
|
|
1795
|
-
// Skip low-signal functions where matrix noise dominates.
|
|
1796
|
-
if (
|
|
1797
|
-
entry1.transitionCount < MIN_SIMILARITY_TRANSITIONS ||
|
|
1798
|
-
entry2.transitionCount < MIN_SIMILARITY_TRANSITIONS
|
|
1799
|
-
) {
|
|
1800
|
-
continue;
|
|
1801
|
-
}
|
|
1802
|
-
|
|
1803
|
-
// Skip pairs with very different complexity/size; these are often
|
|
1804
|
-
// boilerplate-wrapper false positives (shared try/catch/logging shell).
|
|
1805
|
-
const maxTransitions = Math.max(
|
|
1806
|
-
entry1.transitionCount,
|
|
1807
|
-
entry2.transitionCount,
|
|
1808
|
-
);
|
|
1809
|
-
const minTransitions = Math.min(
|
|
1810
|
-
entry1.transitionCount,
|
|
1811
|
-
entry2.transitionCount,
|
|
1812
|
-
);
|
|
1813
|
-
if (minTransitions <= 0) continue;
|
|
1814
|
-
if (maxTransitions / minTransitions > MAX_TRANSITION_RATIO) continue;
|
|
1815
|
-
|
|
1816
|
-
const similarity = calculateSimilarity(entry1.matrix, entry2.matrix);
|
|
1817
|
-
|
|
1818
|
-
if (similarity >= SEMANTIC_SIMILARITY_THRESHOLD) {
|
|
1819
|
-
// Canonical pair key (sorted to avoid duplicates)
|
|
1820
|
-
const pairKey = [entry1.id, entry2.id]
|
|
1821
|
-
.sort((a, b) => a.localeCompare(b))
|
|
1822
|
-
.join("::");
|
|
1823
|
-
if (seenPairs.has(pairKey)) continue;
|
|
1824
|
-
seenPairs.add(pairKey);
|
|
1825
|
-
|
|
1826
|
-
pairs.push({
|
|
1827
|
-
func1: entry1.id,
|
|
1828
|
-
func2: entry2.id,
|
|
1829
|
-
similarity,
|
|
1830
|
-
});
|
|
1831
|
-
}
|
|
1832
|
-
}
|
|
1833
|
-
}
|
|
1834
|
-
|
|
1835
|
-
// Sort by similarity descending, take top N
|
|
1836
|
-
return pairs.sort((a, b) => b.similarity - a.similarity).slice(0, maxPairs);
|
|
1837
|
-
}
|