pi-lens 3.8.21 → 3.8.23
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 +28 -0
- package/README.md +2 -0
- package/clients/dispatch/dispatcher.ts +75 -91
- package/clients/dispatch/fact-provider-types.ts +22 -0
- package/clients/dispatch/fact-rule-runner.ts +22 -0
- package/clients/dispatch/fact-runner.ts +28 -0
- package/clients/dispatch/fact-scheduler.ts +78 -0
- package/clients/dispatch/fact-store.ts +67 -0
- package/clients/dispatch/facts/comment-facts.ts +59 -0
- package/clients/dispatch/facts/file-content.ts +20 -0
- package/clients/dispatch/facts/function-facts.ts +177 -0
- package/clients/dispatch/facts/try-catch-facts.ts +80 -0
- package/clients/dispatch/integration.ts +130 -24
- package/clients/dispatch/priorities.ts +22 -0
- package/clients/dispatch/rules/async-noise.ts +43 -0
- package/clients/dispatch/rules/error-obscuring.ts +40 -0
- package/clients/dispatch/rules/error-swallowing.ts +35 -0
- package/clients/dispatch/rules/pass-through-wrappers.ts +52 -0
- package/clients/dispatch/rules/placeholder-comments.ts +47 -0
- package/clients/dispatch/runners/architect.ts +2 -1
- package/clients/dispatch/runners/ast-grep-napi.ts +2 -1
- package/clients/dispatch/runners/biome-check.ts +40 -8
- package/clients/dispatch/runners/biome.ts +2 -1
- package/clients/dispatch/runners/eslint.ts +34 -6
- package/clients/dispatch/runners/go-vet.ts +2 -1
- package/clients/dispatch/runners/golangci-lint.ts +2 -1
- package/clients/dispatch/runners/index.ts +29 -27
- package/clients/dispatch/runners/lsp.ts +60 -4
- package/clients/dispatch/runners/oxlint.ts +2 -1
- package/clients/dispatch/runners/pyright.ts +2 -1
- package/clients/dispatch/runners/python-slop.ts +2 -1
- package/clients/dispatch/runners/rubocop.ts +2 -1
- package/clients/dispatch/runners/ruff.ts +2 -1
- package/clients/dispatch/runners/rust-clippy.ts +2 -1
- package/clients/dispatch/runners/shellcheck.ts +2 -1
- package/clients/dispatch/runners/similarity.ts +2 -1
- package/clients/dispatch/runners/spellcheck.ts +2 -1
- package/clients/dispatch/runners/sqlfluff.ts +2 -1
- package/clients/dispatch/runners/tree-sitter.ts +469 -1
- package/clients/dispatch/runners/ts-lsp.ts +2 -1
- package/clients/dispatch/runners/type-safety.ts +2 -1
- package/clients/dispatch/runners/yamllint.ts +2 -1
- package/clients/dispatch/tool-profile.ts +40 -0
- package/clients/dispatch/types.ts +3 -13
- package/clients/lsp/client.ts +366 -12
- package/clients/lsp/index.ts +374 -76
- package/clients/lsp/launch.ts +42 -2
- package/clients/lsp/server.ts +186 -12
- package/clients/pipeline.ts +2 -2
- package/clients/runtime-context.ts +2 -2
- package/clients/runtime-session.ts +43 -5
- package/clients/session-summary.ts +21 -0
- package/clients/tree-sitter-client.ts +162 -0
- package/clients/tree-sitter-logger.ts +47 -0
- package/clients/tree-sitter-query-loader.ts +13 -2
- package/index.ts +67 -17
- package/package.json +3 -1
- package/rules/rule-catalog.json +64 -0
- package/rules/tree-sitter-queries/go/go-bare-error.yml +19 -7
- package/rules/tree-sitter-queries/go/go-command-injection.yml +55 -0
- package/rules/tree-sitter-queries/go/go-direct-panic.yml +45 -0
- package/rules/tree-sitter-queries/go/go-empty-if-err.yml +47 -0
- package/rules/tree-sitter-queries/go/go-goroutine-loop-capture.yml +49 -0
- package/rules/tree-sitter-queries/go/go-ignored-call-result.yml +51 -0
- package/rules/tree-sitter-queries/go/go-insecure-random.yml +51 -0
- package/rules/tree-sitter-queries/go/go-log-fatal.yml +49 -0
- package/rules/tree-sitter-queries/go/go-path-traversal.yml +51 -0
- package/rules/tree-sitter-queries/go/go-shared-map-write-goroutine.yml +54 -0
- package/rules/tree-sitter-queries/go/go-sql-injection.yml +55 -0
- package/rules/tree-sitter-queries/go/go-weak-hash.yml +51 -0
- package/rules/tree-sitter-queries/python/python-command-injection.yml +63 -0
- package/rules/tree-sitter-queries/python/python-insecure-deserialization.yml +48 -0
- package/rules/tree-sitter-queries/python/python-insecure-random.yml +51 -0
- package/rules/tree-sitter-queries/python/python-path-traversal.yml +55 -0
- package/rules/tree-sitter-queries/python/python-sql-injection.yml +47 -0
- package/rules/tree-sitter-queries/python/python-ssrf.yml +50 -0
- package/rules/tree-sitter-queries/python/python-thread-global-write.yml +58 -0
- package/rules/tree-sitter-queries/python/python-weak-hash.yml +51 -0
- package/rules/tree-sitter-queries/ruby/ruby-command-injection.yml +56 -0
- package/rules/tree-sitter-queries/ruby/ruby-insecure-deserialization.yml +47 -0
- package/rules/tree-sitter-queries/ruby/ruby-insecure-random.yml +54 -0
- package/rules/tree-sitter-queries/ruby/ruby-weak-hash.yml +50 -0
- package/rules/tree-sitter-queries/rust/rust-lock-held-across-await.yml +59 -0
- package/rules/tree-sitter-queries/typescript/ts-command-injection.yml +60 -0
- package/rules/tree-sitter-queries/typescript/ts-detached-async-call.yml +56 -0
- package/rules/tree-sitter-queries/typescript/ts-insecure-random.yml +54 -0
- package/rules/tree-sitter-queries/typescript/ts-ssrf.yml +53 -0
- package/rules/tree-sitter-queries/typescript/ts-weak-hash.yml +54 -0
- package/scripts/validate-rule-catalog.mjs +227 -0
- package/skills/lsp-navigation/SKILL.md +15 -3
- package/tools/lsp-navigation.js +466 -79
- package/tools/lsp-navigation.ts +587 -85
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import * as ts from "typescript";
|
|
2
|
+
import type { FactProvider } from "../fact-provider-types.js";
|
|
3
|
+
|
|
4
|
+
const BOUNDARY_PREFIXES = [
|
|
5
|
+
"fetch",
|
|
6
|
+
"fs.",
|
|
7
|
+
"db.",
|
|
8
|
+
"http",
|
|
9
|
+
"axios",
|
|
10
|
+
"got",
|
|
11
|
+
"req.",
|
|
12
|
+
"res.",
|
|
13
|
+
];
|
|
14
|
+
|
|
15
|
+
export interface FunctionSummary {
|
|
16
|
+
name: string;
|
|
17
|
+
line: number;
|
|
18
|
+
column: number;
|
|
19
|
+
isAsync: boolean;
|
|
20
|
+
hasAwait: boolean;
|
|
21
|
+
hasReturnAwaitCall: boolean;
|
|
22
|
+
statementCount: number;
|
|
23
|
+
parameterCount: number;
|
|
24
|
+
isPassThroughWrapper: boolean;
|
|
25
|
+
passThroughTarget?: string;
|
|
26
|
+
isBoundaryWrapper: boolean;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function getFunctionName(node: ts.Node): string {
|
|
30
|
+
if (ts.isFunctionDeclaration(node)) {
|
|
31
|
+
return node.name?.text ?? "<anonymous>";
|
|
32
|
+
}
|
|
33
|
+
if (ts.isMethodDeclaration(node)) {
|
|
34
|
+
if (ts.isIdentifier(node.name)) return node.name.text;
|
|
35
|
+
return node.name.getText();
|
|
36
|
+
}
|
|
37
|
+
if (ts.isArrowFunction(node) || ts.isFunctionExpression(node)) {
|
|
38
|
+
const parent = node.parent;
|
|
39
|
+
if (ts.isVariableDeclaration(parent) && ts.isIdentifier(parent.name)) {
|
|
40
|
+
return parent.name.text;
|
|
41
|
+
}
|
|
42
|
+
if (ts.isPropertyAssignment(parent)) {
|
|
43
|
+
return parent.name.getText();
|
|
44
|
+
}
|
|
45
|
+
return "<anonymous>";
|
|
46
|
+
}
|
|
47
|
+
return "<unknown>";
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function isCallPassThrough(
|
|
51
|
+
stmt: ts.Statement,
|
|
52
|
+
paramNames: string[],
|
|
53
|
+
): { pass: boolean; target?: string } {
|
|
54
|
+
if (!ts.isReturnStatement(stmt) || !stmt.expression) return { pass: false };
|
|
55
|
+
const expr = stmt.expression;
|
|
56
|
+
if (!ts.isCallExpression(expr)) return { pass: false };
|
|
57
|
+
|
|
58
|
+
const args = expr.arguments.map((a) => a.getText());
|
|
59
|
+
if (args.length !== paramNames.length) return { pass: false };
|
|
60
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
61
|
+
if (args[i] !== paramNames[i]) return { pass: false };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return { pass: true, target: expr.expression.getText() };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function hasAwaitInNode(node: ts.Node): boolean {
|
|
68
|
+
let found = false;
|
|
69
|
+
const walk = (n: ts.Node): void => {
|
|
70
|
+
if (found) return;
|
|
71
|
+
if (ts.isAwaitExpression(n)) {
|
|
72
|
+
found = true;
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
ts.forEachChild(n, walk);
|
|
76
|
+
};
|
|
77
|
+
walk(node);
|
|
78
|
+
return found;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function hasReturnAwaitCall(node: ts.Node): boolean {
|
|
82
|
+
let found = false;
|
|
83
|
+
const walk = (n: ts.Node): void => {
|
|
84
|
+
if (found) return;
|
|
85
|
+
if (
|
|
86
|
+
ts.isReturnStatement(n) &&
|
|
87
|
+
n.expression &&
|
|
88
|
+
ts.isAwaitExpression(n.expression) &&
|
|
89
|
+
ts.isCallExpression(n.expression.expression)
|
|
90
|
+
) {
|
|
91
|
+
found = true;
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
ts.forEachChild(n, walk);
|
|
95
|
+
};
|
|
96
|
+
walk(node);
|
|
97
|
+
return found;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export const functionFactProvider: FactProvider = {
|
|
101
|
+
id: "fact.file.functions",
|
|
102
|
+
provides: ["file.functionSummaries"],
|
|
103
|
+
requires: ["file.content"],
|
|
104
|
+
appliesTo(ctx) {
|
|
105
|
+
return /\.tsx?$/.test(ctx.filePath);
|
|
106
|
+
},
|
|
107
|
+
run(ctx, store) {
|
|
108
|
+
const content = store.getFileFact<string>(ctx.filePath, "file.content");
|
|
109
|
+
if (!content) {
|
|
110
|
+
store.setFileFact(ctx.filePath, "file.functionSummaries", []);
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const sourceFile = ts.createSourceFile(
|
|
115
|
+
ctx.filePath,
|
|
116
|
+
content,
|
|
117
|
+
ts.ScriptTarget.Latest,
|
|
118
|
+
true,
|
|
119
|
+
ts.ScriptKind.TSX,
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
const summaries: FunctionSummary[] = [];
|
|
123
|
+
|
|
124
|
+
const addSummary = (
|
|
125
|
+
node:
|
|
126
|
+
| ts.FunctionDeclaration
|
|
127
|
+
| ts.MethodDeclaration
|
|
128
|
+
| ts.FunctionExpression
|
|
129
|
+
| ts.ArrowFunction,
|
|
130
|
+
): void => {
|
|
131
|
+
const body = node.body;
|
|
132
|
+
if (!body || !ts.isBlock(body)) return;
|
|
133
|
+
|
|
134
|
+
const lc = sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile));
|
|
135
|
+
const paramNames = node.parameters.map((p) => p.name.getText(sourceFile));
|
|
136
|
+
const statementCount = body.statements.length;
|
|
137
|
+
const passThrough =
|
|
138
|
+
statementCount === 1
|
|
139
|
+
? isCallPassThrough(body.statements[0], paramNames)
|
|
140
|
+
: { pass: false as const };
|
|
141
|
+
const target = passThrough.target ?? "";
|
|
142
|
+
const lowerTarget = target.toLowerCase();
|
|
143
|
+
const isBoundaryWrapper = BOUNDARY_PREFIXES.some((prefix) =>
|
|
144
|
+
lowerTarget.startsWith(prefix),
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
summaries.push({
|
|
148
|
+
name: getFunctionName(node),
|
|
149
|
+
line: lc.line + 1,
|
|
150
|
+
column: lc.character + 1,
|
|
151
|
+
isAsync: !!node.modifiers?.some((m) => m.kind === ts.SyntaxKind.AsyncKeyword),
|
|
152
|
+
hasAwait: hasAwaitInNode(body),
|
|
153
|
+
hasReturnAwaitCall: hasReturnAwaitCall(body),
|
|
154
|
+
statementCount,
|
|
155
|
+
parameterCount: node.parameters.length,
|
|
156
|
+
isPassThroughWrapper: passThrough.pass,
|
|
157
|
+
passThroughTarget: passThrough.target,
|
|
158
|
+
isBoundaryWrapper,
|
|
159
|
+
});
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
const visit = (node: ts.Node): void => {
|
|
163
|
+
if (
|
|
164
|
+
ts.isFunctionDeclaration(node) ||
|
|
165
|
+
ts.isMethodDeclaration(node) ||
|
|
166
|
+
ts.isFunctionExpression(node) ||
|
|
167
|
+
ts.isArrowFunction(node)
|
|
168
|
+
) {
|
|
169
|
+
addSummary(node);
|
|
170
|
+
}
|
|
171
|
+
ts.forEachChild(node, visit);
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
visit(sourceFile);
|
|
175
|
+
store.setFileFact(ctx.filePath, "file.functionSummaries", summaries);
|
|
176
|
+
},
|
|
177
|
+
};
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import * as ts from "typescript";
|
|
2
|
+
import type { FactProvider } from "../fact-provider-types.js";
|
|
3
|
+
|
|
4
|
+
export interface TryCatchSummary {
|
|
5
|
+
line: number;
|
|
6
|
+
column: number;
|
|
7
|
+
catchParam: string | null;
|
|
8
|
+
bodyText: string;
|
|
9
|
+
isEmpty: boolean;
|
|
10
|
+
hasRethrow: boolean;
|
|
11
|
+
hasLogging: boolean;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function isOnlyWhitespaceOrComments(text: string): boolean {
|
|
15
|
+
// Remove block comments
|
|
16
|
+
let stripped = text.replace(/\/\*[\s\S]*?\*\//g, "");
|
|
17
|
+
// Remove line comments
|
|
18
|
+
stripped = stripped.replace(/\/\/[^\n]*/g, "");
|
|
19
|
+
return stripped.trim().length === 0;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export const tryCatchFactProvider: FactProvider = {
|
|
23
|
+
id: "tryCatchFacts",
|
|
24
|
+
provides: ["file.tryCatchSummaries"],
|
|
25
|
+
requires: ["file.content"],
|
|
26
|
+
appliesTo(ctx) {
|
|
27
|
+
return /\.tsx?$/.test(ctx.filePath);
|
|
28
|
+
},
|
|
29
|
+
run(ctx, store) {
|
|
30
|
+
const content = store.getFileFact<string>(ctx.filePath, "file.content");
|
|
31
|
+
if (!content) {
|
|
32
|
+
store.setFileFact(ctx.filePath, "file.tryCatchSummaries", []);
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const sourceFile = ts.createSourceFile(
|
|
37
|
+
ctx.filePath,
|
|
38
|
+
content,
|
|
39
|
+
ts.ScriptTarget.Latest,
|
|
40
|
+
true,
|
|
41
|
+
ts.ScriptKind.TSX,
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
const summaries: TryCatchSummary[] = [];
|
|
45
|
+
|
|
46
|
+
function visit(node: ts.Node): void {
|
|
47
|
+
if (ts.isCatchClause(node)) {
|
|
48
|
+
const pos = sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile));
|
|
49
|
+
const line = pos.line + 1;
|
|
50
|
+
const column = pos.character + 1;
|
|
51
|
+
|
|
52
|
+
let catchParam: string | null = null;
|
|
53
|
+
if (node.variableDeclaration) {
|
|
54
|
+
const name = node.variableDeclaration.name;
|
|
55
|
+
if (ts.isIdentifier(name)) {
|
|
56
|
+
catchParam = name.text;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const bodyText = node.block.getText(sourceFile)
|
|
61
|
+
.replace(/^\{/, "")
|
|
62
|
+
.replace(/\}$/, "")
|
|
63
|
+
.trim();
|
|
64
|
+
|
|
65
|
+
const isEmpty = isOnlyWhitespaceOrComments(bodyText);
|
|
66
|
+
const hasRethrow = /\bthrow\b/.test(bodyText);
|
|
67
|
+
const hasLogging =
|
|
68
|
+
/\bconsole\.(log|warn|error)\b/.test(bodyText) ||
|
|
69
|
+
/\blogger\./.test(bodyText);
|
|
70
|
+
|
|
71
|
+
summaries.push({ line, column, catchParam, bodyText, isEmpty, hasRethrow, hasLogging });
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
ts.forEachChild(node, visit);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
visit(sourceFile);
|
|
78
|
+
store.setFileFact(ctx.filePath, "file.tryCatchSummaries", summaries);
|
|
79
|
+
},
|
|
80
|
+
};
|
|
@@ -11,21 +11,24 @@ import {
|
|
|
11
11
|
getLspCapableKinds,
|
|
12
12
|
getPrimaryDispatchGroup,
|
|
13
13
|
} from "../language-policy.js";
|
|
14
|
+
import {
|
|
15
|
+
formatSlopScoreSummary,
|
|
16
|
+
type SlopScoreSummary,
|
|
17
|
+
} from "../session-summary.js";
|
|
18
|
+
import { FactStore } from "./fact-store.js";
|
|
14
19
|
import {
|
|
15
20
|
clearLatencyReports,
|
|
16
21
|
clearCoverageNoticeState,
|
|
17
|
-
createBaselineStore,
|
|
18
22
|
createDispatchContext,
|
|
23
|
+
RunnerRegistry,
|
|
19
24
|
type DispatchLatencyReport,
|
|
20
25
|
dispatchForFile,
|
|
21
26
|
formatLatencyReport,
|
|
22
27
|
getLatencyReports,
|
|
23
|
-
getRunnersForKind,
|
|
24
28
|
type RunnerLatency,
|
|
25
29
|
} from "./dispatcher.js";
|
|
26
30
|
import { TOOL_PLANS } from "./plan.js";
|
|
27
31
|
import type {
|
|
28
|
-
BaselineStore,
|
|
29
32
|
DispatchResult,
|
|
30
33
|
ModifiedRange,
|
|
31
34
|
PiAgentAPI,
|
|
@@ -36,16 +39,114 @@ export type { DispatchLatencyReport, RunnerLatency };
|
|
|
36
39
|
// Re-export latency tracking types and functions
|
|
37
40
|
export { clearLatencyReports, formatLatencyReport, getLatencyReports };
|
|
38
41
|
|
|
39
|
-
|
|
40
|
-
|
|
42
|
+
import { registerDefaultRunners } from "./runners/index.js";
|
|
43
|
+
|
|
44
|
+
// Register fact providers
|
|
45
|
+
import { registerProvider } from "./fact-runner.js";
|
|
46
|
+
import { runProviders } from "./fact-runner.js";
|
|
47
|
+
import { fileContentProvider } from "./facts/file-content.js";
|
|
48
|
+
registerProvider(fileContentProvider);
|
|
49
|
+
import { tryCatchFactProvider } from "./facts/try-catch-facts.js";
|
|
50
|
+
registerProvider(tryCatchFactProvider);
|
|
51
|
+
import { functionFactProvider } from "./facts/function-facts.js";
|
|
52
|
+
registerProvider(functionFactProvider);
|
|
53
|
+
import { commentFactProvider } from "./facts/comment-facts.js";
|
|
54
|
+
registerProvider(commentFactProvider);
|
|
41
55
|
|
|
42
|
-
//
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
56
|
+
// Register fact rules
|
|
57
|
+
import { registerRule } from "./fact-rule-runner.js";
|
|
58
|
+
import { errorObscuringRule } from "./rules/error-obscuring.js";
|
|
59
|
+
import { errorSwallowingRule } from "./rules/error-swallowing.js";
|
|
60
|
+
import { asyncNoiseRule } from "./rules/async-noise.js";
|
|
61
|
+
import { passThroughWrappersRule } from "./rules/pass-through-wrappers.js";
|
|
62
|
+
import { placeholderCommentsRule } from "./rules/placeholder-comments.js";
|
|
63
|
+
registerRule(errorObscuringRule);
|
|
64
|
+
registerRule(errorSwallowingRule);
|
|
65
|
+
registerRule(asyncNoiseRule);
|
|
66
|
+
registerRule(passThroughWrappersRule);
|
|
67
|
+
registerRule(placeholderCommentsRule);
|
|
68
|
+
|
|
69
|
+
const sessionFacts = new FactStore();
|
|
70
|
+
const sessionRunnerRegistry = new RunnerRegistry();
|
|
71
|
+
registerDefaultRunners(sessionRunnerRegistry);
|
|
48
72
|
const LSP_CAPABLE_KINDS = new Set<FileKind>(getLspCapableKinds());
|
|
73
|
+
const FACT_RULE_IDS = new Set([
|
|
74
|
+
"error-obscuring",
|
|
75
|
+
"error-swallowing",
|
|
76
|
+
"async-noise",
|
|
77
|
+
"pass-through-wrappers",
|
|
78
|
+
"placeholder-comments",
|
|
79
|
+
]);
|
|
80
|
+
const sessionSlopRuleCounts = new Map<string, number>();
|
|
81
|
+
let sessionSlopDiagnosticCount = 0;
|
|
82
|
+
let sessionWrittenLineCount = 0;
|
|
83
|
+
|
|
84
|
+
function resetSessionSlopScore(): void {
|
|
85
|
+
sessionSlopRuleCounts.clear();
|
|
86
|
+
sessionSlopDiagnosticCount = 0;
|
|
87
|
+
sessionWrittenLineCount = 0;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function detectFactRuleId(diagnostic: {
|
|
91
|
+
id?: string;
|
|
92
|
+
rule?: string;
|
|
93
|
+
tool?: string;
|
|
94
|
+
}): string | undefined {
|
|
95
|
+
if (diagnostic.rule && FACT_RULE_IDS.has(diagnostic.rule)) {
|
|
96
|
+
return diagnostic.rule;
|
|
97
|
+
}
|
|
98
|
+
if (diagnostic.tool && FACT_RULE_IDS.has(diagnostic.tool)) {
|
|
99
|
+
return diagnostic.tool;
|
|
100
|
+
}
|
|
101
|
+
if (diagnostic.id) {
|
|
102
|
+
const prefix = diagnostic.id.split(":", 1)[0];
|
|
103
|
+
if (FACT_RULE_IDS.has(prefix)) {
|
|
104
|
+
return prefix;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return undefined;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function trackSessionSlopStats(
|
|
111
|
+
ctx: ReturnType<typeof createDispatchContext>,
|
|
112
|
+
diagnostics: DispatchResult["diagnostics"],
|
|
113
|
+
): void {
|
|
114
|
+
const lineCount = ctx.facts.getFileFact<number>(ctx.filePath, "file.lineCount");
|
|
115
|
+
if (typeof lineCount === "number" && Number.isFinite(lineCount) && lineCount > 0) {
|
|
116
|
+
sessionWrittenLineCount += lineCount;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
for (const diagnostic of diagnostics) {
|
|
120
|
+
const ruleId = detectFactRuleId(diagnostic);
|
|
121
|
+
if (!ruleId) continue;
|
|
122
|
+
sessionSlopDiagnosticCount += 1;
|
|
123
|
+
sessionSlopRuleCounts.set(ruleId, (sessionSlopRuleCounts.get(ruleId) ?? 0) + 1);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function getDispatchSlopScoreSummary(): SlopScoreSummary | undefined {
|
|
128
|
+
if (sessionSlopDiagnosticCount === 0 || sessionWrittenLineCount <= 0) {
|
|
129
|
+
return undefined;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const totalKlocWritten = sessionWrittenLineCount / 1000;
|
|
133
|
+
const ruleCounts = [...sessionSlopRuleCounts.entries()]
|
|
134
|
+
.map(([ruleId, count]) => ({ ruleId, count }))
|
|
135
|
+
.sort((a, b) => b.count - a.count || a.ruleId.localeCompare(b.ruleId));
|
|
136
|
+
|
|
137
|
+
return {
|
|
138
|
+
totalRuleDiagnostics: sessionSlopDiagnosticCount,
|
|
139
|
+
totalKlocWritten,
|
|
140
|
+
scorePerKloc: sessionSlopDiagnosticCount / totalKlocWritten,
|
|
141
|
+
ruleCounts,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export function getDispatchSlopScoreLine(): string {
|
|
146
|
+
const summary = getDispatchSlopScoreSummary();
|
|
147
|
+
if (!summary) return "";
|
|
148
|
+
return formatSlopScoreSummary(summary);
|
|
149
|
+
}
|
|
49
150
|
|
|
50
151
|
function withPrimaryPolicyGroup(
|
|
51
152
|
kind: keyof typeof TOOL_PLANS,
|
|
@@ -104,7 +205,8 @@ export function getDispatchGroupsForKind(
|
|
|
104
205
|
* starts with a clean slate.
|
|
105
206
|
*/
|
|
106
207
|
export function resetDispatchBaselines(): void {
|
|
107
|
-
|
|
208
|
+
sessionFacts.clearAll();
|
|
209
|
+
resetSessionSlopScore();
|
|
108
210
|
clearCoverageNoticeState();
|
|
109
211
|
}
|
|
110
212
|
|
|
@@ -129,10 +231,11 @@ export async function dispatchLint(
|
|
|
129
231
|
filePath,
|
|
130
232
|
cwd,
|
|
131
233
|
pi,
|
|
132
|
-
|
|
234
|
+
sessionFacts,
|
|
133
235
|
true,
|
|
134
236
|
modifiedRanges,
|
|
135
237
|
);
|
|
238
|
+
sessionFacts.clearFileFactsFor(ctx.filePath);
|
|
136
239
|
|
|
137
240
|
const kind = ctx.kind;
|
|
138
241
|
if (!kind) return "";
|
|
@@ -140,7 +243,9 @@ export async function dispatchLint(
|
|
|
140
243
|
const groups = getDispatchGroupsForKind(kind, pi);
|
|
141
244
|
if (groups.length === 0) return "";
|
|
142
245
|
|
|
143
|
-
|
|
246
|
+
await runProviders(ctx);
|
|
247
|
+
const result = await dispatchForFile(ctx, groups, sessionRunnerRegistry);
|
|
248
|
+
trackSessionSlopStats(ctx, result.diagnostics);
|
|
144
249
|
return result.output;
|
|
145
250
|
}
|
|
146
251
|
|
|
@@ -157,10 +262,11 @@ export async function dispatchLintWithResult(
|
|
|
157
262
|
filePath,
|
|
158
263
|
cwd,
|
|
159
264
|
pi,
|
|
160
|
-
|
|
265
|
+
sessionFacts,
|
|
161
266
|
true,
|
|
162
267
|
modifiedRanges,
|
|
163
268
|
);
|
|
269
|
+
sessionFacts.clearFileFactsFor(ctx.filePath);
|
|
164
270
|
|
|
165
271
|
const kind = ctx.kind;
|
|
166
272
|
if (!kind) {
|
|
@@ -190,14 +296,10 @@ export async function dispatchLintWithResult(
|
|
|
190
296
|
};
|
|
191
297
|
}
|
|
192
298
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
* Create a baseline store for delta mode tracking
|
|
198
|
-
*/
|
|
199
|
-
export function createLintBaselines() {
|
|
200
|
-
return createBaselineStore();
|
|
299
|
+
await runProviders(ctx);
|
|
300
|
+
const result = await dispatchForFile(ctx, groups, sessionRunnerRegistry);
|
|
301
|
+
trackSessionSlopStats(ctx, result.diagnostics);
|
|
302
|
+
return result;
|
|
201
303
|
}
|
|
202
304
|
|
|
203
305
|
/**
|
|
@@ -216,6 +318,10 @@ export async function getAvailableRunners(filePath: string): Promise<string[]> {
|
|
|
216
318
|
const kind = detectFileKind(filePath);
|
|
217
319
|
if (!kind) return [];
|
|
218
320
|
|
|
219
|
-
const
|
|
321
|
+
const normalizedPath = filePath.replace(/\\/g, "/");
|
|
322
|
+
const pathForFilter = normalizedPath.startsWith("/")
|
|
323
|
+
? normalizedPath
|
|
324
|
+
: `/${normalizedPath}`;
|
|
325
|
+
const runners = sessionRunnerRegistry.getForKind(kind, pathForFilter);
|
|
220
326
|
return runners.map((r) => r.id);
|
|
221
327
|
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dispatch runner priority tiers.
|
|
3
|
+
*
|
|
4
|
+
* NOTE: These priorities only govern ordering within dispatch runner execution.
|
|
5
|
+
* They do not represent full write/edit pipeline order.
|
|
6
|
+
*/
|
|
7
|
+
export const PRIORITY = {
|
|
8
|
+
LSP_PRIMARY: 4,
|
|
9
|
+
LSP_FALLBACK: 5,
|
|
10
|
+
FORMAT_AND_LINT_PRIMARY: 10,
|
|
11
|
+
LINT_SECONDARY: 12,
|
|
12
|
+
STRUCTURAL_ANALYSIS: 14,
|
|
13
|
+
SPECIALIZED_ANALYSIS: 15,
|
|
14
|
+
GENERAL_ANALYSIS: 20,
|
|
15
|
+
YAML_LINT: 22,
|
|
16
|
+
SQL_LINT: 24,
|
|
17
|
+
PYTHON_SLOP: 25,
|
|
18
|
+
DOC_QUALITY: 30,
|
|
19
|
+
SIMILARITY: 35,
|
|
20
|
+
ARCHITECTURE: 40,
|
|
21
|
+
DEEP_LANGUAGE_ANALYSIS: 50,
|
|
22
|
+
} as const;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { isTestFile } from "../../file-utils.js";
|
|
2
|
+
import type { FactRule } from "../fact-provider-types.js";
|
|
3
|
+
import type { FunctionSummary } from "../facts/function-facts.js";
|
|
4
|
+
import type { Diagnostic } from "../types.js";
|
|
5
|
+
|
|
6
|
+
export const asyncNoiseRule: FactRule = {
|
|
7
|
+
id: "async-noise",
|
|
8
|
+
requires: ["file.functionSummaries"],
|
|
9
|
+
appliesTo(ctx) {
|
|
10
|
+
return /\.tsx?$/.test(ctx.filePath) && !isTestFile(ctx.filePath);
|
|
11
|
+
},
|
|
12
|
+
evaluate(ctx, store) {
|
|
13
|
+
const summaries = store.getFileFact<FunctionSummary[]>(
|
|
14
|
+
ctx.filePath,
|
|
15
|
+
"file.functionSummaries",
|
|
16
|
+
);
|
|
17
|
+
if (!summaries) return [];
|
|
18
|
+
|
|
19
|
+
const diagnostics: Diagnostic[] = [];
|
|
20
|
+
for (const fn of summaries) {
|
|
21
|
+
if (
|
|
22
|
+
fn.isAsync &&
|
|
23
|
+
!fn.hasAwait &&
|
|
24
|
+
!fn.hasReturnAwaitCall &&
|
|
25
|
+
!fn.isPassThroughWrapper
|
|
26
|
+
) {
|
|
27
|
+
diagnostics.push({
|
|
28
|
+
id: `async-noise:${ctx.filePath}:${fn.line}:${fn.column}`,
|
|
29
|
+
tool: "async-noise",
|
|
30
|
+
filePath: ctx.filePath,
|
|
31
|
+
line: fn.line,
|
|
32
|
+
column: fn.column,
|
|
33
|
+
severity: "warning",
|
|
34
|
+
semantic: "warning",
|
|
35
|
+
message: `Async function '${fn.name}' has no await and appears to add async noise`,
|
|
36
|
+
rule: "async-noise",
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return diagnostics;
|
|
42
|
+
},
|
|
43
|
+
};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { FactRule } from "../fact-provider-types.js";
|
|
2
|
+
import type { Diagnostic } from "../types.js";
|
|
3
|
+
import type { TryCatchSummary } from "../facts/try-catch-facts.js";
|
|
4
|
+
|
|
5
|
+
export const errorObscuringRule: FactRule = {
|
|
6
|
+
id: "error-obscuring",
|
|
7
|
+
requires: ["file.tryCatchSummaries"],
|
|
8
|
+
appliesTo(ctx) {
|
|
9
|
+
return /\.tsx?$/.test(ctx.filePath);
|
|
10
|
+
},
|
|
11
|
+
evaluate(ctx, store) {
|
|
12
|
+
const summaries = store.getFileFact<TryCatchSummary[]>(
|
|
13
|
+
ctx.filePath,
|
|
14
|
+
"file.tryCatchSummaries",
|
|
15
|
+
);
|
|
16
|
+
if (!summaries) return [];
|
|
17
|
+
|
|
18
|
+
const diagnostics: Diagnostic[] = [];
|
|
19
|
+
for (const s of summaries) {
|
|
20
|
+
if (
|
|
21
|
+
!s.isEmpty &&
|
|
22
|
+
!s.hasRethrow &&
|
|
23
|
+
s.catchParam !== null &&
|
|
24
|
+
!s.bodyText.includes(s.catchParam)
|
|
25
|
+
) {
|
|
26
|
+
diagnostics.push({
|
|
27
|
+
id: `error-obscuring:${ctx.filePath}:${s.line}:${s.column}`,
|
|
28
|
+
tool: "error-obscuring",
|
|
29
|
+
filePath: ctx.filePath,
|
|
30
|
+
line: s.line,
|
|
31
|
+
column: s.column,
|
|
32
|
+
severity: "warning",
|
|
33
|
+
semantic: "warning",
|
|
34
|
+
message: `Catch block catches '${s.catchParam}' but never references it — the error is obscured`,
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return diagnostics;
|
|
39
|
+
},
|
|
40
|
+
};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { FactRule } from "../fact-provider-types.js";
|
|
2
|
+
import type { Diagnostic } from "../types.js";
|
|
3
|
+
import type { TryCatchSummary } from "../facts/try-catch-facts.js";
|
|
4
|
+
|
|
5
|
+
export const errorSwallowingRule: FactRule = {
|
|
6
|
+
id: "error-swallowing",
|
|
7
|
+
requires: ["file.tryCatchSummaries"],
|
|
8
|
+
appliesTo(ctx) {
|
|
9
|
+
return /\.tsx?$/.test(ctx.filePath);
|
|
10
|
+
},
|
|
11
|
+
evaluate(ctx, store) {
|
|
12
|
+
const summaries = store.getFileFact<TryCatchSummary[]>(
|
|
13
|
+
ctx.filePath,
|
|
14
|
+
"file.tryCatchSummaries",
|
|
15
|
+
);
|
|
16
|
+
if (!summaries) return [];
|
|
17
|
+
|
|
18
|
+
const diagnostics: Diagnostic[] = [];
|
|
19
|
+
for (const s of summaries) {
|
|
20
|
+
if (s.isEmpty) {
|
|
21
|
+
diagnostics.push({
|
|
22
|
+
id: `error-swallowing:${ctx.filePath}:${s.line}:${s.column}`,
|
|
23
|
+
tool: "error-swallowing",
|
|
24
|
+
filePath: ctx.filePath,
|
|
25
|
+
line: s.line,
|
|
26
|
+
column: s.column,
|
|
27
|
+
severity: "warning",
|
|
28
|
+
semantic: "warning",
|
|
29
|
+
message: `Empty catch block silently swallows errors`,
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return diagnostics;
|
|
34
|
+
},
|
|
35
|
+
};
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { isTestFile } from "../../file-utils.js";
|
|
2
|
+
import type { FactRule } from "../fact-provider-types.js";
|
|
3
|
+
import type { CommentSummary } from "../facts/comment-facts.js";
|
|
4
|
+
import type { FunctionSummary } from "../facts/function-facts.js";
|
|
5
|
+
import type { Diagnostic } from "../types.js";
|
|
6
|
+
|
|
7
|
+
const ALIAS_COMMENT_RE =
|
|
8
|
+
/\b(alias|backward\s*compat|backwards\s*compat|compatibility|shim|adapter)\b/i;
|
|
9
|
+
|
|
10
|
+
function hasAliasCommentNear(line: number, comments: CommentSummary[]): boolean {
|
|
11
|
+
return comments.some(
|
|
12
|
+
(comment) => comment.line >= line - 2 && comment.line <= line && ALIAS_COMMENT_RE.test(comment.text),
|
|
13
|
+
);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const passThroughWrappersRule: FactRule = {
|
|
17
|
+
id: "pass-through-wrappers",
|
|
18
|
+
requires: ["file.functionSummaries", "file.comments"],
|
|
19
|
+
appliesTo(ctx) {
|
|
20
|
+
return /\.tsx?$/.test(ctx.filePath) && !isTestFile(ctx.filePath);
|
|
21
|
+
},
|
|
22
|
+
evaluate(ctx, store) {
|
|
23
|
+
const summaries = store.getFileFact<FunctionSummary[]>(
|
|
24
|
+
ctx.filePath,
|
|
25
|
+
"file.functionSummaries",
|
|
26
|
+
);
|
|
27
|
+
const comments = store.getFileFact<CommentSummary[]>(ctx.filePath, "file.comments");
|
|
28
|
+
if (!summaries || !comments) return [];
|
|
29
|
+
|
|
30
|
+
const diagnostics: Diagnostic[] = [];
|
|
31
|
+
for (const fn of summaries) {
|
|
32
|
+
if (!fn.isPassThroughWrapper || fn.statementCount !== 1 || fn.isBoundaryWrapper) {
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
if (hasAliasCommentNear(fn.line, comments)) continue;
|
|
36
|
+
|
|
37
|
+
diagnostics.push({
|
|
38
|
+
id: `pass-through-wrapper:${ctx.filePath}:${fn.line}:${fn.column}`,
|
|
39
|
+
tool: "pass-through-wrappers",
|
|
40
|
+
filePath: ctx.filePath,
|
|
41
|
+
line: fn.line,
|
|
42
|
+
column: fn.column,
|
|
43
|
+
severity: "warning",
|
|
44
|
+
semantic: "warning",
|
|
45
|
+
rule: "pass-through-wrappers",
|
|
46
|
+
message: `Function '${fn.name}' is a trivial pass-through wrapper`,
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return diagnostics;
|
|
51
|
+
},
|
|
52
|
+
};
|