pi-lens 3.6.6 → 3.6.7
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 +54 -0
- package/README.md +39 -2
- package/clients/dispatch/runners/similarity.ts +100 -1
- package/clients/installer/index.ts +26 -20
- package/clients/lsp/client.ts +249 -110
- package/clients/native-rust-client.ts +531 -0
- package/commands/booboo.ts +2 -2
- package/package.json +14 -2
- package/rust/Cargo.toml +34 -0
- package/rust/src/cache.rs +127 -0
- package/rust/src/index.rs +407 -0
- package/rust/src/lib.rs +209 -0
- package/rust/src/main.rs +24 -0
- package/rust/src/scan.rs +116 -0
- package/rust/src/similarity.rs +387 -0
- package/skills/ast-grep/SKILL.md +16 -4
|
@@ -0,0 +1,531 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Native Rust Core Client for pi-lens
|
|
3
|
+
*
|
|
4
|
+
* High-performance analysis via pi-lens-core binary:
|
|
5
|
+
* - Fast file scanning with gitignore support
|
|
6
|
+
* - State matrix similarity detection
|
|
7
|
+
* - Parallel project indexing
|
|
8
|
+
* - Tree-sitter query execution
|
|
9
|
+
*
|
|
10
|
+
* Communicates via JSON-RPC over stdin/stdout
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
14
|
+
import * as fs from "node:fs";
|
|
15
|
+
import * as path from "node:path";
|
|
16
|
+
import { fileURLToPath } from "node:url";
|
|
17
|
+
|
|
18
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
19
|
+
const __dirname = path.dirname(__filename);
|
|
20
|
+
|
|
21
|
+
/** Recursively collect all `.rs` files under a directory. */
|
|
22
|
+
async function collectRustSourceFiles(dir: string): Promise<string[]> {
|
|
23
|
+
let entries: fs.Dirent[];
|
|
24
|
+
try {
|
|
25
|
+
entries = await fs.promises.readdir(dir, { withFileTypes: true });
|
|
26
|
+
} catch {
|
|
27
|
+
return [];
|
|
28
|
+
}
|
|
29
|
+
const nested = await Promise.all(
|
|
30
|
+
entries.map(async (e) => {
|
|
31
|
+
const full = path.join(dir, e.name);
|
|
32
|
+
if (e.isDirectory()) return collectRustSourceFiles(full);
|
|
33
|
+
return e.name.endsWith(".rs") ? [full] : [];
|
|
34
|
+
}),
|
|
35
|
+
);
|
|
36
|
+
return nested.flat();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// --- Types matching Rust API ---
|
|
40
|
+
|
|
41
|
+
export interface ScanRequest {
|
|
42
|
+
command: "scan";
|
|
43
|
+
project_root: string;
|
|
44
|
+
extensions: string[];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface BuildIndexRequest {
|
|
48
|
+
command: "build_index";
|
|
49
|
+
project_root: string;
|
|
50
|
+
files: string[];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface SimilarityRequest {
|
|
54
|
+
command: "similarity";
|
|
55
|
+
project_root: string;
|
|
56
|
+
file_path: string;
|
|
57
|
+
threshold: number;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface QueryRequest {
|
|
61
|
+
command: "query";
|
|
62
|
+
project_root: string;
|
|
63
|
+
language: string;
|
|
64
|
+
query: string;
|
|
65
|
+
file_path: string;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export type AnalyzeRequest =
|
|
69
|
+
| ScanRequest
|
|
70
|
+
| BuildIndexRequest
|
|
71
|
+
| SimilarityRequest
|
|
72
|
+
| QueryRequest;
|
|
73
|
+
|
|
74
|
+
export interface FileEntry {
|
|
75
|
+
path: string;
|
|
76
|
+
size: number;
|
|
77
|
+
modified: number;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export interface FunctionEntry {
|
|
81
|
+
id: string;
|
|
82
|
+
file_path: string;
|
|
83
|
+
name: string;
|
|
84
|
+
line: number;
|
|
85
|
+
signature: string;
|
|
86
|
+
matrix_hash: string;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export interface IndexData {
|
|
90
|
+
entry_count: number;
|
|
91
|
+
functions: FunctionEntry[];
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export interface SimilarityMatch {
|
|
95
|
+
source_id: string;
|
|
96
|
+
target_id: string;
|
|
97
|
+
similarity: number;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export interface QueryMatch {
|
|
101
|
+
line: number;
|
|
102
|
+
column: number;
|
|
103
|
+
text: string;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export type ResponseData =
|
|
107
|
+
| { files: FileEntry[] }
|
|
108
|
+
| { index: IndexData }
|
|
109
|
+
| { similarities: SimilarityMatch[] }
|
|
110
|
+
| { query_results: QueryMatch[] }
|
|
111
|
+
| { empty: null };
|
|
112
|
+
|
|
113
|
+
export interface AnalyzeResponse {
|
|
114
|
+
success: boolean;
|
|
115
|
+
data: ResponseData;
|
|
116
|
+
error?: string;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// --- Client ---
|
|
120
|
+
|
|
121
|
+
export class NativeRustCoreClient {
|
|
122
|
+
private binaryPath: string | null = null;
|
|
123
|
+
private binaryAvailable: boolean | null = null;
|
|
124
|
+
private log: (msg: string) => void;
|
|
125
|
+
|
|
126
|
+
constructor(verbose = false) {
|
|
127
|
+
this.log = verbose
|
|
128
|
+
? (msg: string) => console.error(`[rust-core] ${msg}`)
|
|
129
|
+
: () => {};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Find the pi-lens-core binary
|
|
134
|
+
*/
|
|
135
|
+
private findBinary(): string | null {
|
|
136
|
+
if (this.binaryPath) return this.binaryPath;
|
|
137
|
+
|
|
138
|
+
// Possible locations (in order of preference)
|
|
139
|
+
const candidates = [
|
|
140
|
+
// Development: relative to this file
|
|
141
|
+
path.join(
|
|
142
|
+
__dirname,
|
|
143
|
+
"..",
|
|
144
|
+
"rust",
|
|
145
|
+
"target",
|
|
146
|
+
"release",
|
|
147
|
+
"pi-lens-core.exe",
|
|
148
|
+
),
|
|
149
|
+
path.join(__dirname, "..", "rust", "target", "release", "pi-lens-core"),
|
|
150
|
+
// Development: debug build
|
|
151
|
+
path.join(__dirname, "..", "rust", "target", "debug", "pi-lens-core.exe"),
|
|
152
|
+
path.join(__dirname, "..", "rust", "target", "debug", "pi-lens-core"),
|
|
153
|
+
// PATH
|
|
154
|
+
"pi-lens-core.exe",
|
|
155
|
+
"pi-lens-core",
|
|
156
|
+
];
|
|
157
|
+
|
|
158
|
+
for (const candidate of candidates) {
|
|
159
|
+
try {
|
|
160
|
+
if (candidate.includes("\\") || candidate.includes("/")) {
|
|
161
|
+
if (fs.existsSync(candidate)) {
|
|
162
|
+
this.binaryPath = candidate;
|
|
163
|
+
this.log(`Found binary: ${candidate}`);
|
|
164
|
+
return candidate;
|
|
165
|
+
}
|
|
166
|
+
} else {
|
|
167
|
+
// Try to spawn from PATH
|
|
168
|
+
const result = spawnSync(candidate, ["--version"], {
|
|
169
|
+
timeout: 3000,
|
|
170
|
+
encoding: "utf-8",
|
|
171
|
+
windowsHide: true,
|
|
172
|
+
});
|
|
173
|
+
if (!result.error && result.status === 0) {
|
|
174
|
+
this.binaryPath = candidate;
|
|
175
|
+
return candidate;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
} catch (err) {
|
|
179
|
+
void err;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Check if native core is available
|
|
188
|
+
*/
|
|
189
|
+
isAvailable(): boolean {
|
|
190
|
+
if (this.binaryAvailable !== null) return this.binaryAvailable;
|
|
191
|
+
this.binaryAvailable = this.findBinary() !== null;
|
|
192
|
+
if (this.binaryAvailable) {
|
|
193
|
+
this.log(`Native Rust core available: ${this.binaryPath}`);
|
|
194
|
+
} else {
|
|
195
|
+
this.log("Native Rust core not found");
|
|
196
|
+
}
|
|
197
|
+
return this.binaryAvailable;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Check if the binary is up-to-date relative to the Rust source files.
|
|
202
|
+
*
|
|
203
|
+
* Returns true when:
|
|
204
|
+
* - No binary exists (needs a build)
|
|
205
|
+
* - Binary mtime is older than any `.rs` or `Cargo.toml` source file
|
|
206
|
+
*
|
|
207
|
+
* Returns false when the binary is fresh (nothing to do).
|
|
208
|
+
*/
|
|
209
|
+
async isBinaryStale(): Promise<boolean> {
|
|
210
|
+
const rustDir = path.join(__dirname, "..", "rust");
|
|
211
|
+
if (!fs.existsSync(rustDir)) return false;
|
|
212
|
+
|
|
213
|
+
// Collect mtime of the binary (if it exists).
|
|
214
|
+
const binaryPath = this.findBinary();
|
|
215
|
+
let binaryMtime = 0;
|
|
216
|
+
if (binaryPath) {
|
|
217
|
+
try {
|
|
218
|
+
binaryMtime = (await fs.promises.stat(binaryPath)).mtimeMs;
|
|
219
|
+
} catch {
|
|
220
|
+
return true; // Can't stat the binary — treat as stale.
|
|
221
|
+
}
|
|
222
|
+
} else {
|
|
223
|
+
return true; // No binary at all.
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Walk rust/src/*.rs and Cargo.toml; find the newest mtime.
|
|
227
|
+
const sourceFiles = await collectRustSourceFiles(path.join(rustDir, "src"));
|
|
228
|
+
sourceFiles.push(path.join(rustDir, "Cargo.toml"));
|
|
229
|
+
|
|
230
|
+
for (const src of sourceFiles) {
|
|
231
|
+
try {
|
|
232
|
+
const { mtimeMs } = await fs.promises.stat(src);
|
|
233
|
+
if (mtimeMs > binaryMtime) {
|
|
234
|
+
this.log(`Stale: ${path.basename(src)} is newer than binary`);
|
|
235
|
+
return true;
|
|
236
|
+
}
|
|
237
|
+
} catch {
|
|
238
|
+
/* file vanished between readdir and stat — ignore */
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
return false;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Build the binary if in development mode
|
|
247
|
+
*/
|
|
248
|
+
async build(): Promise<boolean> {
|
|
249
|
+
const rustDir = path.join(__dirname, "..", "rust");
|
|
250
|
+
if (!fs.existsSync(rustDir)) {
|
|
251
|
+
this.log("No rust directory found");
|
|
252
|
+
return false;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Check if cargo is available via our workaround
|
|
256
|
+
const cargo = this.findCargo();
|
|
257
|
+
if (!cargo) {
|
|
258
|
+
this.log("Cargo not available for building");
|
|
259
|
+
return false;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
this.log("Building pi-lens-core...");
|
|
263
|
+
|
|
264
|
+
return new Promise((resolve) => {
|
|
265
|
+
const proc = spawn(cargo, ["build", "--release"], {
|
|
266
|
+
cwd: rustDir,
|
|
267
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
268
|
+
windowsHide: true,
|
|
269
|
+
env: {
|
|
270
|
+
...process.env,
|
|
271
|
+
PATH: `${process.env.PATH};${path.dirname(cargo)}`,
|
|
272
|
+
},
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
let output = "";
|
|
276
|
+
proc.stdout?.on("data", (data) => {
|
|
277
|
+
output += data.toString();
|
|
278
|
+
});
|
|
279
|
+
proc.stderr?.on("data", (data) => {
|
|
280
|
+
output += data.toString();
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
proc.on("close", (code) => {
|
|
284
|
+
if (code === 0) {
|
|
285
|
+
this.log("Build successful");
|
|
286
|
+
this.binaryPath = null; // Reset to find the new binary
|
|
287
|
+
this.binaryAvailable = null;
|
|
288
|
+
resolve(true);
|
|
289
|
+
} else {
|
|
290
|
+
this.log(`Build failed: ${output}`);
|
|
291
|
+
resolve(false);
|
|
292
|
+
}
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
proc.on("error", (err) => {
|
|
296
|
+
this.log(`Build error: ${err.message}`);
|
|
297
|
+
resolve(false);
|
|
298
|
+
});
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Find cargo executable (using workaround for rustup issues)
|
|
304
|
+
*/
|
|
305
|
+
private findCargo(): string | null {
|
|
306
|
+
const candidates = [
|
|
307
|
+
// Direct toolchain path (our workaround)
|
|
308
|
+
path.join(
|
|
309
|
+
process.env.HOME || "",
|
|
310
|
+
".rustup",
|
|
311
|
+
"toolchains",
|
|
312
|
+
"stable-x86_64-pc-windows-gnu",
|
|
313
|
+
"bin",
|
|
314
|
+
"cargo.exe",
|
|
315
|
+
),
|
|
316
|
+
path.join(
|
|
317
|
+
process.env.HOME || "",
|
|
318
|
+
".rustup",
|
|
319
|
+
"toolchains",
|
|
320
|
+
"stable-x86_64-pc-windows-gnu",
|
|
321
|
+
"bin",
|
|
322
|
+
"cargo",
|
|
323
|
+
),
|
|
324
|
+
// Standard cargo
|
|
325
|
+
path.join(process.env.USERPROFILE || "", ".cargo", "bin", "cargo.exe"),
|
|
326
|
+
path.join(process.env.HOME || "", ".cargo", "bin", "cargo"),
|
|
327
|
+
"cargo.exe",
|
|
328
|
+
"cargo",
|
|
329
|
+
];
|
|
330
|
+
|
|
331
|
+
for (const candidate of candidates) {
|
|
332
|
+
try {
|
|
333
|
+
if (candidate.includes("\\") || candidate.includes("/")) {
|
|
334
|
+
if (fs.existsSync(candidate)) {
|
|
335
|
+
return candidate;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
} catch {
|
|
339
|
+
// ignore
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
return null;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Send a request to the native core
|
|
348
|
+
*/
|
|
349
|
+
private async sendRequest(req: AnalyzeRequest): Promise<AnalyzeResponse> {
|
|
350
|
+
const binary = this.findBinary();
|
|
351
|
+
if (!binary) {
|
|
352
|
+
return {
|
|
353
|
+
success: false,
|
|
354
|
+
data: { empty: null },
|
|
355
|
+
error: "Native core binary not found",
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
return new Promise((resolve) => {
|
|
360
|
+
const proc = spawn(binary, [], {
|
|
361
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
362
|
+
windowsHide: true,
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
let stdout = "";
|
|
366
|
+
let stderr = "";
|
|
367
|
+
|
|
368
|
+
proc.stdout?.on("data", (data) => {
|
|
369
|
+
stdout += data.toString();
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
proc.stderr?.on("data", (data) => {
|
|
373
|
+
stderr += data.toString();
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
proc.on("close", (code) => {
|
|
377
|
+
if (code !== 0) {
|
|
378
|
+
this.log(`Process exited with code ${code}: ${stderr}`);
|
|
379
|
+
resolve({
|
|
380
|
+
success: false,
|
|
381
|
+
data: { empty: null },
|
|
382
|
+
error: `Process failed: ${stderr || "unknown error"}`,
|
|
383
|
+
});
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
try {
|
|
388
|
+
const response: AnalyzeResponse = JSON.parse(stdout);
|
|
389
|
+
resolve(response);
|
|
390
|
+
} catch (err) {
|
|
391
|
+
this.log(`Failed to parse response: ${err}`);
|
|
392
|
+
resolve({
|
|
393
|
+
success: false,
|
|
394
|
+
data: { empty: null },
|
|
395
|
+
error: `Invalid JSON response: ${err}`,
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
proc.on("error", (err) => {
|
|
401
|
+
this.log(`Process error: ${err.message}`);
|
|
402
|
+
resolve({
|
|
403
|
+
success: false,
|
|
404
|
+
data: { empty: null },
|
|
405
|
+
error: `Process error: ${err.message}`,
|
|
406
|
+
});
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
// Guard stdin against ERR_STREAM_DESTROYED / EPIPE if the process
|
|
410
|
+
// crashes between spawn and the write below.
|
|
411
|
+
proc.stdin?.on("error", (err: NodeJS.ErrnoException) => {
|
|
412
|
+
if (err.code === "ERR_STREAM_DESTROYED" || err.code === "EPIPE") return;
|
|
413
|
+
this.log(`stdin error: ${err.message}`);
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
// Send the request
|
|
417
|
+
proc.stdin?.write(JSON.stringify(req));
|
|
418
|
+
proc.stdin?.end();
|
|
419
|
+
});
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* Scan project for files
|
|
424
|
+
*/
|
|
425
|
+
async scanProject(
|
|
426
|
+
projectRoot: string,
|
|
427
|
+
extensions: string[],
|
|
428
|
+
): Promise<FileEntry[]> {
|
|
429
|
+
const req = {
|
|
430
|
+
command: {
|
|
431
|
+
scan: { extensions },
|
|
432
|
+
},
|
|
433
|
+
project_root: projectRoot,
|
|
434
|
+
};
|
|
435
|
+
|
|
436
|
+
const resp = await this.sendRequest(req as unknown as AnalyzeRequest);
|
|
437
|
+
if (!resp.success || !("files" in resp.data)) {
|
|
438
|
+
this.log(`Scan failed: ${resp.error}`);
|
|
439
|
+
return [];
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
return (resp.data as { files: FileEntry[] }).files;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
/**
|
|
446
|
+
* Build project index
|
|
447
|
+
*/
|
|
448
|
+
async buildIndex(
|
|
449
|
+
projectRoot: string,
|
|
450
|
+
files: string[],
|
|
451
|
+
): Promise<IndexData | null> {
|
|
452
|
+
const req = {
|
|
453
|
+
command: {
|
|
454
|
+
build_index: { files },
|
|
455
|
+
},
|
|
456
|
+
project_root: projectRoot,
|
|
457
|
+
};
|
|
458
|
+
|
|
459
|
+
const resp = await this.sendRequest(req as unknown as AnalyzeRequest);
|
|
460
|
+
if (!resp.success || !("index" in resp.data)) {
|
|
461
|
+
this.log(`Index build failed: ${resp.error}`);
|
|
462
|
+
return null;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
return (resp.data as { index: IndexData }).index;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
/**
|
|
469
|
+
* Find similar functions
|
|
470
|
+
*/
|
|
471
|
+
async findSimilarities(
|
|
472
|
+
projectRoot: string,
|
|
473
|
+
filePath: string,
|
|
474
|
+
threshold = 0.9,
|
|
475
|
+
): Promise<SimilarityMatch[]> {
|
|
476
|
+
const req = {
|
|
477
|
+
command: {
|
|
478
|
+
similarity: { file_path: filePath, threshold },
|
|
479
|
+
},
|
|
480
|
+
project_root: projectRoot,
|
|
481
|
+
};
|
|
482
|
+
|
|
483
|
+
const resp = await this.sendRequest(req as unknown as AnalyzeRequest);
|
|
484
|
+
if (!resp.success || !("similarities" in resp.data)) {
|
|
485
|
+
this.log(`Similarity check failed: ${resp.error}`);
|
|
486
|
+
return [];
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
return (resp.data as { similarities: SimilarityMatch[] }).similarities;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
/**
|
|
493
|
+
* Run tree-sitter query
|
|
494
|
+
*/
|
|
495
|
+
async runQuery(
|
|
496
|
+
projectRoot: string,
|
|
497
|
+
language: string,
|
|
498
|
+
query: string,
|
|
499
|
+
filePath: string,
|
|
500
|
+
): Promise<QueryMatch[]> {
|
|
501
|
+
const req = {
|
|
502
|
+
command: {
|
|
503
|
+
query: { language, query, file_path: filePath },
|
|
504
|
+
},
|
|
505
|
+
project_root: projectRoot,
|
|
506
|
+
};
|
|
507
|
+
|
|
508
|
+
const resp = await this.sendRequest(req as unknown as AnalyzeRequest);
|
|
509
|
+
if (!resp.success || !("query_results" in resp.data)) {
|
|
510
|
+
this.log(`Query failed: ${resp.error}`);
|
|
511
|
+
return [];
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
return (resp.data as { query_results: QueryMatch[] }).query_results;
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
// --- Singleton ---
|
|
519
|
+
|
|
520
|
+
let globalClient: NativeRustCoreClient | null = null;
|
|
521
|
+
|
|
522
|
+
export function getNativeRustCoreClient(verbose = false): NativeRustCoreClient {
|
|
523
|
+
if (!globalClient) {
|
|
524
|
+
globalClient = new NativeRustCoreClient(verbose);
|
|
525
|
+
}
|
|
526
|
+
return globalClient;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
export function resetNativeRustCoreClient(): void {
|
|
530
|
+
globalClient = null;
|
|
531
|
+
}
|
package/commands/booboo.ts
CHANGED
|
@@ -366,7 +366,7 @@ export async function handleBooboo(
|
|
|
366
366
|
});
|
|
367
367
|
|
|
368
368
|
let fullSection = `## Semantic Duplicates (Amain Algorithm)\n\n`;
|
|
369
|
-
fullSection += `**${topPairs.length} pair(s) with >
|
|
369
|
+
fullSection += `**${topPairs.length} pair(s) with >90% semantic similarity**\n\n`;
|
|
370
370
|
fullSection +=
|
|
371
371
|
"Functions with different names/variables but similar logic structures.\n\n";
|
|
372
372
|
|
|
@@ -1269,7 +1269,7 @@ function findTopSimilarPairs(
|
|
|
1269
1269
|
|
|
1270
1270
|
const similarity = calculateSimilarity(entry1.matrix, entry2.matrix);
|
|
1271
1271
|
|
|
1272
|
-
if (similarity >= 0.
|
|
1272
|
+
if (similarity >= 0.9) {
|
|
1273
1273
|
// Canonical pair key (sorted to avoid duplicates)
|
|
1274
1274
|
const pairKey = [entry1.id, entry2.id].sort().join("::");
|
|
1275
1275
|
if (seenPairs.has(pairKey)) continue;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-lens",
|
|
3
|
-
"version": "3.6.
|
|
3
|
+
"version": "3.6.7",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Real-time code feedback for pi — LSP, linters, formatters, type-checking, structural analysis & booboo",
|
|
6
6
|
"repository": {
|
|
@@ -12,7 +12,15 @@
|
|
|
12
12
|
"build": "tsc",
|
|
13
13
|
"watch": "tsc --watch",
|
|
14
14
|
"test": "vitest run",
|
|
15
|
-
"test:watch": "vitest"
|
|
15
|
+
"test:watch": "vitest",
|
|
16
|
+
"rust:fmt": "cargo fmt --manifest-path rust/Cargo.toml --all --check",
|
|
17
|
+
"rust:fmt:write": "cargo fmt --manifest-path rust/Cargo.toml --all",
|
|
18
|
+
"rust:check": "cargo check --manifest-path rust/Cargo.toml --all-targets --all-features",
|
|
19
|
+
"rust:lint": "cargo clippy --manifest-path rust/Cargo.toml --all-targets --all-features -- -D warnings",
|
|
20
|
+
"rust:test": "cargo test --manifest-path rust/Cargo.toml --all-targets --all-features",
|
|
21
|
+
"rust:build": "cargo build --manifest-path rust/Cargo.toml --release",
|
|
22
|
+
"rust:build:debug": "cargo build --manifest-path rust/Cargo.toml",
|
|
23
|
+
"rust:test:integration": "node rust/test_integration.mjs"
|
|
16
24
|
},
|
|
17
25
|
"keywords": [
|
|
18
26
|
"pi",
|
|
@@ -37,6 +45,8 @@
|
|
|
37
45
|
"commands/**/*.ts",
|
|
38
46
|
"rules/",
|
|
39
47
|
"skills/",
|
|
48
|
+
"rust/src/",
|
|
49
|
+
"rust/Cargo.toml",
|
|
40
50
|
"default-architect.yaml",
|
|
41
51
|
"tsconfig.json",
|
|
42
52
|
"README.md",
|
|
@@ -47,6 +57,7 @@
|
|
|
47
57
|
},
|
|
48
58
|
"dependencies": {
|
|
49
59
|
"@sinclair/typebox": "^0.34.0",
|
|
60
|
+
"cross-spawn": "^7.0.6",
|
|
50
61
|
"effect": "^3.21.0",
|
|
51
62
|
"vscode-jsonrpc": "^8.2.1"
|
|
52
63
|
},
|
|
@@ -57,6 +68,7 @@
|
|
|
57
68
|
"devDependencies": {
|
|
58
69
|
"@ast-grep/napi": "^0.42.0",
|
|
59
70
|
"@biomejs/biome": "^2.4.10",
|
|
71
|
+
"@types/cross-spawn": "^6.0.6",
|
|
60
72
|
"@types/node": "^22.10.5",
|
|
61
73
|
"js-yaml": "^4.1.1",
|
|
62
74
|
"typescript": "^5.0.0",
|
package/rust/Cargo.toml
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
[package]
|
|
2
|
+
name = "pi-lens-core"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
edition = "2024"
|
|
5
|
+
|
|
6
|
+
[lib]
|
|
7
|
+
name = "pi_lens_core"
|
|
8
|
+
path = "src/lib.rs"
|
|
9
|
+
|
|
10
|
+
[[bin]]
|
|
11
|
+
name = "pi-lens-core"
|
|
12
|
+
path = "src/main.rs"
|
|
13
|
+
|
|
14
|
+
[dependencies]
|
|
15
|
+
serde = { version = "1.0", features = ["derive"] }
|
|
16
|
+
serde_json = "1.0"
|
|
17
|
+
ignore = "0.4"
|
|
18
|
+
rayon = "1.8"
|
|
19
|
+
xxhash-rust = { version = "0.8", features = ["xxh3"] }
|
|
20
|
+
ndarray = "0.15"
|
|
21
|
+
tree-sitter = "0.22"
|
|
22
|
+
tree-sitter-typescript = "0.21"
|
|
23
|
+
tree-sitter-rust = "0.21"
|
|
24
|
+
walkdir = "2.5"
|
|
25
|
+
anyhow = "1.0"
|
|
26
|
+
|
|
27
|
+
[dev-dependencies]
|
|
28
|
+
tempfile = "3.10"
|
|
29
|
+
|
|
30
|
+
# Relaxed lints for development - can be tightened later
|
|
31
|
+
[lints.rust]
|
|
32
|
+
unsafe_code = "forbid"
|
|
33
|
+
|
|
34
|
+
[lints.clippy]
|