@mrclrchtr/supi-code-intelligence 4.9.0 → 4.10.0

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 (28) hide show
  1. package/node_modules/@mrclrchtr/supi-code-runtime/package.json +1 -1
  2. package/node_modules/@mrclrchtr/supi-core/package.json +1 -1
  3. package/node_modules/@mrclrchtr/supi-core/src/debug-timing.ts +34 -21
  4. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-code-runtime/package.json +1 -1
  5. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/package.json +1 -1
  6. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/debug-timing.ts +34 -21
  7. package/node_modules/@mrclrchtr/supi-lsp/package.json +3 -3
  8. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-timing.ts +155 -0
  9. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostics.ts +70 -34
  10. package/node_modules/@mrclrchtr/supi-lsp/src/client/transport.ts +104 -5
  11. package/node_modules/@mrclrchtr/supi-tree-sitter/README.md +10 -0
  12. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-code-runtime/package.json +1 -1
  13. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/package.json +1 -1
  14. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/src/debug-timing.ts +34 -21
  15. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/web-tree-sitter/debug/web-tree-sitter.wasm +0 -0
  16. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/web-tree-sitter/debug/web-tree-sitter.wasm.map +4 -4
  17. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/web-tree-sitter/package.json +1 -1
  18. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/web-tree-sitter/web-tree-sitter.wasm +0 -0
  19. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/web-tree-sitter/web-tree-sitter.wasm.map +4 -4
  20. package/node_modules/@mrclrchtr/supi-tree-sitter/package.json +4 -3
  21. package/node_modules/@mrclrchtr/supi-tree-sitter/resources/grammars/kotlin/tree-sitter-kotlin.wasm.json +1 -1
  22. package/node_modules/@mrclrchtr/supi-tree-sitter/resources/grammars/sql/tree-sitter-sql.wasm.json +1 -1
  23. package/node_modules/@mrclrchtr/supi-tree-sitter/src/session/runtime.ts +106 -2
  24. package/package.json +5 -5
  25. package/src/analysis/refactor/apply.ts +14 -19
  26. package/src/analysis/refactor/position.ts +47 -0
  27. package/src/analysis/refactor/safety.ts +18 -10
  28. package/src/analysis/search/ast-scan-timing.ts +0 -2
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mrclrchtr/supi-tree-sitter",
3
- "version": "4.9.0",
3
+ "version": "4.10.0",
4
4
  "description": "Structural AST analysis for SuPi code intelligence",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -34,8 +34,8 @@
34
34
  ],
35
35
  "dependencies": {
36
36
  "web-tree-sitter": "^0.26.8",
37
- "@mrclrchtr/supi-code-runtime": "4.9.0",
38
- "@mrclrchtr/supi-core": "4.9.0"
37
+ "@mrclrchtr/supi-code-runtime": "4.10.0",
38
+ "@mrclrchtr/supi-core": "4.10.0"
39
39
  },
40
40
  "bundledDependencies": [
41
41
  "@mrclrchtr/supi-code-runtime",
@@ -49,6 +49,7 @@
49
49
  "./provider/tree-sitter-provider": "./src/provider/tree-sitter-provider.ts"
50
50
  },
51
51
  "scripts": {
52
+ "bench:structural": "vitest bench __tests__/benchmark/structural-baseline.bench.ts --run",
52
53
  "vendor:wasm": "node scripts/vendor-wasm.mjs",
53
54
  "check:wasm": "node scripts/vendor-wasm.mjs --check",
54
55
  "generate:kotlin-wasm": "node scripts/generate-kotlin-wasm.mjs",
@@ -6,7 +6,7 @@
6
6
  "releaseAsset": "https://github.com/fwcd/tree-sitter-kotlin/releases/download/0.3.8/tree-sitter-kotlin.wasm"
7
7
  },
8
8
  "generatedWith": {
9
- "treeSitterCli": "0.26.11"
9
+ "treeSitterCli": "0.26.12"
10
10
  },
11
11
  "sha256": "e08447e36ad5d86f39f91f13defce0636aea1490b8d65dafde6fa72649422061"
12
12
  }
@@ -5,7 +5,7 @@
5
5
  "repository": "https://github.com/derekstride/tree-sitter-sql"
6
6
  },
7
7
  "generatedWith": {
8
- "treeSitterCli": "0.26.11"
8
+ "treeSitterCli": "0.26.12"
9
9
  },
10
10
  "sha256": "9b93a15dc3eade66df4347731f2687b939daf987c8382660d6118e863604ab09",
11
11
  "trust": {
@@ -1,7 +1,9 @@
1
1
  // Tree-sitter runtime — parser management, parse, and query services.
2
2
 
3
+ import { createHash } from "node:crypto";
3
4
  import * as fs from "node:fs";
4
5
  import * as path from "node:path";
6
+ import { startDebugTimer } from "@mrclrchtr/supi-core/debug";
5
7
  import { resolveToolPath } from "@mrclrchtr/supi-core/path";
6
8
  import type { Language, Parser, Tree } from "web-tree-sitter";
7
9
  import { nodeToRange } from "../coordinates.ts";
@@ -126,11 +128,25 @@ export class TreeSitterRuntime {
126
128
  };
127
129
  }
128
130
 
131
+ const timer = startDebugTimer();
132
+ const parserState = this.parsers.has(grammarId)
133
+ ? "reused"
134
+ : this.parserPromises.has(grammarId)
135
+ ? "initializing"
136
+ : "cold";
137
+
129
138
  // Read the file
130
139
  let source: string;
131
140
  try {
132
141
  source = fs.readFileSync(resolvedPath, "utf-8");
133
142
  } catch (err: unknown) {
143
+ finishStructuralTiming(timer, {
144
+ operation: "parse",
145
+ grammar: grammarId,
146
+ parserState,
147
+ outcome: "file-access-error",
148
+ finalPhase: "file-read",
149
+ });
134
150
  const message = err instanceof Error ? err.message : "File could not be read";
135
151
  return {
136
152
  kind: "file-access-error",
@@ -138,15 +154,42 @@ export class TreeSitterRuntime {
138
154
  message,
139
155
  };
140
156
  }
157
+ timer.mark("file-read");
158
+
159
+ if (timer.enabled) {
160
+ try {
161
+ createHash("sha256").update(source).digest();
162
+ } catch {
163
+ // Timing-only hashing must not change structural behavior.
164
+ }
165
+ }
166
+ timer.mark("content-hash");
141
167
 
168
+ let parserReady = false;
142
169
  try {
143
170
  const entry = await this.ensureGrammarParser(grammarId);
171
+ timer.mark("parser-setup");
172
+ parserReady = true;
144
173
  const tree = entry.parser.parse(source);
174
+ finishStructuralTiming(timer, {
175
+ operation: "parse",
176
+ grammar: grammarId,
177
+ parserState,
178
+ outcome: "completed",
179
+ finalPhase: "parse",
180
+ });
145
181
  return {
146
182
  kind: "success",
147
183
  data: { tree: tree as Tree, source, resolvedPath, grammarId },
148
184
  };
149
185
  } catch (err: unknown) {
186
+ finishStructuralTiming(timer, {
187
+ operation: "parse",
188
+ grammar: grammarId,
189
+ parserState,
190
+ outcome: "runtime-error",
191
+ finalPhase: parserReady ? "parse" : "parser-setup",
192
+ });
150
193
  return { kind: "runtime-error", message: formatError(err, "Parser initialization failed") };
151
194
  }
152
195
  }
@@ -169,16 +212,27 @@ export class TreeSitterRuntime {
169
212
  const parseResult = await this.parseFile(filePath);
170
213
  if (parseResult.kind !== "success") return parseResult;
171
214
 
172
- const { tree, source } = parseResult.data;
215
+ const { grammarId, tree, source } = parseResult.data;
216
+ const timer = startDebugTimer();
217
+ let compiled = false;
173
218
 
174
219
  try {
175
- const entry = await this.ensureGrammarParser(parseResult.data.grammarId);
220
+ const entry = await this.ensureGrammarParser(grammarId);
176
221
  const mod = await this.ensureParserInit();
177
222
  let query: InstanceType<typeof mod.Query>;
178
223
 
179
224
  try {
180
225
  query = new mod.Query(entry.language, queryString);
226
+ compiled = true;
227
+ timer.mark("query-compilation");
181
228
  } catch (err: unknown) {
229
+ finishStructuralTiming(timer, {
230
+ operation: "query",
231
+ grammar: grammarId,
232
+ outcome: "validation-error",
233
+ captureCount: 0,
234
+ finalPhase: "query-compilation",
235
+ });
182
236
  return { kind: "validation-error", message: `Invalid query: ${formatError(err)}` };
183
237
  }
184
238
 
@@ -195,11 +249,25 @@ export class TreeSitterRuntime {
195
249
  });
196
250
  }
197
251
  }
252
+ finishStructuralTiming(timer, {
253
+ operation: "query",
254
+ grammar: grammarId,
255
+ outcome: "completed",
256
+ captureCount: captures.length,
257
+ finalPhase: "query-execution",
258
+ });
198
259
  return { kind: "success", data: captures };
199
260
  } finally {
200
261
  query.delete();
201
262
  }
202
263
  } catch (err: unknown) {
264
+ finishStructuralTiming(timer, {
265
+ operation: "query",
266
+ grammar: grammarId,
267
+ outcome: "runtime-error",
268
+ captureCount: 0,
269
+ finalPhase: compiled ? "query-execution" : "query-compilation",
270
+ });
203
271
  return { kind: "runtime-error", message: formatError(err, "Query execution failed") };
204
272
  } finally {
205
273
  tree.delete();
@@ -239,6 +307,42 @@ export class TreeSitterRuntime {
239
307
  /** Max query string length to prevent ReDoS via overly complex Tree-sitter patterns. */
240
308
  const MAX_QUERY_LENGTH = 10_000;
241
309
 
310
+ type StructuralTimer = ReturnType<typeof startDebugTimer>;
311
+ type ParserState = "cold" | "initializing" | "reused";
312
+
313
+ type StructuralTimingObservation =
314
+ | {
315
+ readonly operation: "parse";
316
+ readonly grammar: GrammarId;
317
+ readonly parserState: ParserState;
318
+ readonly outcome: "completed" | "file-access-error" | "runtime-error";
319
+ readonly finalPhase: "file-read" | "parse" | "parser-setup";
320
+ }
321
+ | {
322
+ readonly operation: "query";
323
+ readonly grammar: GrammarId;
324
+ readonly outcome: "completed" | "runtime-error" | "validation-error";
325
+ readonly captureCount: number;
326
+ readonly finalPhase: "query-compilation" | "query-execution";
327
+ };
328
+
329
+ function finishStructuralTiming(
330
+ timer: StructuralTimer,
331
+ observation: StructuralTimingObservation,
332
+ ): void {
333
+ const { finalPhase, ...data } = observation;
334
+ timer.finish(
335
+ () => ({
336
+ source: "tree-sitter",
337
+ level: "debug",
338
+ category: `structural.${data.operation}.timing`,
339
+ message: `Tree-sitter ${data.operation} ${data.outcome}`,
340
+ data: { ...data },
341
+ }),
342
+ finalPhase,
343
+ );
344
+ }
345
+
242
346
  /** Format errors with their cause chain's first message for user-facing tool output. */
243
347
  function formatError(err: unknown, fallback = "Operation failed"): string {
244
348
  if (!(err instanceof Error)) return String(err || fallback);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mrclrchtr/supi-code-intelligence",
3
- "version": "4.9.0",
3
+ "version": "4.10.0",
4
4
  "description": "LSP and AST navigation, search, diagnostics, and refactoring",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -35,10 +35,10 @@
35
35
  ],
36
36
  "dependencies": {
37
37
  "yaml": "^2.9.0",
38
- "@mrclrchtr/supi-code-runtime": "4.9.0",
39
- "@mrclrchtr/supi-core": "4.9.0",
40
- "@mrclrchtr/supi-tree-sitter": "4.9.0",
41
- "@mrclrchtr/supi-lsp": "4.9.0"
38
+ "@mrclrchtr/supi-code-runtime": "4.10.0",
39
+ "@mrclrchtr/supi-tree-sitter": "4.10.0",
40
+ "@mrclrchtr/supi-lsp": "4.10.0",
41
+ "@mrclrchtr/supi-core": "4.10.0"
42
42
  },
43
43
  "bundledDependencies": [
44
44
  "@mrclrchtr/supi-code-runtime",
@@ -16,7 +16,7 @@ import { readFileSync, renameSync, rmSync, statSync, writeFileSync } from "node:
16
16
  import { basename, dirname, join } from "node:path";
17
17
  import { withFileMutationQueue } from "@earendil-works/pi-coding-agent";
18
18
  import type { FileEdit, WorkspaceEdit } from "@mrclrchtr/supi-code-runtime/api";
19
- import { compareCodePositions } from "./position.ts";
19
+ import { compareCodePositions, createLogicalLineIndex } from "./position.ts";
20
20
  import { validateEditAgainstFiles } from "./safety.ts";
21
21
 
22
22
  export type ApplyResult =
@@ -158,18 +158,21 @@ function buildTransformedContents(
158
158
  }
159
159
 
160
160
  function applyEditsToContent(content: string, edits: FileEdit[]): string {
161
- const lines = content.split("\n");
162
- const sortedEdits = [...edits].sort(
163
- (left, right) =>
164
- // Descending by start position so later edits don't shift earlier ones.
165
- compareCodePositions(right.range.start, left.range.start) ||
166
- compareCodePositions(right.range.end, left.range.end),
167
- );
161
+ const lineIndex = createLogicalLineIndex(content);
162
+ const sortedEdits = edits
163
+ .map((edit, protocolIndex) => ({ edit, protocolIndex }))
164
+ .sort(
165
+ (left, right) =>
166
+ // Descending positions prevent shifts. Reverse protocol order at one
167
+ // position so sequential inserts appear in their supplied order.
168
+ compareCodePositions(right.edit.range.start, left.edit.range.start) ||
169
+ right.protocolIndex - left.protocolIndex,
170
+ );
168
171
 
169
172
  let updated = content;
170
- for (const fileEdit of sortedEdits) {
171
- const startOffset = toOffset(lines, fileEdit.range.start.line, fileEdit.range.start.character);
172
- const endOffset = toOffset(lines, fileEdit.range.end.line, fileEdit.range.end.character);
173
+ for (const { edit: fileEdit } of sortedEdits) {
174
+ const startOffset = lineIndex.offsetAt(fileEdit.range.start);
175
+ const endOffset = lineIndex.offsetAt(fileEdit.range.end);
173
176
  updated = updated.slice(0, startOffset) + fileEdit.newText + updated.slice(endOffset);
174
177
  }
175
178
 
@@ -266,14 +269,6 @@ function rollbackCommittedFiles(
266
269
  }
267
270
  }
268
271
 
269
- function toOffset(lines: string[], line: number, character: number): number {
270
- let offset = 0;
271
- for (let index = 0; index < line && index < lines.length; index++) {
272
- offset += lines[index].length + 1;
273
- }
274
- return offset + character;
275
- }
276
-
277
272
  function toErrorMessage(error: unknown): string {
278
273
  return error instanceof Error ? error.message : String(error);
279
274
  }
@@ -1,5 +1,52 @@
1
1
  import type { CodePosition } from "@mrclrchtr/supi-code-runtime/api";
2
2
 
3
+ interface IndexedLine {
4
+ readonly startOffset: number;
5
+ readonly length: number;
6
+ }
7
+
8
+ /** Logical source lines with offsets into the unchanged source string. */
9
+ export interface LogicalLineIndex {
10
+ readonly lineCount: number;
11
+ /** Get one logical line length in UTF-16 code units. */
12
+ lineLength(line: number): number | undefined;
13
+ /** Convert a validated zero-based UTF-16 position to a source-string offset. */
14
+ offsetAt(position: CodePosition): number;
15
+ }
16
+
17
+ /**
18
+ * Index LF, CRLF, and bare-CR lines without normalizing source bytes.
19
+ *
20
+ * JavaScript string offsets and lengths use UTF-16 code units, which are the
21
+ * only LSP coordinate encoding that the refactor path supports.
22
+ */
23
+ export function createLogicalLineIndex(source: string): LogicalLineIndex {
24
+ const lines: IndexedLine[] = [];
25
+ let lineStart = 0;
26
+
27
+ for (let offset = 0; offset < source.length; offset++) {
28
+ const codeUnit = source.charCodeAt(offset);
29
+ if (codeUnit !== 0x0a && codeUnit !== 0x0d) continue;
30
+
31
+ lines.push({ startOffset: lineStart, length: offset - lineStart });
32
+ if (codeUnit === 0x0d && source.charCodeAt(offset + 1) === 0x0a) offset++;
33
+ lineStart = offset + 1;
34
+ }
35
+ lines.push({ startOffset: lineStart, length: source.length - lineStart });
36
+
37
+ return {
38
+ lineCount: lines.length,
39
+ lineLength: (line) => lines[line]?.length,
40
+ offsetAt: (position) => {
41
+ const line = lines[position.line];
42
+ if (!line || position.character < 0 || position.character > line.length) {
43
+ throw new RangeError("Position is outside the logical source lines");
44
+ }
45
+ return line.startOffset + position.character;
46
+ },
47
+ };
48
+ }
49
+
3
50
  /** Compare zero-based source positions lexicographically by line, then character. */
4
51
  export function compareCodePositions(left: CodePosition, right: CodePosition): number {
5
52
  return left.line - right.line || left.character - right.character;
@@ -7,7 +7,7 @@
7
7
 
8
8
  import { readFileSync } from "node:fs";
9
9
  import type { FileEdit, WorkspaceEdit } from "@mrclrchtr/supi-code-runtime/api";
10
- import { compareCodePositions } from "./position.ts";
10
+ import { compareCodePositions, createLogicalLineIndex, type LogicalLineIndex } from "./position.ts";
11
11
 
12
12
  export type ValidationResult = { safe: true } | { safe: false; reason: string };
13
13
 
@@ -76,12 +76,12 @@ export function validateEditAgainstFiles(edit: WorkspaceEdit): ValidationResult
76
76
  };
77
77
  }
78
78
 
79
- const lines = content.split("\n");
79
+ const lineIndex = createLogicalLineIndex(content);
80
80
  for (const fileEdit of fileEdits) {
81
- const lineValidation = validateLineBounds(file, fileEdit, lines);
81
+ const lineValidation = validateLineBounds(file, fileEdit, lineIndex);
82
82
  if (!lineValidation.safe) return lineValidation;
83
83
 
84
- const characterValidation = validateCharacterBounds(file, fileEdit, lines);
84
+ const characterValidation = validateCharacterBounds(file, fileEdit, lineIndex);
85
85
  if (!characterValidation.safe) return characterValidation;
86
86
  }
87
87
  }
@@ -89,19 +89,27 @@ export function validateEditAgainstFiles(edit: WorkspaceEdit): ValidationResult
89
89
  return { safe: true };
90
90
  }
91
91
 
92
- function validateLineBounds(file: string, edit: FileEdit, lines: string[]): ValidationResult {
93
- const maxLine = lines.length - 1;
92
+ function validateLineBounds(
93
+ file: string,
94
+ edit: FileEdit,
95
+ lineIndex: LogicalLineIndex,
96
+ ): ValidationResult {
97
+ const maxLine = lineIndex.lineCount - 1;
94
98
  if (edit.range.start.line > maxLine || edit.range.end.line > maxLine) {
95
99
  return {
96
100
  safe: false,
97
- reason: `Edit in file "${file}" references line ${Math.max(edit.range.start.line, edit.range.end.line)}, but the file has only ${lines.length} line${lines.length === 1 ? "" : "s"}`,
101
+ reason: `Edit in file "${file}" references line ${Math.max(edit.range.start.line, edit.range.end.line)}, but the file has only ${lineIndex.lineCount} line${lineIndex.lineCount === 1 ? "" : "s"}`,
98
102
  };
99
103
  }
100
104
  return { safe: true };
101
105
  }
102
106
 
103
- function validateCharacterBounds(file: string, edit: FileEdit, lines: string[]): ValidationResult {
104
- const startLineLength = lines[edit.range.start.line]?.length ?? 0;
107
+ function validateCharacterBounds(
108
+ file: string,
109
+ edit: FileEdit,
110
+ lineIndex: LogicalLineIndex,
111
+ ): ValidationResult {
112
+ const startLineLength = lineIndex.lineLength(edit.range.start.line) ?? 0;
105
113
  if (edit.range.start.character > startLineLength) {
106
114
  return {
107
115
  safe: false,
@@ -109,7 +117,7 @@ function validateCharacterBounds(file: string, edit: FileEdit, lines: string[]):
109
117
  };
110
118
  }
111
119
 
112
- const endLineLength = lines[edit.range.end.line]?.length ?? 0;
120
+ const endLineLength = lineIndex.lineLength(edit.range.end.line) ?? 0;
113
121
  if (edit.range.end.character > endLineLength) {
114
122
  return {
115
123
  safe: false,
@@ -3,7 +3,6 @@ import type { StructuralSearchOperation } from "@mrclrchtr/supi-tree-sitter/api"
3
3
  import type { StructuredFileAnalysis, StructuredPatternParams } from "./pattern-analysis.ts";
4
4
 
5
5
  interface ScanContext {
6
- readonly cwd: string;
7
6
  readonly params: StructuredPatternParams;
8
7
  }
9
8
 
@@ -40,7 +39,6 @@ export function startAstScanTimer(): AstScanTimer {
40
39
  level: "debug",
41
40
  category: "ast-scan.timing",
42
41
  message: `AST ${input.context.params.kind} scan analyzed ${input.analysis.analyzedFileCount} files`,
43
- cwd: input.context.cwd,
44
42
  data: {
45
43
  kind: input.context.params.kind,
46
44
  operation: input.operation,