pi-lens 3.8.29 → 3.8.31
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 +69 -0
- package/clients/architect-client.ts +0 -5
- package/clients/ast-grep-client.ts +0 -7
- package/clients/biome-client.ts +0 -11
- package/clients/complexity-client.ts +0 -1
- package/clients/dispatch/dispatcher.ts +0 -1
- package/clients/dispatch/integration.ts +0 -6
- package/clients/dispatch/rules/sonar-rules.ts +21 -15
- package/clients/dispatch/runners/ast-grep-napi.ts +1 -1
- package/clients/dispatch/runners/golangci-lint.ts +2 -5
- package/clients/dispatch/runners/index.ts +24 -25
- package/clients/dispatch/runners/python-slop.ts +1 -2
- package/clients/dispatch/runners/similarity.ts +68 -48
- package/clients/dispatch/runners/tree-sitter.ts +32 -36
- package/clients/dispatch/runners/ts-lsp.ts +0 -49
- package/clients/dispatch/runners/utils/diagnostic-parsers.ts +10 -7
- package/clients/file-utils.ts +39 -0
- package/clients/go-client.ts +0 -1
- package/clients/installer/index.ts +16 -1
- package/clients/lsp/client.ts +82 -53
- package/clients/lsp/config.ts +0 -3
- package/clients/lsp/index.ts +28 -8
- package/clients/lsp/interactive-install.ts +1 -5
- package/clients/lsp/launch.ts +34 -1
- package/clients/lsp/server.ts +358 -148
- package/clients/pipeline.ts +25 -63
- package/clients/review-graph/builder.ts +59 -43
- package/clients/runner-tracker.ts +4 -2
- package/clients/runtime-coordinator.ts +7 -0
- package/clients/runtime-tool-result.ts +52 -25
- package/clients/runtime-turn.ts +17 -5
- package/clients/rust-client.ts +0 -1
- package/clients/scan-utils.ts +0 -4
- package/clients/secrets-scanner.ts +1 -3
- package/clients/subprocess-client.ts +2 -2
- package/clients/test-runner-client.ts +7 -34
- package/clients/tool-availability.ts +0 -1
- package/clients/tree-sitter-cache.ts +1 -1
- package/clients/tree-sitter-client.ts +51 -67
- package/clients/type-coverage-client.ts +0 -1
- package/clients/type-safety-client.ts +0 -2
- package/commands/booboo.ts +23 -28
- package/index.ts +1 -21
- package/package.json +2 -2
- package/scripts/download-grammars.js +0 -1
- package/tools/ast-grep-replace.js +2 -2
- package/tools/ast-grep-replace.ts +2 -2
- package/tools/ast-grep-search.js +2 -2
- package/tools/ast-grep-search.ts +2 -2
- package/tools/lsp-navigation.js +5 -5
- package/tools/lsp-navigation.ts +5 -5
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,75 @@ All notable changes to pi-lens will be documented in this file.
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [3.8.31] - 2026-04-23
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
- **Duplicate inline feedback on edit arrays** — `tool_result` calls for the same file are now deduplicated within a turn using a `reportedThisTurn` set on `RuntimeCoordinator`, cleared on each `turn_start`; previously pi's sequential per-hunk `tool_result` firing caused the pipeline to re-run and feedback to repeat N times per edit array
|
|
11
|
+
- **Double latency logging on pipeline completion** — removed redundant `logLatency` call in `pipeline.ts`; `runtime-tool-result.ts` already logs the outer `tool_result completed` with full duration including format, autofix, and cascade phases
|
|
12
|
+
- **Modified range tracking broken for 3-digit+ line numbers** — `parseDiffRanges` regex changed from `\s+` to `\s*` to handle unpadded line numbers; the diff format right-pads to the file's max digit width so e.g. line 613 in a <1000-line file has no leading space and was silently dropped
|
|
13
|
+
- **Stale gleam grammar entries** — removed dead `LANGUAGE_TO_GRAMMAR` and `getExtensionsForLanguage` entries for gleam; `tree-sitter-gleam.wasm` was never published in `tree-sitter-wasms@0.1.13`
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
- **TypeBox 0.34.x → 1.x migration** — updated `package.json` dependency from `@sinclair/typebox` to `typebox ^1.0.0` and updated imports in `tools/lsp-navigation.ts`, `tools/ast-grep-search.ts`, and `tools/ast-grep-replace.ts` to match pi-mono 0.69.0
|
|
17
|
+
|
|
18
|
+
## [3.8.30] - 2026-04-22
|
|
19
|
+
|
|
20
|
+
### Fixed
|
|
21
|
+
- **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`
|
|
22
|
+
- **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
|
|
23
|
+
- **@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
|
|
24
|
+
- **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
|
|
25
|
+
- **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
|
|
26
|
+
- **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
|
|
27
|
+
- **CI npm publish runs without token** — gated `publish-npm` job and dry-run step on `NPM_TOKEN` secret being set
|
|
28
|
+
- **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
|
|
29
|
+
- **ast_grep_search test mock** — updated test mock from `isAvailable` to `ensureAvailable` to match the new async availability check
|
|
30
|
+
- **Stale LSP diagnostics in cascade** — cascade diagnostics now skip entries older than 240s, preventing false positives from earlier test injections bleeding across turns
|
|
31
|
+
- **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
|
|
32
|
+
- **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`
|
|
33
|
+
- **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
|
|
34
|
+
- **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`
|
|
35
|
+
- **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
|
|
36
|
+
- **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`
|
|
37
|
+
- **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
|
|
38
|
+
|
|
39
|
+
### Changed
|
|
40
|
+
- **Tuned false-positive thresholds across all runners** — reduced noise in `lens-booboo` and dispatch for all users:
|
|
41
|
+
- Added `FACT_SEVERITY_FILTER` (`error`/`warning` only) and `MIN_TREE_SITTER_HITS_PER_RULE = 3`
|
|
42
|
+
- Filtered entropy/AI-style warnings from complexity metrics
|
|
43
|
+
- Aligned complexity markdown headers with actual thresholds (`MI < 20`, `cognitive > 80`, `nesting > 8`)
|
|
44
|
+
- Raised `SEMANTIC_SIMILARITY_THRESHOLD` from `0.96` → `0.98` (aligned with dispatch similarity runner)
|
|
45
|
+
- Raised duplicate-string-literal `MIN_DUPLICATES` from `4` → `10`
|
|
46
|
+
- Unregistered `no-magic-numbers` and `high-entropy-string` fact rules globally
|
|
47
|
+
|
|
48
|
+
### Removed
|
|
49
|
+
- **Dead code across 32 files** — removed 51 sites of unused imports, locals, and parameters flagged by `tsc --noUnusedLocals --noUnusedParameters`:
|
|
50
|
+
- `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`
|
|
51
|
+
- `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`
|
|
52
|
+
- `clients/lsp/client.ts`, `config.ts`, `interactive-install.ts`, `launch.ts`, `server.ts`
|
|
53
|
+
- `clients/pipeline.ts`, `review-graph/builder.ts`, `runner-tracker.ts`
|
|
54
|
+
- `commands/booboo.ts`, `index.ts`
|
|
55
|
+
|
|
56
|
+
### Tests
|
|
57
|
+
- **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
|
|
58
|
+
- **Autofix helper tests** — `tests/clients/autofix-helpers.test.ts` (12 tests): config detection (eslint, stylelint, sqlfluff), malformed JSON handling, file change detection after command
|
|
59
|
+
- **LSP lifecycle tests** — `tests/clients/lsp/lifecycle.test.ts` (4 tests): missing binary error, process spawn, immediate exit detection, process kill
|
|
60
|
+
- **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
|
|
61
|
+
- **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
|
|
62
|
+
- **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)
|
|
63
|
+
- **Runtime event flow test fix** — added missing `gatherCascadeDiagnostics` mock export to `tests/clients/runtime-event-flow.test.ts`
|
|
64
|
+
- **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
|
|
65
|
+
- **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
|
|
66
|
+
|
|
67
|
+
### Refactored
|
|
68
|
+
- **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
|
|
69
|
+
- **Export testable pipeline helpers** — exported `hasEslintConfig`, `hasStylelintConfig`, `hasSqlfluffConfig` from `clients/pipeline.ts` so config detection is testable
|
|
70
|
+
- **Export LSP client internals** — exported `clientWaitForDiagnostics`, `handleNotifyOpen`, `handleNotifyChange`, and `LSPClientState` from `clients/lsp/client.ts` for direct testing with mocks
|
|
71
|
+
- **Export `isCmdShimValid`** — exported from `clients/lsp/launch.ts` so the npm `.cmd` shim validator is unit-testable
|
|
72
|
+
|
|
73
|
+
### CI
|
|
74
|
+
- **Dead-code gate** — `lint-and-typecheck` job now runs `tsc --noUnusedLocals --noUnusedParameters --noEmit` alongside `--noEmit` so dead code regressions fail CI immediately
|
|
75
|
+
|
|
7
76
|
## [3.8.29] - 2026-04-21
|
|
8
77
|
|
|
9
78
|
### 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 {
|
package/clients/biome-client.ts
CHANGED
|
@@ -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
|
-
|
|
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 =
|
|
99
|
-
|
|
100
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
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 {
|
|
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 {
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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(/[_
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
216
|
-
|
|
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 (
|
|
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
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
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
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
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(
|
|
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;
|