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 CHANGED
@@ -2,6 +2,60 @@
2
2
 
3
3
  All notable changes to pi-lens will be documented in this file.
4
4
 
5
+ ## [3.6.7] - 2026-04-04
6
+
7
+ ### Fixed
8
+ - **LSP `ERR_STREAM_DESTROYED` crash** — When an LSP process (e.g. rust-analyzer) exits, Node.js emits
9
+ `'error'` events on the destroyed stdio streams. Without listeners these became uncaught exceptions
10
+ that crashed the extension. Added persistent `error` listeners to `stdin`, `stdout`, and `stderr`
11
+ before handing them to `vscode-jsonrpc`, covering the post-`connection.dispose()` window.
12
+ Same guard added to `NativeRustCoreClient` stdin writes.
13
+
14
+ ### Added
15
+ - **Rust performance core (`pi-lens-core`)** — Optional Rust binary for CPU-intensive operations.
16
+ All features fall back to TypeScript automatically if the binary is not available (it is **not**
17
+ built automatically on `npm install` — run `npm run rust:build` once if you have Rust installed).
18
+ - **File scanning** — ripgrep’s `ignore` crate for `.gitignore`-aware project scanning
19
+ - **Similarity detection** — parallel 57×72 state-matrix index, persisted to
20
+ `.pi-lens/rust-index.json` between invocations (fixes in-memory cache that reset on every
21
+ process spawn)
22
+ - **Tree-sitter queries** — TypeScript and Rust AST queries via the binary
23
+ - **`NativeRustCoreClient`** — TypeScript wrapper with `isBinaryStale()` freshness detection,
24
+ JSON-IPC over stdin/stdout
25
+ - **Integration tests** — `npm run rust:test:integration` (37 assertions across all commands)
26
+
27
+ - **Rust similarity fast-path in dispatch runner** — `similarity.ts` now tries the Rust binary
28
+ first (scan → build index → query), falls through to the TypeScript implementation on any
29
+ failure. Feature flag `USE_RUST = true` at top of file.
30
+
31
+ ### Changed
32
+ - **Similarity threshold raised from 0.75 → 0.90** — Empirical evaluation showed that below 0.90
33
+ false positives (structurally similar but semantically unrelated functions) outnumber true
34
+ positives with the current 57×72 matrix resolution. Applies to both the dispatch runner and
35
+ `/lens-booboo`.
36
+
37
+ - **Rust `kind_id` mapping improved** — Replaced `kind % dim` modulo (caused up to 4 unrelated
38
+ node types to share one matrix slot) with even-distribution across named slots plus a dedicated
39
+ last slot for anonymous punctuation tokens. Max named-slot collisions reduced from 4 to 3;
40
+ unnamed tokens no longer pollute named slots.
41
+
42
+ ### Fixed (Rust)
43
+ - `tree_sitter_rust::language_rust()` → `language()` (correct API for tree-sitter-rust 0.21)
44
+ - `FunctionInfo` missing `#[derive(Clone)]` — caused compile error in `find_similar_to`
45
+ - `export function foo()` was missed by the index builder — TypeScript wraps exported functions
46
+ in `export_statement`; replaced flat top-level walk with recursive `collect_functions()`
47
+ - `find_similar_to` returned only the first function in a file — changed `find` to `filter`
48
+ - `tempfile` moved from `[dependencies]` to `[dev-dependencies]`
49
+ - Deleted orphan `test_lsp.rs` (intentional type errors caused rust-analyzer to crash the LSP stream)
50
+
51
+ ### Repository
52
+ - Rust source (`rust/src/`, `rust/Cargo.toml`) added to npm `files` whitelist so users can build
53
+ the binary from an npm-installed package
54
+ - Removed stale `src/main.rs` rule from root `.gitignore` (no such file at repo root)
55
+ - Untracked `docs/plans/2025-04-03-auto-install-logging.md` (committed before `*.md` exclusion rule)
56
+
57
+ ---
58
+
5
59
  ## [3.6.3] - 2026-04-03
6
60
 
7
61
  ### Removed (Dead Code Cleanup)
package/README.md CHANGED
@@ -196,7 +196,7 @@ pi-lens uses a **dispatcher-runner architecture** for extensible multi-language
196
196
  | **shellcheck** | Shell | 20 | Warning | Bash/sh/zsh/fish linting |
197
197
  | **python-slop** | Python | 25 | Warning | AI slop detection (~40 patterns) |
198
198
  | **spellcheck** | Markdown | 30 | Warning | Typo detection in docs |
199
- | **similarity** | TS | 35 | Warning | Semantic duplicate detection (structural similarity) |
199
+ | **similarity** | TS | 35 | Warning | Semantic duplicate detection (≥90% structural similarity, Rust-accelerated when available) |
200
200
  | **architect** | All | 40 | Warning | Architectural rule violations |
201
201
  | **go-vet** | Go | 50 | Warning | Go static analysis |
202
202
  | **rust-clippy** | Rust | 50 | Warning | Rust linting |
@@ -327,7 +327,7 @@ Full codebase analysis with **10 tracked runners** producing a comprehensive rep
327
327
  |---|--------|---------------|
328
328
  | 1 | **ast-grep (design smells)** | Structural issues (empty catch, no-debugger, etc.) |
329
329
  | 2 | **ast-grep (similar functions)** | Duplicate function patterns across files |
330
- | 3 | **semantic similarity (Amain)** | 57×72 matrix semantic clones (>75% similarity) |
330
+ | 3 | **semantic similarity (Amain)** | 57×72 matrix semantic clones (≥90% similarity) |
331
331
  | 4 | **complexity metrics** | Low MI, high cognitive complexity, AI slop indicators |
332
332
  | 5 | **TODO scanner** | TODO/FIXME annotations and tech debt markers |
333
333
  | 6 | **dead code (Knip)** | Unused exports, files, dependencies |
@@ -501,6 +501,9 @@ pi-lens/
501
501
  ├── commands/ # /lens-booboo, /lens-format commands
502
502
  ├── docs/ # Documentation
503
503
  ├── rules/ # AST-grep rules
504
+ ├── rust/ # Optional Rust core for performance acceleration
505
+ │ ├── src/ # Rust source (pi-lens-core binary)
506
+ │ └── Cargo.toml
504
507
  ├── skills/ # Built-in pi skills
505
508
  ├── index.ts # Main extension entry point
506
509
  └── package.json
@@ -510,6 +513,40 @@ See source for detailed structure.
510
513
 
511
514
  ---
512
515
 
516
+ ## Rust Core (Optional)
517
+
518
+ pi-lens includes a **Rust performance core** (`pi-lens-core`) for CPU-intensive operations. It is entirely optional — all features fall back to the TypeScript implementation automatically if the binary is not available.
519
+
520
+ **What it accelerates:**
521
+ - **File scanning** — Uses ripgrep's `ignore` crate for fast, `.gitignore`-aware project scanning (~10× faster than glob)
522
+ - **Similarity detection** — Parallel 57×72 state-matrix computation and index querying
523
+ - **Tree-sitter queries** — Runs TypeScript and Rust AST queries directly from the binary
524
+
525
+ **Status:** Does not work out of the box after `npm install`. The source is included in the package so you can build it yourself if you have Rust installed.
526
+
527
+ **Build the binary (one-time):**
528
+ ```bash
529
+ # Requires Rust toolchain — https://rustup.rs
530
+ npm run rust:build # release build (recommended)
531
+ npm run rust:build:debug # debug build
532
+ ```
533
+
534
+ Once built, pi-lens will automatically use the Rust binary and fall back to TypeScript if it is absent, outdated, or fails.
535
+
536
+ **Verify the binary is being used:**
537
+ ```bash
538
+ node -e "import('./clients/native-rust-client.js').then(m => console.log('available:', m.getNativeRustCoreClient(true).isAvailable()))"
539
+ ```
540
+
541
+ **Run integration tests** (requires debug binary):
542
+ ```bash
543
+ npm run rust:build:debug
544
+ npm run rust:test:integration # 37 assertions
545
+ npm run rust:test # Rust unit tests
546
+ ```
547
+
548
+ ---
549
+
513
550
  ## Skills
514
551
 
515
552
  pi-lens includes two built-in skills that guide the LLM on when to use specific tools:
@@ -9,6 +9,7 @@ import * as fs from "node:fs/promises";
9
9
  import * as path from "node:path";
10
10
  import * as ts from "typescript";
11
11
  import { EXCLUDED_DIRS } from "../../file-utils.js";
12
+ import { NativeRustCoreClient } from "../../native-rust-client.js";
12
13
  import {
13
14
  buildProjectIndex,
14
15
  findSimilarFunctions,
@@ -23,12 +24,18 @@ import type {
23
24
  RunnerResult,
24
25
  } from "../types.js";
25
26
 
27
+ // Singleton Rust client — initialised once, reused across runner invocations.
28
+ const rustClient = new NativeRustCoreClient();
29
+
30
+ /** Feature flag: set to false to force the pure-TypeScript path. */
31
+ const USE_RUST = true;
32
+
26
33
  // ============================================================================
27
34
  // Configuration
28
35
  // ============================================================================
29
36
 
30
37
  const CONFIG = {
31
- SIMILARITY_THRESHOLD: 0.75, // 75% minimum similarity
38
+ SIMILARITY_THRESHOLD: 0.9, // 90% minimum similarity — below this false positives dominate
32
39
  MIN_TRANSITIONS: 20, // Skip functions with <20 AST transitions
33
40
  MAX_SUGGESTIONS: 3, // Max 3 suggestions per file
34
41
  USAGE_THRESHOLD: 2, // Only suggest utilities with 2+ uses (placeholder)
@@ -64,6 +71,24 @@ const similarityRunner: RunnerDefinition = {
64
71
  return { status: "skipped", diagnostics: [], semantic: "none" };
65
72
  }
66
73
 
74
+ // ── Rust fast-path ─────────────────────────────────────────────────────
75
+ // Try Rust for file scanning + similarity detection. If the Rust binary
76
+ // is available, use it. On any failure, fall through to the pure-TS path.
77
+ if (USE_RUST && rustClient.isAvailable()) {
78
+ try {
79
+ const rustResult = await runWithRust(
80
+ filePath,
81
+ projectRoot,
82
+ CONFIG.SIMILARITY_THRESHOLD,
83
+ CONFIG.MAX_SUGGESTIONS,
84
+ );
85
+ if (rustResult !== null) return rustResult;
86
+ } catch {
87
+ // Fall through to TypeScript implementation.
88
+ }
89
+ }
90
+ // ── TypeScript fallback ─────────────────────────────────────────────────
91
+
67
92
  const index = await loadOrBuildIndex(projectRoot);
68
93
  if (!index || index.entries.size === 0) {
69
94
  return { status: "skipped", diagnostics: [], semantic: "none" };
@@ -261,6 +286,80 @@ function buildSuggestionMessage(
261
286
  return `Function '${func.name}' has ${similarityPct}% similarity to existing utility '${name}()' in ${location}. Consider reusing the existing utility.`;
262
287
  }
263
288
 
289
+ // ============================================================================
290
+ // Rust fast-path
291
+ // ============================================================================
292
+
293
+ /**
294
+ * Run similarity detection via the Rust binary.
295
+ *
296
+ * Flow:
297
+ * 1. Scan project files with Rust (respects .gitignore, much faster than glob).
298
+ * 2. Build the Rust index (persisted to .pi-lens/rust-index.json).
299
+ * 3. Query similarity for the current file.
300
+ * 4. Convert matches to Diagnostics.
301
+ *
302
+ * Returns `null` if the Rust path cannot produce results (no matches is still
303
+ * a valid result — returned as an empty-diagnostic RunnerResult).
304
+ */
305
+ async function runWithRust(
306
+ filePath: string,
307
+ projectRoot: string,
308
+ threshold: number,
309
+ maxSuggestions: number,
310
+ ): Promise<RunnerResult | null> {
311
+ // 1. Scan project files.
312
+ const scanned = await rustClient.scanProject(projectRoot, [".ts", ".tsx"]);
313
+ if (scanned.length === 0) return null;
314
+
315
+ const relativeFiles = scanned.map((e) =>
316
+ path.relative(projectRoot, e.path).replace(/\\/g, "/"),
317
+ );
318
+
319
+ // 2. Build index (saves to .pi-lens/rust-index.json).
320
+ await rustClient.buildIndex(projectRoot, relativeFiles);
321
+
322
+ // 3. Find similarities for the current file.
323
+ const matches = await rustClient.findSimilarities(
324
+ projectRoot,
325
+ filePath,
326
+ threshold,
327
+ );
328
+
329
+ if (matches.length === 0) {
330
+ return { status: "succeeded", diagnostics: [], semantic: "none" };
331
+ }
332
+
333
+ // 4. Convert to Diagnostics.
334
+ const diagnostics: Diagnostic[] = matches
335
+ .slice(0, maxSuggestions)
336
+ .map((m) => {
337
+ const similarityPct = Math.round(m.similarity * 100);
338
+ // source_id format: "path/to/file.ts::funcName@line"
339
+ const [srcFile, srcFunc] = m.source_id.split("::");
340
+ const [targetFile, targetFunc] = m.target_id.split("::");
341
+ const funcName = srcFunc?.split("@")[0] ?? "?";
342
+ const targetName = targetFunc?.split("@")[0] ?? "?";
343
+ void srcFile; // file is implicit (it's the current file)
344
+ return {
345
+ id: `similarity-rust-${m.source_id}-${m.target_id}`,
346
+ tool: "similarity",
347
+ filePath,
348
+ line: 1, // Rust gives us the function source_id; line resolution is TODO
349
+ column: 1,
350
+ message: `Function '${funcName}' has ${similarityPct}% similarity to '${targetName}()' in ${targetFile}. Consider reusing the existing utility.`,
351
+ severity: "warning" as const,
352
+ semantic: "warning" as const,
353
+ };
354
+ });
355
+
356
+ return {
357
+ status: "succeeded",
358
+ diagnostics,
359
+ semantic: diagnostics.length > 0 ? "warning" : "none",
360
+ };
361
+ }
362
+
264
363
  // ============================================================================
265
364
  // Index Management
266
365
  // ============================================================================
@@ -39,6 +39,19 @@ import path from "node:path";
39
39
  // Global installation directory for pi-lens tools
40
40
  const TOOLS_DIR = path.join(process.cwd(), ".pi-lens", "tools");
41
41
 
42
+ // Debug flag - set via PI_LENS_DEBUG=1 or --debug
43
+ const DEBUG =
44
+ process.env.PI_LENS_DEBUG === "1" || process.argv.includes("--debug");
45
+
46
+ /**
47
+ * Log debug messages only when DEBUG is enabled
48
+ */
49
+ function debugLog(...args: unknown[]): void {
50
+ if (DEBUG) {
51
+ console.error("[auto-install:debug]", ...args);
52
+ }
53
+ }
54
+
42
55
  // --- Tool Definitions ---
43
56
 
44
57
  interface ToolDefinition {
@@ -237,22 +250,18 @@ async function verifyToolBinary(binPath: string): Promise<boolean> {
237
250
 
238
251
  proc.on("exit", (code) => {
239
252
  if (code === 0) {
240
- console.error(
241
- `[auto-install] Verified: ${binPath} (version: ${stdout.trim()})`,
242
- );
253
+ debugLog(`Verified: ${binPath} (version: ${stdout.trim()})`);
243
254
  resolve(true);
244
255
  } else {
245
- console.error(
246
- `[auto-install] Verification failed for ${binPath}: exit code ${code}, stderr: ${stderr}`,
247
- );
256
+ console.error(`[auto-install] Verification failed for ${binPath}`);
257
+ debugLog("Exit code:", code, "stderr:", stderr);
248
258
  resolve(false);
249
259
  }
250
260
  });
251
261
 
252
262
  proc.on("error", (err) => {
253
- console.error(
254
- `[auto-install] Verification failed for ${binPath}: ${err.message}`,
255
- );
263
+ console.error(`[auto-install] Verification failed for ${binPath}`);
264
+ debugLog("Error:", err.message);
256
265
  resolve(false);
257
266
  });
258
267
  });
@@ -322,11 +331,11 @@ async function installNpmTool(
322
331
  }
323
332
 
324
333
  // NEW: Verify the binary actually works before returning
325
- console.error(`[auto-install] Verifying ${binaryName}...`);
334
+ debugLog(`Verifying ${binaryName}...`);
326
335
  const isValid = await verifyToolBinary(binPath);
327
336
  if (!isValid) {
328
337
  console.error(
329
- `[auto-install] ${packageName} installed but verification failed. The binary may be corrupted.`,
338
+ `[auto-install] ${packageName} installed but verification failed (binary may be corrupted)`,
330
339
  );
331
340
  // Clean up the broken installation
332
341
  try {
@@ -357,10 +366,8 @@ async function installNpmTool(
357
366
  proc.on("error", (err) => reject(err));
358
367
  });
359
368
  } catch (err) {
360
- console.error(
361
- `[auto-install] Failed to install npm tool ${packageName}:`,
362
- err,
363
- );
369
+ console.error(`[auto-install] Failed to install ${packageName}: ${(err as Error).message}`);
370
+ debugLog("Full error:", err);
364
371
  return undefined;
365
372
  }
366
373
  }
@@ -393,10 +400,8 @@ async function installPipTool(
393
400
  proc.on("error", (err) => reject(err));
394
401
  });
395
402
  } catch (err) {
396
- console.error(
397
- `[auto-install] Failed to install pip tool ${packageName}:`,
398
- err,
399
- );
403
+ console.error(`[auto-install] Failed to install ${packageName}: ${(err as Error).message}`);
404
+ debugLog("Full error:", err);
400
405
  return undefined;
401
406
  }
402
407
  }
@@ -434,7 +439,8 @@ export async function installTool(toolId: string): Promise<boolean> {
434
439
  return false;
435
440
  }
436
441
  } catch (err) {
437
- console.error(`[auto-install] Failed to install ${tool.name}:`, err);
442
+ console.error(`[auto-install] Failed to install ${tool.name}: ${(err as Error).message}`);
443
+ debugLog("Full error:", err);
438
444
  return false;
439
445
  }
440
446
  }