pi-lens 3.8.29 → 3.8.30

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/CHANGELOG.md +58 -0
  2. package/clients/architect-client.ts +0 -5
  3. package/clients/ast-grep-client.ts +0 -7
  4. package/clients/biome-client.ts +0 -11
  5. package/clients/complexity-client.ts +0 -1
  6. package/clients/dispatch/dispatcher.ts +0 -1
  7. package/clients/dispatch/integration.ts +0 -6
  8. package/clients/dispatch/rules/sonar-rules.ts +21 -15
  9. package/clients/dispatch/runners/ast-grep-napi.ts +1 -1
  10. package/clients/dispatch/runners/golangci-lint.ts +2 -5
  11. package/clients/dispatch/runners/index.ts +24 -25
  12. package/clients/dispatch/runners/python-slop.ts +1 -2
  13. package/clients/dispatch/runners/similarity.ts +68 -48
  14. package/clients/dispatch/runners/tree-sitter.ts +32 -36
  15. package/clients/dispatch/runners/ts-lsp.ts +0 -49
  16. package/clients/dispatch/runners/utils/diagnostic-parsers.ts +10 -7
  17. package/clients/file-utils.ts +39 -0
  18. package/clients/go-client.ts +0 -1
  19. package/clients/installer/index.ts +16 -1
  20. package/clients/lsp/client.ts +82 -53
  21. package/clients/lsp/config.ts +0 -3
  22. package/clients/lsp/index.ts +28 -8
  23. package/clients/lsp/interactive-install.ts +1 -5
  24. package/clients/lsp/launch.ts +34 -1
  25. package/clients/lsp/server.ts +358 -148
  26. package/clients/pipeline.ts +25 -56
  27. package/clients/review-graph/builder.ts +59 -43
  28. package/clients/runner-tracker.ts +4 -2
  29. package/clients/runtime-tool-result.ts +41 -24
  30. package/clients/runtime-turn.ts +17 -5
  31. package/clients/rust-client.ts +0 -1
  32. package/clients/scan-utils.ts +0 -4
  33. package/clients/secrets-scanner.ts +1 -3
  34. package/clients/subprocess-client.ts +2 -2
  35. package/clients/test-runner-client.ts +7 -34
  36. package/clients/tool-availability.ts +0 -1
  37. package/clients/tree-sitter-cache.ts +1 -1
  38. package/clients/tree-sitter-client.ts +51 -65
  39. package/clients/type-coverage-client.ts +0 -1
  40. package/clients/type-safety-client.ts +0 -2
  41. package/commands/booboo.ts +23 -28
  42. package/index.ts +1 -21
  43. package/package.json +1 -1
  44. package/scripts/download-grammars.js +0 -1
  45. package/tools/ast-grep-replace.js +1 -1
  46. package/tools/ast-grep-replace.ts +1 -1
  47. package/tools/ast-grep-search.js +1 -1
  48. package/tools/ast-grep-search.ts +1 -1
  49. package/tools/lsp-navigation.js +4 -4
  50. package/tools/lsp-navigation.ts +4 -4
package/CHANGELOG.md CHANGED
@@ -4,6 +4,64 @@ All notable changes to pi-lens will be documented in this file.
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [3.8.30] - 2026-04-22
8
+
9
+ ### Fixed
10
+ - **lsp_navigation permanently disabled** — removed stale `lens-lsp` flag check (flag was removed in 3.8.29) that caused every `lsp_navigation` call to short-circuit with `lsp_disabled`; tool now only gates on `--no-lsp`
11
+ - **ast_grep_search / ast_grep_replace auto-install** — switched availability check from sync `isAvailable()` to async `ensureAvailable()` so the auto-installer triggers when `sg` is missing
12
+ - **@ast-grep/cli postinstall skipped** — added `@ast-grep/cli` to `NEEDS_POSTINSTALL`; without it `--ignore-scripts` left ASCII stubs in place of `sg.exe` / `ast-grep.exe` on Windows
13
+ - **Windows .exe binary lookup** — `getToolPath` now also probes the `.exe` extension on Windows, covering packages (like `@ast-grep/cli`) that place a `.exe` directly without a `.cmd` wrapper
14
+ - **jscpd broken on Node 24** — pinned `jscpd` to `3.5.10`; v4 introduced a `reprism` dependency whose `lib/languages/` directory is absent from the published package
15
+ - **TypeScript LSP using home dir as workspace root** — wrapped `TypeScriptServer` and `ESLintServer` roots with `IgnoreHomeRoot` so a `package.json` / eslint config in `~` can no longer hijacks the workspace root; fallback is the file's own directory
16
+ - **CI npm publish runs without token** — gated `publish-npm` job and dry-run step on `NPM_TOKEN` secret being set
17
+ - **Stale compiled .js triggered test failures** — rebuilt project; `secrets-scanner.js` and `project-index.js` were from before the env-var-name false-positive fix and line-number capture fix respectively
18
+ - **ast_grep_search test mock** — updated test mock from `isAvailable` to `ensureAvailable` to match the new async availability check
19
+ - **Stale LSP diagnostics in cascade** — cascade diagnostics now skip entries older than 240s, preventing false positives from earlier test injections bleeding across turns
20
+ - **Biome check on Vue/Svelte** — biome-check-json was briefly skipped on `.vue`/`.svelte` but restored after confirming Biome 2.x has native support; the 3 blocking diagnostics were real lint findings, not parse errors
21
+ - **Vue/Svelte TypeScript SDK** — extracted `findTsserverPath` helper and wired it into `VueServer` and `SvelteServer` `initializationOptions` so Vue/Svelte LSP servers find the correct `typescript.tsdk`
22
+ - **Broken npm .cmd shims on Windows** — `launch.ts` now validates npm `.cmd` shims before spawning; if the target JS file doesn't exist the shim exits with code 1 after a 500ms startup window, pre-checking avoids the delay for all LSP servers on Windows
23
+ - **Tree-sitter WASM path in hoisted installs** — `tree-sitter-client.ts` now resolves `web-tree-sitter/tree-sitter.wasm` via `createRequire` so Node walks `node_modules` ancestors correctly; fixes `ENOENT` crash in pnpm/monorepo layouts where the wasm is not nested under pi-lens's own `node_modules`
24
+ - **Grammar directory lookups in hoisted installs** — `findGrammarsDir` uses the same `createRequire` fix to anchor `web-tree-sitter/grammars` and `tree-sitter-wasms/out` paths correctly in pnpm/monorepo layouts
25
+ - **tree-sitter-gleam download 404** — removed `tree-sitter-gleam.wasm` from grammar downloads; the file was never published in `tree-sitter-wasms@0.1.13`
26
+ - **Pipeline deduplication** — `handleToolResult` now deduplicates concurrent pipeline calls for the same file; the pi framework fires `tool_result` once per hunk in an Edit array, causing duplicate pipeline runs and doubled agent output
27
+
28
+ ### Changed
29
+ - **Tuned false-positive thresholds across all runners** — reduced noise in `lens-booboo` and dispatch for all users:
30
+ - Added `FACT_SEVERITY_FILTER` (`error`/`warning` only) and `MIN_TREE_SITTER_HITS_PER_RULE = 3`
31
+ - Filtered entropy/AI-style warnings from complexity metrics
32
+ - Aligned complexity markdown headers with actual thresholds (`MI < 20`, `cognitive > 80`, `nesting > 8`)
33
+ - Raised `SEMANTIC_SIMILARITY_THRESHOLD` from `0.96` → `0.98` (aligned with dispatch similarity runner)
34
+ - Raised duplicate-string-literal `MIN_DUPLICATES` from `4` → `10`
35
+ - Unregistered `no-magic-numbers` and `high-entropy-string` fact rules globally
36
+
37
+ ### Removed
38
+ - **Dead code across 32 files** — removed 51 sites of unused imports, locals, and parameters flagged by `tsc --noUnusedLocals --noUnusedParameters`:
39
+ - `clients/architect-client.ts`, `ast-grep-client.ts`, `biome-client.ts`, `complexity-client.ts`, `go-client.ts`, `rust-client.ts`, `scan-utils.ts`, `secrets-scanner.ts`, `subprocess-client.ts`, `test-runner-client.ts`, `tool-availability.ts`, `tree-sitter-cache.ts`, `tree-sitter-client.ts`, `type-coverage-client.ts`, `type-safety-client.ts`
40
+ - `clients/dispatch/dispatcher.ts`, `runners/ast-grep-napi.ts`, `runners/golangci-lint.ts`, `runners/index.ts`, `runners/python-slop.ts`, `runners/ts-lsp.ts`, `runners/utils/diagnostic-parsers.ts`
41
+ - `clients/lsp/client.ts`, `config.ts`, `interactive-install.ts`, `launch.ts`, `server.ts`
42
+ - `clients/pipeline.ts`, `review-graph/builder.ts`, `runner-tracker.ts`
43
+ - `commands/booboo.ts`, `index.ts`
44
+
45
+ ### Tests
46
+ - **Pipeline regression tests** — `tests/clients/pipeline.test.ts` (11 tests): secrets blocking, format modification, LSP sync, dispatch blockers, autofix output, test runner skip, all-clear output
47
+ - **Autofix helper tests** — `tests/clients/autofix-helpers.test.ts` (12 tests): config detection (eslint, stylelint, sqlfluff), malformed JSON handling, file change detection after command
48
+ - **LSP lifecycle tests** — `tests/clients/lsp/lifecycle.test.ts` (4 tests): missing binary error, process spawn, immediate exit detection, process kill
49
+ - **FormatService tests** — `tests/clients/format-service.test.ts` (11 tests): disabled/skip mode, no matching formatters, successful run with change detection, formatter failure, external modification detection, singleton behavior, state clearing, file tracking
50
+ - **Dispatch integration tests** — `tests/clients/dispatch/integration.test.ts` (11 tests): `dispatchLintWithResult` empty results, result propagation, warnings-only; `shouldDispatch` for supported/unsupported; `getAvailableRunners` for supported/unsupported
51
+ - **LSP client internals tests** — `tests/clients/lsp/client-internals.test.ts` (13 tests): `handleNotifyOpen` (first open, re-open, pending opens, clear diagnostics, skip when not alive), `handleNotifyChange` (didChange when open, fallback to didOpen, clear stale diagnostics, skip when not alive), `clientWaitForDiagnostics` (immediate resolve if cached, resolve via emitter, timeout, ignore other files)
52
+ - **Runtime event flow test fix** — added missing `gatherCascadeDiagnostics` mock export to `tests/clients/runtime-event-flow.test.ts`
53
+ - **LSP launch tests** — `tests/clients/lsp/launch.test.ts` (8 new tests): `isCmdShimValid` unit tests (target exists/missing, non-npm shim, unreadable file, `.mjs` extension), early `.cmd` shim rejection without spawning, `.ps1` bypass to `.cmd` sibling, `.ps1` fallback to direct `node <js>` execution
54
+ - **Tree-sitter hoisted-install tests** — `tests/clients/tree-sitter-client-init.test.ts` (3 tests): wasm resolution via `require.resolve`, `locateFile` directory derivation, `findGrammarsDir` external package resolution
55
+
56
+ ### Refactored
57
+ - **Extract `detectFileChangedAfterCommand`** — moved from `clients/pipeline.ts` to `clients/file-utils.ts` and exported for reuse/testing; imported back into `pipeline.ts`; `tests/clients/autofix-helpers.test.ts` now imports the real function instead of reimplementing a copy
58
+ - **Export testable pipeline helpers** — exported `hasEslintConfig`, `hasStylelintConfig`, `hasSqlfluffConfig` from `clients/pipeline.ts` so config detection is testable
59
+ - **Export LSP client internals** — exported `clientWaitForDiagnostics`, `handleNotifyOpen`, `handleNotifyChange`, and `LSPClientState` from `clients/lsp/client.ts` for direct testing with mocks
60
+ - **Export `isCmdShimValid`** — exported from `clients/lsp/launch.ts` so the npm `.cmd` shim validator is unit-testable
61
+
62
+ ### CI
63
+ - **Dead-code gate** — `lint-and-typecheck` job now runs `tsc --noUnusedLocals --noUnusedParameters --noEmit` alongside `--noEmit` so dead code regressions fail CI immediately
64
+
7
65
  ## [3.8.29] - 2026-04-21
8
66
 
9
67
  ### Added
@@ -53,7 +53,6 @@ export interface FileArchitectResult {
53
53
  export class ArchitectClient {
54
54
  private config: ArchitectConfig | null = null;
55
55
  private isUserConfig: boolean = false;
56
- private configPath: string | undefined;
57
56
  private log: (msg: string) => void;
58
57
 
59
58
  constructor(verbose = false) {
@@ -78,7 +77,6 @@ export class ArchitectClient {
78
77
  try {
79
78
  const content = fs.readFileSync(configPath, "utf-8");
80
79
  this.config = this.parseYaml(content);
81
- this.configPath = configPath;
82
80
  this.isUserConfig = true;
83
81
  this.log(`Loaded user architect config from ${configPath}`);
84
82
  return true;
@@ -112,7 +110,6 @@ export class ArchitectClient {
112
110
  try {
113
111
  const content = fs.readFileSync(defaultPath, "utf-8");
114
112
  this.config = this.parseYaml(content);
115
- this.configPath = defaultPath;
116
113
  this.isUserConfig = false;
117
114
  this.log(
118
115
  "Using default architect rules (create .pi-lens/architect.yaml to customize)",
@@ -387,5 +384,3 @@ export class ArchitectClient {
387
384
  }
388
385
 
389
386
  // --- Singleton ---
390
-
391
- const _instance: ArchitectClient | null = null;
@@ -22,13 +22,6 @@ import type {
22
22
  import { resolvePackagePath } from "./package-root.js";
23
23
  import { SgRunner } from "./sg-runner.js";
24
24
 
25
- const _getExtensionDir = () => {
26
- if (typeof __dirname !== "undefined") {
27
- return __dirname;
28
- }
29
- return ".";
30
- };
31
-
32
25
  // --- Client ---
33
26
 
34
27
  export class AstGrepClient {
@@ -28,17 +28,6 @@ export interface BiomeDiagnostic {
28
28
  fixable: boolean;
29
29
  }
30
30
 
31
- interface BiomeJsonDiagnostic {
32
- message: string;
33
- severity: "error" | "warning" | "info" | "hint";
34
- category: string;
35
- span?: {
36
- start: { line: number; column: number };
37
- end: { line: number; column: number };
38
- };
39
- advice?: Array<{ message: string }>;
40
- }
41
-
42
31
  // --- Client ---
43
32
 
44
33
  export class BiomeClient {
@@ -363,7 +363,6 @@ export class ComplexityClient {
363
363
  * Calculate max parameters across all functions
364
364
  */
365
365
  private calculateMaxParams(functions: FunctionMetrics[]): number {
366
- const _maxParams = 0;
367
366
  // We stored function params in the metrics during analysis
368
367
  // For now, estimate based on function length (longer functions often have more params)
369
368
  return Math.min(
@@ -627,7 +627,6 @@ export async function dispatchForFile(
627
627
  ): Promise<DispatchResult> {
628
628
  const _overallStart = Date.now();
629
629
  const allDiagnostics: Diagnostic[] = [];
630
- const _fixed: Diagnostic[] = [];
631
630
  let stopped = false;
632
631
  const runnerLatencies: RunnerLatency[] = [];
633
632
 
@@ -81,11 +81,9 @@ import { missingErrorPropagationRule } from "./rules/missing-error-propagation.j
81
81
  import { passThroughWrappersRule } from "./rules/pass-through-wrappers.js";
82
82
  import { placeholderCommentsRule } from "./rules/placeholder-comments.js";
83
83
  import {
84
- highEntropyStringRule,
85
84
  highImportCouplingRule,
86
85
  noBooleanParamsRule,
87
86
  noComplexConditionalsRule,
88
- noMagicNumbersRule,
89
87
  } from "./rules/quality-rules.js";
90
88
  import {
91
89
  commentedCredentialsRule,
@@ -117,11 +115,9 @@ registerRule(corsWildcardRule);
117
115
  registerRule(dynamicRegexpRule);
118
116
  registerRule(maxSwitchCasesRule);
119
117
  registerRule(commentedCredentialsRule);
120
- registerRule(noMagicNumbersRule);
121
118
  registerRule(noBooleanParamsRule);
122
119
  registerRule(highImportCouplingRule);
123
120
  registerRule(noComplexConditionalsRule);
124
- registerRule(highEntropyStringRule);
125
121
 
126
122
  const sessionFacts = new FactStore();
127
123
  const sessionRunnerRegistry = new RunnerRegistry();
@@ -146,11 +142,9 @@ const FACT_RULE_IDS = new Set([
146
142
  "dynamic-regexp",
147
143
  "max-switch-cases",
148
144
  "no-commented-credentials",
149
- "no-magic-numbers",
150
145
  "no-boolean-params",
151
146
  "high-import-coupling",
152
147
  "no-complex-conditionals",
153
- "high-entropy-string",
154
148
  ]);
155
149
  const sessionSlopRuleCounts = new Map<string, number>();
156
150
  let sessionSlopDiagnosticCount = 0;
@@ -34,7 +34,7 @@ function createSourceFile(filePath: string, content: string): ts.SourceFile {
34
34
  }
35
35
 
36
36
  function makeD(
37
- id: string,
37
+ _id: string,
38
38
  rule: string,
39
39
  filePath: string,
40
40
  line: number,
@@ -95,9 +95,10 @@ export const commentedOutCodeRule: FactRule = {
95
95
  if (seen.has(r.pos)) continue;
96
96
  seen.add(r.pos);
97
97
  const text = content.slice(r.pos, r.end);
98
- const inner = r.kind === ts.SyntaxKind.MultiLineCommentTrivia
99
- ? text.slice(2, -2)
100
- : text.replace(/^\/\//gm, "");
98
+ const inner =
99
+ r.kind === ts.SyntaxKind.MultiLineCommentTrivia
100
+ ? text.slice(2, -2)
101
+ : text.replace(/^\/\//gm, "");
101
102
  if (!looksLikeCode(inner)) continue;
102
103
  const { line } = sf.getLineAndCharacterOfPosition(r.pos);
103
104
  diagnostics.push(
@@ -117,7 +118,7 @@ export const commentedOutCodeRule: FactRule = {
117
118
 
118
119
  // ---------- SN-002: duplicate string literals ----------
119
120
 
120
- const MIN_DUPLICATES = 4;
121
+ const MIN_DUPLICATES = 10;
121
122
  const MIN_STRING_LENGTH = 5;
122
123
  // Skip common non-signal strings (string-enum values, HTTP verbs, primitives, etc.)
123
124
  const SKIP_STRINGS = new Set([
@@ -280,8 +281,9 @@ export const functionInLoopRule: FactRule = {
280
281
 
281
282
  function visit(node: ts.Node) {
282
283
  if (ts.isFunctionDeclaration(node) && isInsideLoop(node)) {
283
- const { line, character } =
284
- sf.getLineAndCharacterOfPosition(node.getStart(sf));
284
+ const { line, character } = sf.getLineAndCharacterOfPosition(
285
+ node.getStart(sf),
286
+ );
285
287
  diagnostics.push(
286
288
  makeD(
287
289
  "function-in-loop",
@@ -393,8 +395,9 @@ export const dynamicRegexpRule: FactRule = {
393
395
  !ts.isStringLiteral(firstArg) &&
394
396
  !ts.isNoSubstitutionTemplateLiteral(firstArg)
395
397
  ) {
396
- const { line, character } =
397
- sf.getLineAndCharacterOfPosition(node.getStart(sf));
398
+ const { line, character } = sf.getLineAndCharacterOfPosition(
399
+ node.getStart(sf),
400
+ );
398
401
  diagnostics.push(
399
402
  makeD(
400
403
  "dynamic-regexp",
@@ -430,12 +433,11 @@ export const maxSwitchCasesRule: FactRule = {
430
433
 
431
434
  function visit(node: ts.Node) {
432
435
  if (ts.isSwitchStatement(node)) {
433
- const caseCount = node.caseBlock.clauses.filter(
434
- ts.isCaseClause,
435
- ).length;
436
+ const caseCount = node.caseBlock.clauses.filter(ts.isCaseClause).length;
436
437
  if (caseCount > MAX_SWITCH_CASES) {
437
- const { line, character } =
438
- sf.getLineAndCharacterOfPosition(node.getStart(sf));
438
+ const { line, character } = sf.getLineAndCharacterOfPosition(
439
+ node.getStart(sf),
440
+ );
439
441
  diagnostics.push(
440
442
  makeD(
441
443
  "max-switch-cases",
@@ -477,7 +479,11 @@ export const commentedCredentialsRule: FactRule = {
477
479
  const lines = content.split("\n");
478
480
  for (let i = 0; i < lines.length; i++) {
479
481
  const line = lines[i].trimStart();
480
- if (!line.startsWith("//") && !line.startsWith("#") && !line.startsWith("*"))
482
+ if (
483
+ !line.startsWith("//") &&
484
+ !line.startsWith("#") &&
485
+ !line.startsWith("*")
486
+ )
481
487
  continue;
482
488
  for (const p of CREDENTIAL_PATTERNS) {
483
489
  if (p.test(line)) {
@@ -358,7 +358,7 @@ function getCandidatesForAll(
358
358
  function executeStructuredRule(
359
359
  rootNode: any,
360
360
  condition: YamlRuleCondition,
361
- matches: unknown[] = [],
361
+ _matches: unknown[] = [],
362
362
  depth = 0,
363
363
  ): unknown[] {
364
364
  return findMatchingNodes(rootNode, condition, depth);
@@ -13,15 +13,14 @@
13
13
  import * as fs from "node:fs";
14
14
  import * as path from "node:path";
15
15
  import { safeSpawnAsync } from "../../safe-spawn.js";
16
- import { stripAnsi } from "../../sanitize.js";
17
- import { tryLazyInstall } from "./utils/lazy-installer.js";
16
+ import { PRIORITY } from "../priorities.js";
18
17
  import type {
19
18
  Diagnostic,
20
19
  DispatchContext,
21
20
  RunnerDefinition,
22
21
  RunnerResult,
23
22
  } from "../types.js";
24
- import { PRIORITY } from "../priorities.js";
23
+ import { tryLazyInstall } from "./utils/lazy-installer.js";
25
24
 
26
25
  const GOLANGCI_CONFIGS = [
27
26
  ".golangci.yml",
@@ -117,8 +116,6 @@ const golangciRunner: RunnerDefinition = {
117
116
  { timeout: 60000, cwd },
118
117
  );
119
118
 
120
- const raw = stripAnsi(result.stdout + result.stderr);
121
-
122
119
  if (result.status === 0) {
123
120
  return { status: "succeeded", diagnostics: [], semantic: "none" };
124
121
  }
@@ -7,49 +7,48 @@ import architectRunner from "./architect.js";
7
7
  import astGrepNapiRunner from "./ast-grep-napi.js";
8
8
  import biomeRunner from "./biome.js";
9
9
  import biomeCheckJsonRunner from "./biome-check.js";
10
+ import cppCheckRunner from "./cpp-check.js";
11
+ import credoRunner from "./credo.js";
12
+ import dartAnalyzeRunner from "./dart-analyze.js";
13
+ import dotnetBuildRunner from "./dotnet-build.js";
14
+ import elixirCheckRunner from "./elixir-check.js";
10
15
  import eslintRunner from "./eslint.js";
16
+ import factRulesRunner from "./fact-rules.js";
17
+ import gleamCheckRunner from "./gleam-check.js";
11
18
  import goVetRunner from "./go-vet.js";
12
19
  import golangciRunner from "./golangci-lint.js";
20
+ import hadolintRunner from "./hadolint.js";
21
+ import htmlhintRunner from "./htmlhint.js";
22
+ import javacRunner from "./javac.js";
23
+ import ktlintRunner from "./ktlint.js";
13
24
  import lspRunner from "./lsp.js";
14
- import oxlintRunner from "./oxlint.js";
25
+ import markdownlintRunner from "./markdownlint.js";
26
+ import mypyRunner from "./mypy.js";
27
+ import phpLintRunner from "./php-lint.js";
28
+ import phpstanRunner from "./phpstan.js";
29
+ import prettierCheckRunner from "./prettier-check.js";
30
+ import prismaValidateRunner from "./prisma-validate.js";
31
+ import psScriptAnalyzerRunner from "./psscriptanalyzer.js";
15
32
  import pyrightRunner from "./pyright.js";
16
33
  import pythonSlopRunner from "./python-slop.js";
17
34
  import rubocopRunner from "./rubocop.js";
18
35
  import ruffRunner from "./ruff.js";
19
36
  import rustClippyRunner from "./rust-clippy.js";
20
37
  import shellcheckRunner from "./shellcheck.js";
21
- import sqlfluffRunner from "./sqlfluff.js";
38
+ import shfmtRunner from "./shfmt.js";
22
39
  // Import similarity runner
23
40
  import similarityRunner from "./similarity.js";
24
41
  import spellcheckRunner from "./spellcheck.js";
25
- import yamllintRunner from "./yamllint.js";
42
+ import sqlfluffRunner from "./sqlfluff.js";
43
+ import stylelintRunner from "./stylelint.js";
44
+ import taploRunner from "./taplo.js";
45
+ import tflintRunner from "./tflint.js";
26
46
  // Import tree-sitter runner
27
47
  import treeSitterRunner from "./tree-sitter.js";
28
48
  import tsLspRunner from "./ts-lsp.js";
29
49
  import typeSafetyRunner from "./type-safety.js";
30
- import markdownlintRunner from "./markdownlint.js";
31
- import mypyRunner from "./mypy.js";
32
- import stylelintRunner from "./stylelint.js";
33
- import shfmtRunner from "./shfmt.js";
34
- import factRulesRunner from "./fact-rules.js";
35
- import htmlhintRunner from "./htmlhint.js";
36
- import hadolintRunner from "./hadolint.js";
37
- import phpLintRunner from "./php-lint.js";
38
- import psScriptAnalyzerRunner from "./psscriptanalyzer.js";
39
- import prismaValidateRunner from "./prisma-validate.js";
40
- import ktlintRunner from "./ktlint.js";
41
- import tflintRunner from "./tflint.js";
42
- import taploRunner from "./taplo.js";
43
- import dartAnalyzeRunner from "./dart-analyze.js";
44
- import javacRunner from "./javac.js";
45
- import dotnetBuildRunner from "./dotnet-build.js";
50
+ import yamllintRunner from "./yamllint.js";
46
51
  import zigCheckRunner from "./zig-check.js";
47
- import gleamCheckRunner from "./gleam-check.js";
48
- import credoRunner from "./credo.js";
49
- import elixirCheckRunner from "./elixir-check.js";
50
- import cppCheckRunner from "./cpp-check.js";
51
- import prettierCheckRunner from "./prettier-check.js";
52
- import phpstanRunner from "./phpstan.js";
53
52
 
54
53
  export function registerDefaultRunners(registry: RunnerRegistry): void {
55
54
  // Register all runners (ordered by priority)
@@ -10,15 +10,14 @@
10
10
  * Based on slop-code-bench: https://github.com/SprocketLab/slop-code-bench
11
11
  */
12
12
 
13
- import { spawnSync } from "node:child_process";
14
13
  import { safeSpawn } from "../../safe-spawn.js";
14
+ import { PRIORITY } from "../priorities.js";
15
15
  import type {
16
16
  Diagnostic,
17
17
  DispatchContext,
18
18
  RunnerDefinition,
19
19
  RunnerResult,
20
20
  } from "../types.js";
21
- import { PRIORITY } from "../priorities.js";
22
21
  import {
23
22
  createConfigFinder,
24
23
  getSgCommand,
@@ -9,21 +9,21 @@ import * as nodeFs from "node:fs";
9
9
  import * as fs from "node:fs/promises";
10
10
  import * as path from "node:path";
11
11
  import { NativeRustCoreClient } from "../../native-rust-client.js";
12
- import { collectSourceFiles } from "../../source-filter.js";
13
12
  import {
14
13
  buildProjectIndex,
15
14
  findSimilarFunctions,
16
15
  loadIndex,
17
16
  type ProjectIndex,
18
17
  } from "../../project-index.js";
18
+ import { collectSourceFiles } from "../../source-filter.js";
19
19
  import { buildStateMatrix, countTransitions } from "../../state-matrix.js";
20
+ import { PRIORITY } from "../priorities.js";
20
21
  import type {
21
22
  Diagnostic,
22
23
  DispatchContext,
23
24
  RunnerDefinition,
24
25
  RunnerResult,
25
26
  } from "../types.js";
26
- import { PRIORITY } from "../priorities.js";
27
27
 
28
28
  // Singleton Rust client — initialised once, reused across runner invocations.
29
29
  const rustClient = new NativeRustCoreClient();
@@ -45,7 +45,7 @@ const USE_RUST = true;
45
45
  // ============================================================================
46
46
 
47
47
  const CONFIG = {
48
- SIMILARITY_THRESHOLD: 0.96, // align with booboo: stricter to reduce boilerplate false positives
48
+ SIMILARITY_THRESHOLD: 0.98, // align with booboo: stricter to reduce boilerplate false positives
49
49
  MIN_TRANSITIONS: 40, // stronger signal floor for structural comparisons
50
50
  MIN_FUNCTION_LINES: 8, // Ignore tiny helpers/wrappers
51
51
  MIN_FILE_CHARS: 140, // Skip tiny/trivial files early
@@ -82,19 +82,24 @@ const GENERIC_NAME_TOKENS = new Set([
82
82
  export function tokenizeFunctionName(name: string): string[] {
83
83
  return name
84
84
  .replace(/([a-z0-9])([A-Z])/g, "$1 $2")
85
- .replace(/[_\-]+/g, " ")
85
+ .replace(/[_-]+/g, " ")
86
86
  .toLowerCase()
87
87
  .split(/\s+/)
88
88
  .filter((t) => t.length >= 3);
89
89
  }
90
90
 
91
- export function hasMeaningfulNameOverlap(sourceName: string, targetName: string): boolean {
91
+ export function hasMeaningfulNameOverlap(
92
+ sourceName: string,
93
+ targetName: string,
94
+ ): boolean {
92
95
  const source = new Set(tokenizeFunctionName(sourceName));
93
96
  const target = new Set(tokenizeFunctionName(targetName));
94
97
  const shared = [...source].filter((token) => target.has(token));
95
98
  if (shared.length === 0) return false;
96
99
 
97
- const specificShared = shared.filter((token) => !GENERIC_NAME_TOKENS.has(token));
100
+ const specificShared = shared.filter(
101
+ (token) => !GENERIC_NAME_TOKENS.has(token),
102
+ );
98
103
  if (specificShared.length > 0) return true;
99
104
 
100
105
  // Fallback: allow overlap if there are at least two shared generic tokens.
@@ -127,7 +132,9 @@ const similarityRunner: RunnerDefinition = {
127
132
 
128
133
  const lineCount = content.split(/\r?\n/).length;
129
134
  if (lineCount > CONFIG.MAX_FILE_LINES) {
130
- console.error(`[runner:similarity] skipped ${filePath} — file exceeds ${CONFIG.MAX_FILE_LINES} lines (${lineCount} lines)`);
135
+ console.error(
136
+ `[runner:similarity] skipped ${filePath} — file exceeds ${CONFIG.MAX_FILE_LINES} lines (${lineCount} lines)`,
137
+ );
131
138
  return { status: "skipped", diagnostics: [], semantic: "none" };
132
139
  }
133
140
  if (
@@ -212,8 +219,14 @@ const similarityRunner: RunnerDefinition = {
212
219
  continue;
213
220
  }
214
221
 
215
- const maxTransitions = Math.max(func.transitionCount, match.targetTransitionCount);
216
- const minTransitions = Math.min(func.transitionCount, match.targetTransitionCount);
222
+ const maxTransitions = Math.max(
223
+ func.transitionCount,
224
+ match.targetTransitionCount,
225
+ );
226
+ const minTransitions = Math.min(
227
+ func.transitionCount,
228
+ match.targetTransitionCount,
229
+ );
217
230
  if (minTransitions <= 0) continue;
218
231
  if (maxTransitions / minTransitions > CONFIG.MAX_TRANSITION_RATIO) {
219
232
  continue;
@@ -343,7 +356,10 @@ function extractArrowFunctions(
343
356
  }
344
357
 
345
358
  const func = decl.initializer;
346
- if (!tsModule.isArrowFunction(func) && !tsModule.isFunctionExpression(func)) {
359
+ if (
360
+ !tsModule.isArrowFunction(func) &&
361
+ !tsModule.isFunctionExpression(func)
362
+ ) {
347
363
  continue;
348
364
  }
349
365
 
@@ -470,44 +486,46 @@ async function runWithRust(
470
486
  const diagnostics: Diagnostic[] = [];
471
487
  const seenTargets = new Map<string, number>();
472
488
  for (const m of matches.slice(0, maxSuggestions)) {
473
- const similarityPct = Math.round(m.similarity * 100);
474
- // source_id / target_id format: "path/to/file.ts::funcName@line"
475
- const parseId = (id: string): { file: string; name: string; line: number } => {
476
- const m = id.match(/^(.*)::([^@]+)@(\d+)$/);
477
- if (!m) return { file: id, name: "?", line: 1 };
478
- return {
479
- file: m[1].replace(/\\/g, "/"),
480
- name: m[2],
481
- line: Number.parseInt(m[3], 10) || 1,
482
- };
489
+ const similarityPct = Math.round(m.similarity * 100);
490
+ // source_id / target_id format: "path/to/file.ts::funcName@line"
491
+ const parseId = (
492
+ id: string,
493
+ ): { file: string; name: string; line: number } => {
494
+ const m = id.match(/^(.*)::([^@]+)@(\d+)$/);
495
+ if (!m) return { file: id, name: "?", line: 1 };
496
+ return {
497
+ file: m[1].replace(/\\/g, "/"),
498
+ name: m[2],
499
+ line: Number.parseInt(m[3], 10) || 1,
483
500
  };
484
- const source = parseId(m.source_id);
485
- const target = parseId(m.target_id);
486
- if (!hasMeaningfulNameOverlap(source.name, target.name)) {
487
- continue;
488
- }
489
- const targetKey = `${target.name}@${target.file}:${target.line}`;
490
- const seenForTarget = seenTargets.get(targetKey) ?? 0;
491
- if (seenForTarget >= CONFIG.MAX_PER_TARGET_NAME) {
492
- continue;
493
- }
494
- seenTargets.set(targetKey, seenForTarget + 1);
495
- const resolvedTarget = path.isAbsolute(target.file)
496
- ? target.file
497
- : path.join(projectRoot, target.file);
498
- if (!nodeFs.existsSync(resolvedTarget)) {
499
- continue;
500
- }
501
- diagnostics.push({
502
- id: `similarity-rust-${m.source_id}-${m.target_id}`,
503
- tool: "similarity",
504
- filePath,
505
- line: source.line,
506
- column: 1,
507
- message: `Function '${source.name}' has ${similarityPct}% similarity to '${target.name}()' at ${target.file}:${target.line}. Consider reusing it if behavior is equivalent.`,
508
- severity: "warning" as const,
509
- semantic: "warning" as const,
510
- });
501
+ };
502
+ const source = parseId(m.source_id);
503
+ const target = parseId(m.target_id);
504
+ if (!hasMeaningfulNameOverlap(source.name, target.name)) {
505
+ continue;
506
+ }
507
+ const targetKey = `${target.name}@${target.file}:${target.line}`;
508
+ const seenForTarget = seenTargets.get(targetKey) ?? 0;
509
+ if (seenForTarget >= CONFIG.MAX_PER_TARGET_NAME) {
510
+ continue;
511
+ }
512
+ seenTargets.set(targetKey, seenForTarget + 1);
513
+ const resolvedTarget = path.isAbsolute(target.file)
514
+ ? target.file
515
+ : path.join(projectRoot, target.file);
516
+ if (!nodeFs.existsSync(resolvedTarget)) {
517
+ continue;
518
+ }
519
+ diagnostics.push({
520
+ id: `similarity-rust-${m.source_id}-${m.target_id}`,
521
+ tool: "similarity",
522
+ filePath,
523
+ line: source.line,
524
+ column: 1,
525
+ message: `Function '${source.name}' has ${similarityPct}% similarity to '${target.name}()' at ${target.file}:${target.line}. Consider reusing it if behavior is equivalent.`,
526
+ severity: "warning" as const,
527
+ semantic: "warning" as const,
528
+ });
511
529
  }
512
530
 
513
531
  return {
@@ -574,7 +592,9 @@ async function loadOrBuildIndex(
574
592
  return index;
575
593
  }
576
594
 
577
- async function loadCachedIndex(projectRoot: string): Promise<ProjectIndex | null> {
595
+ async function loadCachedIndex(
596
+ projectRoot: string,
597
+ ): Promise<ProjectIndex | null> {
578
598
  const cached = indexCache.get(projectRoot);
579
599
  if (cached) {
580
600
  return cached;