@plannotator/pi-extension 0.19.15 → 0.19.16

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.
@@ -0,0 +1,437 @@
1
+ // @generated — DO NOT EDIT. Source: packages/shared/code-nav.ts
2
+ /**
3
+ * Search-based code navigation — shared types and pure logic.
4
+ *
5
+ * Runtime-agnostic: both Bun and Node servers provide their own
6
+ * CodeNavRuntime implementation to run subprocess commands.
7
+ */
8
+
9
+ function validateFilePath(filePath: string): void {
10
+ if (filePath.includes("..") || filePath.startsWith("/")) {
11
+ throw new Error("Invalid file path");
12
+ }
13
+ }
14
+
15
+ // ---------------------------------------------------------------------------
16
+ // Types
17
+ // ---------------------------------------------------------------------------
18
+
19
+ export interface CodeNavRequest {
20
+ symbol: string;
21
+ filePath: string;
22
+ line: number;
23
+ charStart: number;
24
+ side: "old" | "new";
25
+ language?: string;
26
+ }
27
+
28
+ export interface CodeNavLocation {
29
+ kind: "definition" | "reference";
30
+ confidence: "likely" | "possible";
31
+ filePath: string;
32
+ line: number;
33
+ column: number;
34
+ snippet: string;
35
+ }
36
+
37
+ export interface CodeNavResponse {
38
+ backend: "search" | "unavailable";
39
+ complete: boolean;
40
+ definitions: CodeNavLocation[];
41
+ references: CodeNavLocation[];
42
+ stats: { elapsedMs: number; capped: boolean };
43
+ searchScope: "head";
44
+ }
45
+
46
+ export interface CodeNavRuntime {
47
+ runCommand: (
48
+ command: string,
49
+ args: string[],
50
+ options?: { cwd?: string; timeoutMs?: number },
51
+ ) => Promise<{ stdout: string; stderr: string; exitCode: number }>;
52
+ }
53
+
54
+ // ---------------------------------------------------------------------------
55
+ // Constants
56
+ // ---------------------------------------------------------------------------
57
+
58
+ const CODE_NAV_IGNORED_GLOBS = [
59
+ "node_modules",
60
+ ".git",
61
+ "dist",
62
+ "build",
63
+ ".next",
64
+ "__pycache__",
65
+ ".turbo",
66
+ ".cache",
67
+ "target",
68
+ "vendor",
69
+ "coverage",
70
+ ".venv",
71
+ ".pytest_cache",
72
+ ];
73
+
74
+ const RG_TYPE_MAP: Record<string, string> = {
75
+ typescript: "ts",
76
+ javascript: "js",
77
+ python: "py",
78
+ go: "go",
79
+ rust: "rust",
80
+ java: "java",
81
+ ruby: "ruby",
82
+ cpp: "cpp",
83
+ c: "c",
84
+ };
85
+
86
+ // ---------------------------------------------------------------------------
87
+ // Definition patterns
88
+ // ---------------------------------------------------------------------------
89
+
90
+ interface DefinitionPatternSet {
91
+ languages: string[];
92
+ patterns: string[];
93
+ }
94
+
95
+ const DEFINITION_PATTERNS: DefinitionPatternSet[] = [
96
+ {
97
+ languages: ["typescript", "javascript"],
98
+ patterns: [
99
+ String.raw`(?:export\s+)?(?:async\s+)?function\s+SYMBOL\b`,
100
+ String.raw`(?:export\s+)?(?:const|let|var)\s+SYMBOL\s*[=:]`,
101
+ String.raw`(?:export\s+)?class\s+SYMBOL\b`,
102
+ String.raw`(?:export\s+)?(?:interface|type)\s+SYMBOL\b`,
103
+ String.raw`(?:export\s+)?enum\s+SYMBOL\b`,
104
+ String.raw`^\s+(?:(?:async|static|readonly|get|set|private|protected|public)\s+)+SYMBOL\s*[(<:]`,
105
+ ],
106
+ },
107
+ {
108
+ languages: ["python"],
109
+ patterns: [
110
+ String.raw`(?:^|\s)def\s+SYMBOL\s*\(`,
111
+ String.raw`(?:^|\s)class\s+SYMBOL\b`,
112
+ String.raw`^SYMBOL\s*=`,
113
+ ],
114
+ },
115
+ {
116
+ languages: ["go"],
117
+ patterns: [
118
+ String.raw`func\s+(?:\([^)]+\)\s+)?SYMBOL\s*\(`,
119
+ String.raw`type\s+SYMBOL\s`,
120
+ String.raw`var\s+SYMBOL\s`,
121
+ ],
122
+ },
123
+ {
124
+ languages: ["rust"],
125
+ patterns: [
126
+ String.raw`(?:pub(?:\([^)]*\))?\s+)?fn\s+SYMBOL\b`,
127
+ String.raw`(?:pub(?:\([^)]*\))?\s+)?struct\s+SYMBOL\b`,
128
+ String.raw`(?:pub(?:\([^)]*\))?\s+)?enum\s+SYMBOL\b`,
129
+ String.raw`(?:pub(?:\([^)]*\))?\s+)?trait\s+SYMBOL\b`,
130
+ String.raw`(?:pub(?:\([^)]*\))?\s+)?type\s+SYMBOL\b`,
131
+ String.raw`(?:pub(?:\([^)]*\))?\s+)?mod\s+SYMBOL\b`,
132
+ ],
133
+ },
134
+ ];
135
+
136
+ const GENERIC_DEFINITION_PATTERNS: string[] = [
137
+ String.raw`(?:function|def|func|fn|class|struct|enum|trait|interface|type)\s+SYMBOL\b`,
138
+ String.raw`(?:const|let|var|val)\s+SYMBOL\s*[=:]`,
139
+ ];
140
+
141
+ // ---------------------------------------------------------------------------
142
+ // Helpers
143
+ // ---------------------------------------------------------------------------
144
+
145
+ function escapeRegex(str: string): string {
146
+ return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
147
+ }
148
+
149
+ function sameDirectory(a: string, b: string): boolean {
150
+ const dirA = a.lastIndexOf("/");
151
+ const dirB = b.lastIndexOf("/");
152
+ if (dirA === -1 && dirB === -1) return true;
153
+ return a.slice(0, dirA) === b.slice(0, dirB);
154
+ }
155
+
156
+ function isTestFile(filePath: string): boolean {
157
+ return /(?:test|spec|__tests__|_test\.|\.test\.|\.spec\.)/i.test(filePath);
158
+ }
159
+
160
+ // ---------------------------------------------------------------------------
161
+ // rg argument construction
162
+ // ---------------------------------------------------------------------------
163
+
164
+ export function buildRgArgs(symbol: string, language?: string): string[] {
165
+ const args: string[] = [
166
+ "--json",
167
+ "--line-number",
168
+ "--column",
169
+ "--max-count",
170
+ "50",
171
+ "--max-filesize",
172
+ "1M",
173
+ "--no-messages",
174
+ ];
175
+
176
+ for (const dir of CODE_NAV_IGNORED_GLOBS) {
177
+ args.push("--glob", `!${dir}`);
178
+ }
179
+
180
+ if (language) {
181
+ const rgType = RG_TYPE_MAP[language];
182
+ if (rgType) args.push("--type", rgType);
183
+ }
184
+
185
+ args.push("--word-regexp", "--", escapeRegex(symbol), ".");
186
+
187
+ return args;
188
+ }
189
+
190
+ // ---------------------------------------------------------------------------
191
+ // rg JSON output parsing
192
+ // ---------------------------------------------------------------------------
193
+
194
+ interface RgMatchData {
195
+ path: { text: string };
196
+ lines: { text: string };
197
+ line_number: number;
198
+ submatches: Array<{ start: number; end: number }>;
199
+ }
200
+
201
+ const PARSE_CAP = 500;
202
+
203
+ export function parseRgJsonOutput(
204
+ stdout: string,
205
+ symbol: string,
206
+ language?: string,
207
+ ): CodeNavLocation[] {
208
+ const locations: CodeNavLocation[] = [];
209
+ const lines = stdout.split("\n");
210
+
211
+ for (const line of lines) {
212
+ if (locations.length >= PARSE_CAP) break;
213
+ if (!line.trim()) continue;
214
+
215
+ let parsed: { type: string; data: RgMatchData };
216
+ try {
217
+ parsed = JSON.parse(line);
218
+ } catch {
219
+ continue;
220
+ }
221
+
222
+ if (parsed.type !== "match") continue;
223
+
224
+ const d = parsed.data;
225
+ const snippet = d.lines.text.trimEnd();
226
+ const column = d.submatches?.[0]?.start ?? 0;
227
+ const kind = classifyMatch(snippet, symbol, language);
228
+ const filePath = d.path.text.startsWith("./")
229
+ ? d.path.text.slice(2)
230
+ : d.path.text;
231
+
232
+ locations.push({
233
+ kind,
234
+ confidence: kind === "definition" ? "likely" : "possible",
235
+ filePath,
236
+ line: d.line_number,
237
+ column,
238
+ snippet: snippet.length > 200 ? snippet.slice(0, 200) + "…" : snippet,
239
+ });
240
+ }
241
+
242
+ return locations;
243
+ }
244
+
245
+ // ---------------------------------------------------------------------------
246
+ // Match classification
247
+ // ---------------------------------------------------------------------------
248
+
249
+ export function classifyMatch(
250
+ snippet: string,
251
+ symbol: string,
252
+ language?: string,
253
+ ): "definition" | "reference" {
254
+ const escaped = escapeRegex(symbol);
255
+
256
+ if (language) {
257
+ const langPatterns = DEFINITION_PATTERNS.find((p) =>
258
+ p.languages.includes(language),
259
+ );
260
+ if (langPatterns) {
261
+ for (const pattern of langPatterns.patterns) {
262
+ const re = new RegExp(pattern.replace("SYMBOL", escaped));
263
+ if (re.test(snippet)) return "definition";
264
+ }
265
+ }
266
+ }
267
+
268
+ for (const pattern of GENERIC_DEFINITION_PATTERNS) {
269
+ const re = new RegExp(pattern.replace("SYMBOL", escaped));
270
+ if (re.test(snippet)) return "definition";
271
+ }
272
+
273
+ return "reference";
274
+ }
275
+
276
+ // ---------------------------------------------------------------------------
277
+ // Ranking
278
+ // ---------------------------------------------------------------------------
279
+
280
+ export function rankLocations(
281
+ locations: CodeNavLocation[],
282
+ context: {
283
+ sourceFilePath: string;
284
+ changedFiles: string[];
285
+ isTestFile: boolean;
286
+ },
287
+ cap = 50,
288
+ ): { definitions: CodeNavLocation[]; references: CodeNavLocation[]; capped: boolean } {
289
+ const capped = locations.length > cap;
290
+ const changedSet = new Set(context.changedFiles);
291
+
292
+ function score(loc: CodeNavLocation): number {
293
+ let s = 0;
294
+
295
+ if (loc.filePath === context.sourceFilePath) s += 1000;
296
+ else if (changedSet.has(loc.filePath)) s += 500;
297
+ else if (sameDirectory(loc.filePath, context.sourceFilePath)) s += 200;
298
+
299
+ if (isTestFile(loc.filePath) && !context.isTestFile) s -= 300;
300
+
301
+ if (loc.kind === "definition") s += 100;
302
+ if (loc.confidence === "likely") s += 50;
303
+
304
+ return s;
305
+ }
306
+
307
+ const sorted = [...locations].sort((a, b) => score(b) - score(a));
308
+ const truncated = sorted.slice(0, cap);
309
+
310
+ return {
311
+ definitions: truncated.filter((l) => l.kind === "definition"),
312
+ references: truncated.filter((l) => l.kind === "reference"),
313
+ capped,
314
+ };
315
+ }
316
+
317
+ // ---------------------------------------------------------------------------
318
+ // Changed files extraction from unified diff patch
319
+ // ---------------------------------------------------------------------------
320
+
321
+ export function extractChangedFiles(patch: string | null): string[] {
322
+ if (!patch) return [];
323
+ const set = new Set<string>();
324
+ const re = /^diff --git a\/(.+?) b\/(.+)$/gm;
325
+ let m: RegExpExecArray | null;
326
+ while ((m = re.exec(patch)) !== null) {
327
+ set.add(m[1]);
328
+ set.add(m[2]);
329
+ }
330
+ return [...set];
331
+ }
332
+
333
+ // ---------------------------------------------------------------------------
334
+ // Validation
335
+ // ---------------------------------------------------------------------------
336
+
337
+ export function validateCodeNavRequest(
338
+ body: unknown,
339
+ ): string | null {
340
+ if (!body || typeof body !== "object") return "Invalid request body";
341
+ const b = body as Record<string, unknown>;
342
+
343
+ if (typeof b.symbol !== "string" || !b.symbol.trim()) {
344
+ return "Missing or empty symbol";
345
+ }
346
+ if (typeof b.filePath !== "string" || !b.filePath.trim()) {
347
+ return "Missing filePath";
348
+ }
349
+ try {
350
+ validateFilePath(b.filePath as string);
351
+ } catch {
352
+ return "Invalid filePath";
353
+ }
354
+ if (b.side !== "old" && b.side !== "new") {
355
+ return "side must be 'old' or 'new'";
356
+ }
357
+
358
+ return null;
359
+ }
360
+
361
+ // ---------------------------------------------------------------------------
362
+ // Main entry point
363
+ // ---------------------------------------------------------------------------
364
+
365
+ let rgAvailable: boolean | null = null;
366
+
367
+ export async function resolveCodeNav(
368
+ runtime: CodeNavRuntime,
369
+ request: CodeNavRequest,
370
+ cwd: string,
371
+ changedFiles: string[],
372
+ ): Promise<CodeNavResponse> {
373
+ const start = Date.now();
374
+
375
+ if (rgAvailable === null) {
376
+ const check = await runtime.runCommand("rg", ["--version"], {
377
+ cwd,
378
+ timeoutMs: 2000,
379
+ });
380
+ rgAvailable = check.exitCode === 0;
381
+ }
382
+
383
+ if (!rgAvailable) {
384
+ return {
385
+ backend: "unavailable",
386
+ complete: true,
387
+ definitions: [],
388
+ references: [],
389
+ searchScope: "head",
390
+ stats: { elapsedMs: Date.now() - start, capped: false },
391
+ };
392
+ }
393
+
394
+ const args = buildRgArgs(request.symbol, request.language);
395
+
396
+ const result = await runtime.runCommand("rg", args, {
397
+ cwd,
398
+ timeoutMs: 5000,
399
+ });
400
+
401
+ // Exit code 1 = no matches (normal), exit code 2 = error
402
+ if (result.exitCode === 2) {
403
+ return {
404
+ backend: "search",
405
+ complete: true,
406
+ definitions: [],
407
+ references: [],
408
+ searchScope: "head",
409
+ stats: { elapsedMs: Date.now() - start, capped: false },
410
+ };
411
+ }
412
+
413
+ const locations = parseRgJsonOutput(
414
+ result.stdout,
415
+ request.symbol,
416
+ request.language,
417
+ );
418
+
419
+ const ranked = rankLocations(locations, {
420
+ sourceFilePath: request.filePath,
421
+ changedFiles,
422
+ isTestFile: isTestFile(request.filePath),
423
+ });
424
+
425
+ return {
426
+ backend: "search",
427
+ complete: true,
428
+ definitions: ranked.definitions,
429
+ references: ranked.references,
430
+ searchScope: "head",
431
+ stats: { elapsedMs: Date.now() - start, capped: ranked.capped },
432
+ };
433
+ }
434
+
435
+ export function resetRgCache(): void {
436
+ rgAvailable = null;
437
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@plannotator/pi-extension",
3
- "version": "0.19.15",
3
+ "version": "0.19.16",
4
4
  "type": "module",
5
5
  "description": "Plannotator Pi extension - interactive plan review with annotations, annotate agent messages, and review code/PRs",
6
6
  "author": "backnotprop",