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.
Files changed (92) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/README.md +2 -0
  3. package/clients/dispatch/dispatcher.ts +75 -91
  4. package/clients/dispatch/fact-provider-types.ts +22 -0
  5. package/clients/dispatch/fact-rule-runner.ts +22 -0
  6. package/clients/dispatch/fact-runner.ts +28 -0
  7. package/clients/dispatch/fact-scheduler.ts +78 -0
  8. package/clients/dispatch/fact-store.ts +67 -0
  9. package/clients/dispatch/facts/comment-facts.ts +59 -0
  10. package/clients/dispatch/facts/file-content.ts +20 -0
  11. package/clients/dispatch/facts/function-facts.ts +177 -0
  12. package/clients/dispatch/facts/try-catch-facts.ts +80 -0
  13. package/clients/dispatch/integration.ts +130 -24
  14. package/clients/dispatch/priorities.ts +22 -0
  15. package/clients/dispatch/rules/async-noise.ts +43 -0
  16. package/clients/dispatch/rules/error-obscuring.ts +40 -0
  17. package/clients/dispatch/rules/error-swallowing.ts +35 -0
  18. package/clients/dispatch/rules/pass-through-wrappers.ts +52 -0
  19. package/clients/dispatch/rules/placeholder-comments.ts +47 -0
  20. package/clients/dispatch/runners/architect.ts +2 -1
  21. package/clients/dispatch/runners/ast-grep-napi.ts +2 -1
  22. package/clients/dispatch/runners/biome-check.ts +40 -8
  23. package/clients/dispatch/runners/biome.ts +2 -1
  24. package/clients/dispatch/runners/eslint.ts +34 -6
  25. package/clients/dispatch/runners/go-vet.ts +2 -1
  26. package/clients/dispatch/runners/golangci-lint.ts +2 -1
  27. package/clients/dispatch/runners/index.ts +29 -27
  28. package/clients/dispatch/runners/lsp.ts +60 -4
  29. package/clients/dispatch/runners/oxlint.ts +2 -1
  30. package/clients/dispatch/runners/pyright.ts +2 -1
  31. package/clients/dispatch/runners/python-slop.ts +2 -1
  32. package/clients/dispatch/runners/rubocop.ts +2 -1
  33. package/clients/dispatch/runners/ruff.ts +2 -1
  34. package/clients/dispatch/runners/rust-clippy.ts +2 -1
  35. package/clients/dispatch/runners/shellcheck.ts +2 -1
  36. package/clients/dispatch/runners/similarity.ts +2 -1
  37. package/clients/dispatch/runners/spellcheck.ts +2 -1
  38. package/clients/dispatch/runners/sqlfluff.ts +2 -1
  39. package/clients/dispatch/runners/tree-sitter.ts +469 -1
  40. package/clients/dispatch/runners/ts-lsp.ts +2 -1
  41. package/clients/dispatch/runners/type-safety.ts +2 -1
  42. package/clients/dispatch/runners/yamllint.ts +2 -1
  43. package/clients/dispatch/tool-profile.ts +40 -0
  44. package/clients/dispatch/types.ts +3 -13
  45. package/clients/lsp/client.ts +366 -12
  46. package/clients/lsp/index.ts +374 -76
  47. package/clients/lsp/launch.ts +42 -2
  48. package/clients/lsp/server.ts +186 -12
  49. package/clients/pipeline.ts +2 -2
  50. package/clients/runtime-context.ts +2 -2
  51. package/clients/runtime-session.ts +43 -5
  52. package/clients/session-summary.ts +21 -0
  53. package/clients/tree-sitter-client.ts +162 -0
  54. package/clients/tree-sitter-logger.ts +47 -0
  55. package/clients/tree-sitter-query-loader.ts +13 -2
  56. package/index.ts +67 -17
  57. package/package.json +3 -1
  58. package/rules/rule-catalog.json +64 -0
  59. package/rules/tree-sitter-queries/go/go-bare-error.yml +19 -7
  60. package/rules/tree-sitter-queries/go/go-command-injection.yml +55 -0
  61. package/rules/tree-sitter-queries/go/go-direct-panic.yml +45 -0
  62. package/rules/tree-sitter-queries/go/go-empty-if-err.yml +47 -0
  63. package/rules/tree-sitter-queries/go/go-goroutine-loop-capture.yml +49 -0
  64. package/rules/tree-sitter-queries/go/go-ignored-call-result.yml +51 -0
  65. package/rules/tree-sitter-queries/go/go-insecure-random.yml +51 -0
  66. package/rules/tree-sitter-queries/go/go-log-fatal.yml +49 -0
  67. package/rules/tree-sitter-queries/go/go-path-traversal.yml +51 -0
  68. package/rules/tree-sitter-queries/go/go-shared-map-write-goroutine.yml +54 -0
  69. package/rules/tree-sitter-queries/go/go-sql-injection.yml +55 -0
  70. package/rules/tree-sitter-queries/go/go-weak-hash.yml +51 -0
  71. package/rules/tree-sitter-queries/python/python-command-injection.yml +63 -0
  72. package/rules/tree-sitter-queries/python/python-insecure-deserialization.yml +48 -0
  73. package/rules/tree-sitter-queries/python/python-insecure-random.yml +51 -0
  74. package/rules/tree-sitter-queries/python/python-path-traversal.yml +55 -0
  75. package/rules/tree-sitter-queries/python/python-sql-injection.yml +47 -0
  76. package/rules/tree-sitter-queries/python/python-ssrf.yml +50 -0
  77. package/rules/tree-sitter-queries/python/python-thread-global-write.yml +58 -0
  78. package/rules/tree-sitter-queries/python/python-weak-hash.yml +51 -0
  79. package/rules/tree-sitter-queries/ruby/ruby-command-injection.yml +56 -0
  80. package/rules/tree-sitter-queries/ruby/ruby-insecure-deserialization.yml +47 -0
  81. package/rules/tree-sitter-queries/ruby/ruby-insecure-random.yml +54 -0
  82. package/rules/tree-sitter-queries/ruby/ruby-weak-hash.yml +50 -0
  83. package/rules/tree-sitter-queries/rust/rust-lock-held-across-await.yml +59 -0
  84. package/rules/tree-sitter-queries/typescript/ts-command-injection.yml +60 -0
  85. package/rules/tree-sitter-queries/typescript/ts-detached-async-call.yml +56 -0
  86. package/rules/tree-sitter-queries/typescript/ts-insecure-random.yml +54 -0
  87. package/rules/tree-sitter-queries/typescript/ts-ssrf.yml +53 -0
  88. package/rules/tree-sitter-queries/typescript/ts-weak-hash.yml +54 -0
  89. package/scripts/validate-rule-catalog.mjs +227 -0
  90. package/skills/lsp-navigation/SKILL.md +15 -3
  91. package/tools/lsp-navigation.js +466 -79
  92. 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
- // Import runners to register them
40
- import "./runners/index.js";
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
- // --- Persistent Baseline Store ---
43
- // Survives across dispatchLint calls within a session.
44
- // Without this, delta mode is a no-op: every call creates a fresh empty
45
- // store, so baselines.get() always returns undefined and every issue
46
- // looks "new" every time.
47
- const sessionBaselines: BaselineStore = createBaselineStore();
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
- sessionBaselines.clear();
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
- sessionBaselines,
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
- const result = await dispatchForFile(ctx, groups);
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
- sessionBaselines,
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
- return dispatchForFile(ctx, groups);
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 runners = getRunnersForKind(kind);
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
+ };