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.
@@ -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
+ }
@@ -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 >75% semantic similarity**\n\n`;
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.75) {
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.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",
@@ -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]