@plannotator/pi-extension 0.19.15 → 0.19.17
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/generated/code-nav.ts +437 -0
- package/package.json +1 -1
- package/plannotator.html +2 -2
- package/review-editor.html +143 -136
- package/server/serverReview.ts +81 -1
- package/skills/plannotator-setup-goal/SKILL.md +56 -60
- package/skills/plannotator-setup-goal/agents/openai.yaml +3 -3
- package/skills/plannotator-setup-goal/scripts/scaffold_goal.py +0 -219
|
@@ -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.
|
|
3
|
+
"version": "0.19.17",
|
|
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",
|