pi-lens 3.8.21 → 3.8.22
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 +8 -0
- package/README.md +2 -0
- package/clients/dispatch/runners/lsp.ts +58 -3
- package/clients/dispatch/runners/tree-sitter.ts +467 -0
- package/clients/lsp/client.ts +229 -3
- package/clients/lsp/index.ts +111 -1
- package/clients/pipeline.ts +2 -2
- package/clients/runtime-session.ts +43 -5
- 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/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 +259 -28
- package/tools/lsp-navigation.ts +294 -29
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to pi-lens will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## [3.8.22] - 2026-04-09
|
|
6
|
+
|
|
7
|
+
### Changed
|
|
8
|
+
- **Quick startup path for one-shot print sessions** — `--print`/`-p` now auto-selects quick startup mode to skip heavy bootstrap work and reduce startup latency. Added `PI_LENS_STARTUP_MODE=full|minimal|quick` override for explicit control.
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
- **Cascade diagnostics formatting clarity** — turn-end cascade entries now render source location as `line <n>, col <m> code=<id>:` so diagnostic codes (for example `TS2322`) are no longer formatted in a way that can be mistaken for file line numbers.
|
|
12
|
+
|
|
5
13
|
## [3.8.21] - 2026-04-08
|
|
6
14
|
|
|
7
15
|
### Changed
|
package/README.md
CHANGED
|
@@ -28,6 +28,8 @@ At `session_start`, pi-lens:
|
|
|
28
28
|
- emits missing-tool install hints for detected languages when relevant
|
|
29
29
|
- injects session guidance through internal context (non-user channel) to reduce acknowledgement-only first responses
|
|
30
30
|
|
|
31
|
+
For one-shot print sessions (for example `pi --print ...`), pi-lens auto-uses a quick startup path that skips heavy bootstrap work to reduce startup latency. You can override startup behavior with `PI_LENS_STARTUP_MODE=full|minimal|quick`.
|
|
32
|
+
|
|
31
33
|
### Turn End
|
|
32
34
|
|
|
33
35
|
At `turn_end`, pi-lens:
|
|
@@ -25,6 +25,33 @@ import { readFileContent } from "./utils.js";
|
|
|
25
25
|
|
|
26
26
|
const LSP_MAX_FILE_BYTES = RUNTIME_CONFIG.pipeline.lspMaxFileBytes;
|
|
27
27
|
const LSP_MAX_FILE_LINES = RUNTIME_CONFIG.pipeline.lspMaxFileLines;
|
|
28
|
+
const MAX_CODE_ACTION_LOOKUPS = 6;
|
|
29
|
+
const MAX_CODE_ACTION_TITLES = 3;
|
|
30
|
+
|
|
31
|
+
function normalizeActionTitle(title: string): string {
|
|
32
|
+
return title.replace(/\s+/g, " ").trim();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function buildCodeActionSuggestion(
|
|
36
|
+
actions: import("../../lsp/client.js").LSPCodeAction[],
|
|
37
|
+
): string | undefined {
|
|
38
|
+
if (!actions.length) return undefined;
|
|
39
|
+
const quickFixes = actions.filter((action) =>
|
|
40
|
+
action.kind?.startsWith("quickfix"),
|
|
41
|
+
);
|
|
42
|
+
if (!quickFixes.length) return undefined;
|
|
43
|
+
|
|
44
|
+
const titles = Array.from(
|
|
45
|
+
new Set(
|
|
46
|
+
quickFixes
|
|
47
|
+
.map((action) => normalizeActionTitle(action.title))
|
|
48
|
+
.filter((title) => title.length > 0),
|
|
49
|
+
),
|
|
50
|
+
).slice(0, MAX_CODE_ACTION_TITLES);
|
|
51
|
+
|
|
52
|
+
if (!titles.length) return undefined;
|
|
53
|
+
return `LSP quick fixes: ${titles.join("; ")}`;
|
|
54
|
+
}
|
|
28
55
|
|
|
29
56
|
const lspRunner: RunnerDefinition = {
|
|
30
57
|
id: "lsp",
|
|
@@ -125,9 +152,35 @@ const lspRunner: RunnerDefinition = {
|
|
|
125
152
|
|
|
126
153
|
// Convert LSP diagnostics to our format
|
|
127
154
|
// Defensive: filter out malformed diagnostics that may lack range
|
|
128
|
-
const
|
|
129
|
-
|
|
130
|
-
|
|
155
|
+
const validLspDiags = lspDiags.filter((d) => d.range?.start?.line !== undefined);
|
|
156
|
+
const fixSuggestionByIndex = new Map<number, string>();
|
|
157
|
+
|
|
158
|
+
const blockingDiagIndexes = validLspDiags
|
|
159
|
+
.map((d, idx) => ({ d, idx }))
|
|
160
|
+
.filter(({ d }) => d.severity === 1)
|
|
161
|
+
.slice(0, MAX_CODE_ACTION_LOOKUPS);
|
|
162
|
+
|
|
163
|
+
for (const { d, idx } of blockingDiagIndexes) {
|
|
164
|
+
try {
|
|
165
|
+
const start = d.range.start;
|
|
166
|
+
const end = d.range.end ?? d.range.start;
|
|
167
|
+
const actions = await lspService.codeAction(
|
|
168
|
+
ctx.filePath,
|
|
169
|
+
start.line,
|
|
170
|
+
start.character,
|
|
171
|
+
end.line,
|
|
172
|
+
end.character,
|
|
173
|
+
);
|
|
174
|
+
const suggestion = buildCodeActionSuggestion(actions);
|
|
175
|
+
if (suggestion) {
|
|
176
|
+
fixSuggestionByIndex.set(idx, suggestion);
|
|
177
|
+
}
|
|
178
|
+
} catch {
|
|
179
|
+
// Best-effort enrichment only; base diagnostics remain authoritative.
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const diagnostics: Diagnostic[] = validLspDiags.map((d, idx) => ({
|
|
131
184
|
id: `lsp:${d.code ?? "unknown"}:${d.range.start.line}`,
|
|
132
185
|
message: d.message,
|
|
133
186
|
filePath: diagnosticPath,
|
|
@@ -143,6 +196,8 @@ const lspRunner: RunnerDefinition = {
|
|
|
143
196
|
: "none",
|
|
144
197
|
tool: "lsp",
|
|
145
198
|
code: String(d.code ?? ""),
|
|
199
|
+
fixable: fixSuggestionByIndex.has(idx),
|
|
200
|
+
fixSuggestion: fixSuggestionByIndex.get(idx),
|
|
146
201
|
}));
|
|
147
202
|
|
|
148
203
|
const hasErrors = diagnostics.some((d) => d.semantic === "blocking");
|
|
@@ -8,7 +8,9 @@
|
|
|
8
8
|
import * as fs from "node:fs";
|
|
9
9
|
import * as path from "node:path";
|
|
10
10
|
import { RuleCache } from "../../cache/rule-cache.js";
|
|
11
|
+
import { getSourceFiles } from "../../scan-utils.js";
|
|
11
12
|
import { TreeSitterClient } from "../../tree-sitter-client.js";
|
|
13
|
+
import { logTreeSitter } from "../../tree-sitter-logger.js";
|
|
12
14
|
import { classifyDefect } from "../diagnostic-taxonomy.js";
|
|
13
15
|
import {
|
|
14
16
|
queryLoader,
|
|
@@ -25,6 +27,294 @@ import type {
|
|
|
25
27
|
// Creating a new TreeSitterClient() on every write resets TRANSFER_BUFFER (a module-level
|
|
26
28
|
// WASM pointer) — concurrent writes race on _ts_init() and corrupt shared WASM state → crash.
|
|
27
29
|
let _sharedClient: TreeSitterClient | null = null;
|
|
30
|
+
const entitySnapshotByFile = new Map<string, Map<string, string>>();
|
|
31
|
+
const blastFileCache = new Map<string, { expiresAt: number; files: string[] }>();
|
|
32
|
+
const blastCooldownByFile = new Map<string, number>();
|
|
33
|
+
const BLAST_CACHE_TTL_MS = 30_000;
|
|
34
|
+
const MAX_BLAST_FILES = 300;
|
|
35
|
+
const MAX_BLAST_ENTITIES = 8;
|
|
36
|
+
const BLAST_MAX_FILE_BYTES = 128 * 1024;
|
|
37
|
+
const BLAST_MAX_TOTAL_BYTES = 2 * 1024 * 1024;
|
|
38
|
+
const BLAST_MAX_ELAPSED_MS = 120;
|
|
39
|
+
const BLAST_COOLDOWN_MS = 5_000;
|
|
40
|
+
|
|
41
|
+
interface EntityQueryDef {
|
|
42
|
+
id: string;
|
|
43
|
+
kind: string;
|
|
44
|
+
query: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const ENTITY_QUERIES: Partial<Record<string, EntityQueryDef[]>> = {
|
|
48
|
+
typescript: [
|
|
49
|
+
{
|
|
50
|
+
id: "entity-ts-function",
|
|
51
|
+
kind: "function",
|
|
52
|
+
query: "(function_declaration name: (identifier) @NAME)",
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
id: "entity-ts-class",
|
|
56
|
+
kind: "class",
|
|
57
|
+
query: "(class_declaration name: (type_identifier) @NAME)",
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
id: "entity-ts-method",
|
|
61
|
+
kind: "method",
|
|
62
|
+
query: "(method_definition name: (property_identifier) @NAME)",
|
|
63
|
+
},
|
|
64
|
+
],
|
|
65
|
+
javascript: [
|
|
66
|
+
{
|
|
67
|
+
id: "entity-js-function",
|
|
68
|
+
kind: "function",
|
|
69
|
+
query: "(function_declaration name: (identifier) @NAME)",
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
id: "entity-js-class",
|
|
73
|
+
kind: "class",
|
|
74
|
+
query: "(class_declaration name: (identifier) @NAME)",
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
id: "entity-js-method",
|
|
78
|
+
kind: "method",
|
|
79
|
+
query: "(method_definition name: (property_identifier) @NAME)",
|
|
80
|
+
},
|
|
81
|
+
],
|
|
82
|
+
python: [
|
|
83
|
+
{
|
|
84
|
+
id: "entity-py-function",
|
|
85
|
+
kind: "function",
|
|
86
|
+
query: "(function_definition name: (identifier) @NAME)",
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
id: "entity-py-class",
|
|
90
|
+
kind: "class",
|
|
91
|
+
query: "(class_definition name: (identifier) @NAME)",
|
|
92
|
+
},
|
|
93
|
+
],
|
|
94
|
+
go: [
|
|
95
|
+
{
|
|
96
|
+
id: "entity-go-function",
|
|
97
|
+
kind: "function",
|
|
98
|
+
query: "(function_declaration name: (identifier) @NAME)",
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
id: "entity-go-method",
|
|
102
|
+
kind: "method",
|
|
103
|
+
query: "(method_declaration name: (field_identifier) @NAME)",
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
id: "entity-go-type",
|
|
107
|
+
kind: "type",
|
|
108
|
+
query: "(type_spec name: (type_identifier) @NAME)",
|
|
109
|
+
},
|
|
110
|
+
],
|
|
111
|
+
rust: [
|
|
112
|
+
{
|
|
113
|
+
id: "entity-rs-function",
|
|
114
|
+
kind: "function",
|
|
115
|
+
query: "(function_item name: (identifier) @NAME)",
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
id: "entity-rs-struct",
|
|
119
|
+
kind: "struct",
|
|
120
|
+
query: "(struct_item name: (type_identifier) @NAME)",
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
id: "entity-rs-enum",
|
|
124
|
+
kind: "enum",
|
|
125
|
+
query: "(enum_item name: (type_identifier) @NAME)",
|
|
126
|
+
},
|
|
127
|
+
],
|
|
128
|
+
ruby: [
|
|
129
|
+
{
|
|
130
|
+
id: "entity-rb-method",
|
|
131
|
+
kind: "method",
|
|
132
|
+
query: "(method name: (identifier) @NAME)",
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
id: "entity-rb-class",
|
|
136
|
+
kind: "class",
|
|
137
|
+
query: "(class name: (constant) @NAME)",
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
id: "entity-rb-module",
|
|
141
|
+
kind: "module",
|
|
142
|
+
query: "(module name: (constant) @NAME)",
|
|
143
|
+
},
|
|
144
|
+
],
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
function escapeRegex(name: string): string {
|
|
148
|
+
return name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
async function extractEntitySnapshot(
|
|
152
|
+
client: TreeSitterClient,
|
|
153
|
+
filePath: string,
|
|
154
|
+
languageId: string,
|
|
155
|
+
): Promise<Map<string, string>> {
|
|
156
|
+
const defs = ENTITY_QUERIES[languageId] ?? [];
|
|
157
|
+
const snapshot = new Map<string, string>();
|
|
158
|
+
|
|
159
|
+
for (const def of defs) {
|
|
160
|
+
const matches = await client.runQueryOnFile(
|
|
161
|
+
{
|
|
162
|
+
id: def.id,
|
|
163
|
+
name: def.id,
|
|
164
|
+
severity: "info",
|
|
165
|
+
category: "entity",
|
|
166
|
+
language: languageId,
|
|
167
|
+
message: "",
|
|
168
|
+
query: def.query,
|
|
169
|
+
metavars: ["NAME"],
|
|
170
|
+
has_fix: false,
|
|
171
|
+
filePath: "",
|
|
172
|
+
},
|
|
173
|
+
filePath,
|
|
174
|
+
languageId,
|
|
175
|
+
{ maxResults: 200 },
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
for (const match of matches) {
|
|
179
|
+
const name = match.captures.NAME?.trim();
|
|
180
|
+
if (!name) continue;
|
|
181
|
+
const key = `${def.kind}:${name}`;
|
|
182
|
+
snapshot.set(key, `${match.line}:${match.matchedText.slice(0, 400)}`);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return snapshot;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function diffEntitySnapshot(
|
|
190
|
+
prev: Map<string, string> | undefined,
|
|
191
|
+
next: Map<string, string>,
|
|
192
|
+
): { added: string[]; removed: string[]; modified: string[] } {
|
|
193
|
+
if (!prev) {
|
|
194
|
+
return { added: [...next.keys()], removed: [], modified: [] };
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const added: string[] = [];
|
|
198
|
+
const removed: string[] = [];
|
|
199
|
+
const modified: string[] = [];
|
|
200
|
+
|
|
201
|
+
for (const [key, value] of next.entries()) {
|
|
202
|
+
if (!prev.has(key)) {
|
|
203
|
+
added.push(key);
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
if (prev.get(key) !== value) {
|
|
207
|
+
modified.push(key);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
for (const key of prev.keys()) {
|
|
212
|
+
if (!next.has(key)) {
|
|
213
|
+
removed.push(key);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return { added, removed, modified };
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function getBlastFiles(cwd: string): string[] {
|
|
221
|
+
const now = Date.now();
|
|
222
|
+
const cached = blastFileCache.get(cwd);
|
|
223
|
+
if (cached && cached.expiresAt > now) return cached.files;
|
|
224
|
+
|
|
225
|
+
const files = getSourceFiles(cwd).slice(0, MAX_BLAST_FILES);
|
|
226
|
+
blastFileCache.set(cwd, { files, expiresAt: now + BLAST_CACHE_TTL_MS });
|
|
227
|
+
return files;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function computeBlastRadius(
|
|
231
|
+
entityNames: string[],
|
|
232
|
+
filePath: string,
|
|
233
|
+
cwd: string,
|
|
234
|
+
): {
|
|
235
|
+
entities: Array<{ entity: string; dependentFiles: number; references: number }>;
|
|
236
|
+
scannedFiles: number;
|
|
237
|
+
scannedBytes: number;
|
|
238
|
+
totalCandidates: number;
|
|
239
|
+
truncated: boolean;
|
|
240
|
+
elapsedMs: number;
|
|
241
|
+
} {
|
|
242
|
+
const startedAt = Date.now();
|
|
243
|
+
const limited = entityNames.slice(0, MAX_BLAST_ENTITIES);
|
|
244
|
+
if (limited.length === 0) {
|
|
245
|
+
return {
|
|
246
|
+
entities: [],
|
|
247
|
+
scannedFiles: 0,
|
|
248
|
+
scannedBytes: 0,
|
|
249
|
+
totalCandidates: 0,
|
|
250
|
+
truncated: false,
|
|
251
|
+
elapsedMs: 0,
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const regexByEntity = new Map(
|
|
256
|
+
limited.map((name) => [name, new RegExp(`\\b${escapeRegex(name)}\\b`, "g")]),
|
|
257
|
+
);
|
|
258
|
+
const files = getBlastFiles(cwd);
|
|
259
|
+
const stats = new Map(
|
|
260
|
+
limited.map((name) => [name, { dependentFiles: 0, references: 0 }]),
|
|
261
|
+
);
|
|
262
|
+
let scannedFiles = 0;
|
|
263
|
+
let scannedBytes = 0;
|
|
264
|
+
let truncated = false;
|
|
265
|
+
|
|
266
|
+
for (const candidate of files) {
|
|
267
|
+
if (Date.now() - startedAt > BLAST_MAX_ELAPSED_MS) {
|
|
268
|
+
truncated = true;
|
|
269
|
+
break;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
if (path.resolve(candidate) === path.resolve(filePath)) continue;
|
|
273
|
+
let size = 0;
|
|
274
|
+
try {
|
|
275
|
+
size = fs.statSync(candidate).size;
|
|
276
|
+
} catch {
|
|
277
|
+
continue;
|
|
278
|
+
}
|
|
279
|
+
if (size > BLAST_MAX_FILE_BYTES) continue;
|
|
280
|
+
if (scannedBytes + size > BLAST_MAX_TOTAL_BYTES) {
|
|
281
|
+
truncated = true;
|
|
282
|
+
break;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
let content = "";
|
|
286
|
+
try {
|
|
287
|
+
content = fs.readFileSync(candidate, "utf-8");
|
|
288
|
+
} catch {
|
|
289
|
+
continue;
|
|
290
|
+
}
|
|
291
|
+
scannedFiles += 1;
|
|
292
|
+
scannedBytes += size;
|
|
293
|
+
|
|
294
|
+
for (const [name, regex] of regexByEntity.entries()) {
|
|
295
|
+
const matches = content.match(regex);
|
|
296
|
+
if (!matches || matches.length === 0) continue;
|
|
297
|
+
const current = stats.get(name);
|
|
298
|
+
if (!current) continue;
|
|
299
|
+
current.dependentFiles += 1;
|
|
300
|
+
current.references += matches.length;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const entities = limited
|
|
305
|
+
.map((name) => ({ entity: name, ...stats.get(name)! }))
|
|
306
|
+
.sort((a, b) => b.dependentFiles - a.dependentFiles)
|
|
307
|
+
.slice(0, 5);
|
|
308
|
+
|
|
309
|
+
return {
|
|
310
|
+
entities,
|
|
311
|
+
scannedFiles,
|
|
312
|
+
scannedBytes,
|
|
313
|
+
totalCandidates: files.length,
|
|
314
|
+
truncated,
|
|
315
|
+
elapsedMs: Date.now() - startedAt,
|
|
316
|
+
};
|
|
317
|
+
}
|
|
28
318
|
|
|
29
319
|
const SILENT_ERROR_QUERY_IDS = new Set([
|
|
30
320
|
"empty-catch",
|
|
@@ -78,12 +368,25 @@ const treeSitterRunner: RunnerDefinition = {
|
|
|
78
368
|
async run(ctx: DispatchContext): Promise<RunnerResult> {
|
|
79
369
|
// Use singleton client — WASM must never be re-initialized after first call
|
|
80
370
|
const client = getSharedClient();
|
|
371
|
+
logTreeSitter({ phase: "runner_start", filePath: ctx.filePath });
|
|
81
372
|
if (!client.isAvailable()) {
|
|
373
|
+
logTreeSitter({
|
|
374
|
+
phase: "runner_skip",
|
|
375
|
+
filePath: ctx.filePath,
|
|
376
|
+
reason: "client_unavailable",
|
|
377
|
+
status: "skipped",
|
|
378
|
+
});
|
|
82
379
|
return { status: "skipped", diagnostics: [], semantic: "none" };
|
|
83
380
|
}
|
|
84
381
|
|
|
85
382
|
const initialized = await client.init();
|
|
86
383
|
if (!initialized) {
|
|
384
|
+
logTreeSitter({
|
|
385
|
+
phase: "runner_skip",
|
|
386
|
+
filePath: ctx.filePath,
|
|
387
|
+
reason: "client_init_failed",
|
|
388
|
+
status: "skipped",
|
|
389
|
+
});
|
|
87
390
|
return { status: "skipped", diagnostics: [], semantic: "none" };
|
|
88
391
|
}
|
|
89
392
|
|
|
@@ -106,6 +409,12 @@ const treeSitterRunner: RunnerDefinition = {
|
|
|
106
409
|
};
|
|
107
410
|
const languageId = EXT_TO_LANG[ext];
|
|
108
411
|
if (!languageId) {
|
|
412
|
+
logTreeSitter({
|
|
413
|
+
phase: "runner_skip",
|
|
414
|
+
filePath: ctx.filePath,
|
|
415
|
+
reason: `unsupported_extension:${ext}`,
|
|
416
|
+
status: "skipped",
|
|
417
|
+
});
|
|
109
418
|
return { status: "skipped", diagnostics: [], semantic: "none" };
|
|
110
419
|
}
|
|
111
420
|
|
|
@@ -132,8 +441,10 @@ const treeSitterRunner: RunnerDefinition = {
|
|
|
132
441
|
|
|
133
442
|
// Try cache
|
|
134
443
|
const cached = cache.get(ruleFiles);
|
|
444
|
+
let cacheHit = false;
|
|
135
445
|
if (cached) {
|
|
136
446
|
// Use cached queries
|
|
447
|
+
cacheHit = true;
|
|
137
448
|
languageQueries = cached.queries.map(
|
|
138
449
|
(q) =>
|
|
139
450
|
({
|
|
@@ -173,6 +484,16 @@ const treeSitterRunner: RunnerDefinition = {
|
|
|
173
484
|
}
|
|
174
485
|
|
|
175
486
|
if (languageQueries.length === 0) {
|
|
487
|
+
logTreeSitter({
|
|
488
|
+
phase: "runner_complete",
|
|
489
|
+
filePath,
|
|
490
|
+
languageId,
|
|
491
|
+
status: "succeeded",
|
|
492
|
+
diagnostics: 0,
|
|
493
|
+
blocking: 0,
|
|
494
|
+
queryCount: 0,
|
|
495
|
+
effectiveQueryCount: 0,
|
|
496
|
+
});
|
|
176
497
|
return { status: "succeeded", diagnostics: [], semantic: "none" };
|
|
177
498
|
}
|
|
178
499
|
|
|
@@ -186,6 +507,16 @@ const treeSitterRunner: RunnerDefinition = {
|
|
|
186
507
|
)
|
|
187
508
|
: languageQueries;
|
|
188
509
|
|
|
510
|
+
logTreeSitter({
|
|
511
|
+
phase: "queries_loaded",
|
|
512
|
+
filePath,
|
|
513
|
+
languageId,
|
|
514
|
+
queryCount: languageQueries.length,
|
|
515
|
+
effectiveQueryCount: effectiveQueries.length,
|
|
516
|
+
cacheHit,
|
|
517
|
+
metadata: { blockingOnly: !!ctx.blockingOnly },
|
|
518
|
+
});
|
|
519
|
+
|
|
189
520
|
const diagnostics: Diagnostic[] = [];
|
|
190
521
|
|
|
191
522
|
// Run each query against the file
|
|
@@ -244,15 +575,151 @@ const treeSitterRunner: RunnerDefinition = {
|
|
|
244
575
|
} catch (err) {
|
|
245
576
|
// Individual query failure shouldn't stop other queries
|
|
246
577
|
console.error(`[tree-sitter] Query ${query.id} failed:`, err);
|
|
578
|
+
logTreeSitter({
|
|
579
|
+
phase: "query_error",
|
|
580
|
+
filePath,
|
|
581
|
+
languageId,
|
|
582
|
+
queryId: query.id,
|
|
583
|
+
error: err instanceof Error ? err.message : String(err),
|
|
584
|
+
});
|
|
247
585
|
}
|
|
248
586
|
}
|
|
249
587
|
|
|
250
588
|
if (diagnostics.length === 0) {
|
|
589
|
+
try {
|
|
590
|
+
const snapshot = await extractEntitySnapshot(client, filePath, languageId);
|
|
591
|
+
const prev = entitySnapshotByFile.get(filePath);
|
|
592
|
+
const diff = diffEntitySnapshot(prev, snapshot);
|
|
593
|
+
entitySnapshotByFile.set(filePath, snapshot);
|
|
594
|
+
const changedEntityKeys = [...diff.added, ...diff.modified, ...diff.removed];
|
|
595
|
+
const changedNames = [...new Set(changedEntityKeys.map((k) => k.split(":")[1]).filter(Boolean))];
|
|
596
|
+
|
|
597
|
+
if (changedEntityKeys.length > 0) {
|
|
598
|
+
logTreeSitter({
|
|
599
|
+
phase: "entity_diff",
|
|
600
|
+
filePath,
|
|
601
|
+
languageId,
|
|
602
|
+
metadata: {
|
|
603
|
+
added: diff.added,
|
|
604
|
+
modified: diff.modified,
|
|
605
|
+
removed: diff.removed,
|
|
606
|
+
totalChanged: changedEntityKeys.length,
|
|
607
|
+
},
|
|
608
|
+
});
|
|
609
|
+
|
|
610
|
+
const lastBlast = blastCooldownByFile.get(filePath) ?? 0;
|
|
611
|
+
if (Date.now() - lastBlast < BLAST_COOLDOWN_MS) {
|
|
612
|
+
logTreeSitter({
|
|
613
|
+
phase: "blast_radius",
|
|
614
|
+
filePath,
|
|
615
|
+
languageId,
|
|
616
|
+
metadata: { skipped: "cooldown", cooldownMs: BLAST_COOLDOWN_MS },
|
|
617
|
+
});
|
|
618
|
+
} else {
|
|
619
|
+
blastCooldownByFile.set(filePath, Date.now());
|
|
620
|
+
const blastRadius = computeBlastRadius(changedNames, filePath, ctx.cwd);
|
|
621
|
+
logTreeSitter({
|
|
622
|
+
phase: "blast_radius",
|
|
623
|
+
filePath,
|
|
624
|
+
languageId,
|
|
625
|
+
metadata: {
|
|
626
|
+
entities: blastRadius.entities,
|
|
627
|
+
scannedFiles: blastRadius.scannedFiles,
|
|
628
|
+
scannedBytes: blastRadius.scannedBytes,
|
|
629
|
+
totalCandidates: blastRadius.totalCandidates,
|
|
630
|
+
truncated: blastRadius.truncated,
|
|
631
|
+
elapsedMs: blastRadius.elapsedMs,
|
|
632
|
+
},
|
|
633
|
+
});
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
} catch {}
|
|
637
|
+
|
|
638
|
+
logTreeSitter({
|
|
639
|
+
phase: "runner_complete",
|
|
640
|
+
filePath,
|
|
641
|
+
languageId,
|
|
642
|
+
status: "succeeded",
|
|
643
|
+
diagnostics: 0,
|
|
644
|
+
blocking: 0,
|
|
645
|
+
queryCount: languageQueries.length,
|
|
646
|
+
effectiveQueryCount: effectiveQueries.length,
|
|
647
|
+
});
|
|
251
648
|
return { status: "succeeded", diagnostics: [], semantic: "none" };
|
|
252
649
|
}
|
|
253
650
|
|
|
254
651
|
// Check if any blocking issues
|
|
255
652
|
const hasBlocking = diagnostics.some((d) => d.semantic === "blocking");
|
|
653
|
+
const blockingCount = diagnostics.filter(
|
|
654
|
+
(d) => d.semantic === "blocking",
|
|
655
|
+
).length;
|
|
656
|
+
try {
|
|
657
|
+
const snapshot = await extractEntitySnapshot(client, filePath, languageId);
|
|
658
|
+
const prev = entitySnapshotByFile.get(filePath);
|
|
659
|
+
const diff = diffEntitySnapshot(prev, snapshot);
|
|
660
|
+
entitySnapshotByFile.set(filePath, snapshot);
|
|
661
|
+
const changedEntityKeys = [
|
|
662
|
+
...diff.added,
|
|
663
|
+
...diff.modified,
|
|
664
|
+
...diff.removed,
|
|
665
|
+
];
|
|
666
|
+
const changedNames = [
|
|
667
|
+
...new Set(changedEntityKeys.map((k) => k.split(":")[1]).filter(Boolean)),
|
|
668
|
+
];
|
|
669
|
+
|
|
670
|
+
if (changedEntityKeys.length > 0) {
|
|
671
|
+
logTreeSitter({
|
|
672
|
+
phase: "entity_diff",
|
|
673
|
+
filePath,
|
|
674
|
+
languageId,
|
|
675
|
+
metadata: {
|
|
676
|
+
added: diff.added,
|
|
677
|
+
modified: diff.modified,
|
|
678
|
+
removed: diff.removed,
|
|
679
|
+
totalChanged: changedEntityKeys.length,
|
|
680
|
+
},
|
|
681
|
+
});
|
|
682
|
+
|
|
683
|
+
const lastBlast = blastCooldownByFile.get(filePath) ?? 0;
|
|
684
|
+
if (Date.now() - lastBlast < BLAST_COOLDOWN_MS) {
|
|
685
|
+
logTreeSitter({
|
|
686
|
+
phase: "blast_radius",
|
|
687
|
+
filePath,
|
|
688
|
+
languageId,
|
|
689
|
+
metadata: { skipped: "cooldown", cooldownMs: BLAST_COOLDOWN_MS },
|
|
690
|
+
});
|
|
691
|
+
} else {
|
|
692
|
+
blastCooldownByFile.set(filePath, Date.now());
|
|
693
|
+
const blastRadius = computeBlastRadius(changedNames, filePath, ctx.cwd);
|
|
694
|
+
logTreeSitter({
|
|
695
|
+
phase: "blast_radius",
|
|
696
|
+
filePath,
|
|
697
|
+
languageId,
|
|
698
|
+
metadata: {
|
|
699
|
+
entities: blastRadius.entities,
|
|
700
|
+
scannedFiles: blastRadius.scannedFiles,
|
|
701
|
+
scannedBytes: blastRadius.scannedBytes,
|
|
702
|
+
totalCandidates: blastRadius.totalCandidates,
|
|
703
|
+
truncated: blastRadius.truncated,
|
|
704
|
+
elapsedMs: blastRadius.elapsedMs,
|
|
705
|
+
},
|
|
706
|
+
});
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
} catch {
|
|
710
|
+
// best-effort experimental telemetry only
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
logTreeSitter({
|
|
714
|
+
phase: "runner_complete",
|
|
715
|
+
filePath,
|
|
716
|
+
languageId,
|
|
717
|
+
status: hasBlocking ? "failed" : "succeeded",
|
|
718
|
+
diagnostics: diagnostics.length,
|
|
719
|
+
blocking: blockingCount,
|
|
720
|
+
queryCount: languageQueries.length,
|
|
721
|
+
effectiveQueryCount: effectiveQueries.length,
|
|
722
|
+
});
|
|
256
723
|
|
|
257
724
|
return {
|
|
258
725
|
status: hasBlocking ? "failed" : "succeeded",
|