pi-lens 3.3.1 → 3.6.0

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.
Files changed (51) hide show
  1. package/CHANGELOG.md +83 -0
  2. package/README.md +175 -13
  3. package/clients/cache/rule-cache.js +72 -0
  4. package/clients/cache/rule-cache.ts +104 -0
  5. package/clients/dispatch/integration.js +48 -1
  6. package/clients/dispatch/integration.ts +60 -2
  7. package/clients/dispatch/plan.js +5 -2
  8. package/clients/dispatch/plan.ts +5 -2
  9. package/clients/dispatch/runners/ast-grep-napi.js +175 -56
  10. package/clients/dispatch/runners/ast-grep-napi.test.js +2 -1
  11. package/clients/dispatch/runners/ast-grep-napi.test.ts +2 -1
  12. package/clients/dispatch/runners/ast-grep-napi.ts +191 -79
  13. package/clients/dispatch/runners/similarity.js +1 -1
  14. package/clients/dispatch/runners/similarity.ts +2 -2
  15. package/clients/dispatch/runners/tree-sitter.js +137 -10
  16. package/clients/dispatch/runners/tree-sitter.ts +168 -13
  17. package/clients/dispatch/runners/ts-lsp.js +3 -2
  18. package/clients/dispatch/runners/ts-lsp.ts +3 -2
  19. package/clients/dispatch/runners/yaml-rule-parser.js +70 -2
  20. package/clients/dispatch/runners/yaml-rule-parser.ts +71 -2
  21. package/clients/dispatch/types.js +1 -1
  22. package/clients/dispatch/types.ts +1 -1
  23. package/clients/lsp/__tests__/service.test.js +3 -0
  24. package/clients/lsp/__tests__/service.test.ts +3 -0
  25. package/clients/lsp/client.js +42 -0
  26. package/clients/lsp/client.ts +79 -0
  27. package/clients/lsp/index.js +27 -0
  28. package/clients/lsp/index.ts +35 -0
  29. package/clients/metrics-client.js +3 -160
  30. package/clients/metrics-client.tdr.test.js +78 -0
  31. package/clients/metrics-client.test.js +30 -43
  32. package/clients/metrics-client.test.ts +30 -54
  33. package/clients/metrics-client.ts +5 -219
  34. package/clients/metrics-history.js +33 -7
  35. package/clients/metrics-history.ts +47 -10
  36. package/clients/pipeline.js +272 -0
  37. package/clients/pipeline.ts +371 -0
  38. package/clients/sg-runner.js +21 -3
  39. package/clients/sg-runner.ts +22 -3
  40. package/clients/tree-sitter-client.js +23 -2
  41. package/clients/tree-sitter-client.ts +27 -2
  42. package/index.ts +604 -771
  43. package/package.json +1 -1
  44. package/rules/ast-grep-rules/rules/no-architecture-violation.yml +7 -4
  45. package/rules/ast-grep-rules/rules/no-single-char-var.yml +3 -3
  46. package/rules/ast-grep-rules/slop-patterns.yml +85 -62
  47. package/skills/ast-grep/SKILL.md +42 -1
  48. package/skills/lsp-navigation/SKILL.md +62 -0
  49. package/tsconfig.json +1 -1
  50. package/rules/ast-grep-rules/rules/no-console-log.yml +0 -10
  51. package/rules/ast-grep-rules/rules/no-default-export.yml +0 -19
package/CHANGELOG.md CHANGED
@@ -2,6 +2,89 @@
2
2
 
3
3
  All notable changes to pi-lens will be documented in this file.
4
4
 
5
+ ## [3.6.0] - 2026-04-02
6
+
7
+ ### Added
8
+ - **LSP Call Hierarchy Support** — Added 3 new operations to `lsp_navigation` tool:
9
+ - `prepareCallHierarchy` — Get callable item at position
10
+ - `incomingCalls` — Find all functions/methods that CALL this function
11
+ - `outgoingCalls` — Find all functions/methods CALLED by this function
12
+ - Use case: "Who calls this function?" and "What does this function depend on?"
13
+ - **LSP Navigation Skill** — New built-in skill (`skills/lsp-navigation/SKILL.md`) that guides LLM on when to use LSP for code intelligence vs other tools
14
+ - **AST-Grep Skill Improvements** — Enhanced `skills/ast-grep/SKILL.md` with:
15
+ - Testing Tips section (Search → Dry-run → Apply workflow)
16
+ - Metavariable selection guide ($ vs $$$)
17
+ - Specific guidance for "Multiple AST nodes" error
18
+ - **Skills Registration** — Extension now registers `skills/` directory via `resources_discover` event, exposing both `ast-grep` and `lsp-navigation` skills to pi
19
+ - **Enhanced TDI (Technical Debt Index) with 5-factor formula** — Now captures "worst offender" functions and code unpredictability:
20
+ - **Max Cyclomatic (10%)**: Catches worst function complexity (avg hides bad apples)
21
+ - **Entropy (5%)**: Measures code unpredictability/vocabulary richness in bits
22
+ - Rebalanced weights: MI (45%), Cognitive (30%), Nesting (10%), MaxCyc (10%), Entropy (5%)
23
+ - New thresholds: MaxCyc >10 bad, >30 critical; Entropy >4.0 bits risky, >7.0 critical
24
+
25
+ ### Removed
26
+ - **TDR (Technical Debt Ratio)** — Removed orphaned metric tracking system:
27
+ - Deleted `TDREntry`, `TDRCategory` types, `tdrFindings` Map, `updateTDR()` method
28
+ - Removed `convertDiagnosticsToTDREntries()` helper and all `tdrCategory` assignments
29
+ - Deleted TDR test file
30
+ - TDI is sufficient for code health tracking; inline diagnostics provide immediate feedback
31
+
32
+ ### Changed
33
+ - **Updated `/lens-tdi` display** — Shows 5 category breakdown with descriptions:
34
+ ```
35
+ Debt breakdown:
36
+ Maintainability: 45% (MI-based)
37
+ Cognitive: 30%
38
+ Nesting: 10%
39
+ Max Cyclomatic: 10% (worst function)
40
+ Entropy: 5% (code unpredictability)
41
+ ```
42
+ - **Extended MetricSnapshot** — Added `maxCyclomatic` and `entropy` fields for historical tracking
43
+
44
+ ---
45
+
46
+ ## [3.5.0] - 2026-04-02
47
+
48
+ ### Added
49
+ - **Tree-sitter query compilation cache** — 10× performance improvement for structural analysis. Query files (`.yml`) are compiled to binary `.wasm-cache` format once and cached to disk. Subsequent loads use the compiled cache directly, reducing tree-sitter startup from ~50ms to ~5ms per query. Cache uses mtime-based invalidation — automatically recompiles when source `.yml` changes.
50
+ - **Rule cache infrastructure** (`clients/cache/`) — New disk-backed cache system with:
51
+ - `RuleCache` class for storing compiled artifacts
52
+ - mtime-based invalidation (auto-refresh when source files change)
53
+ - JSON metadata tracking for cache entries
54
+ - TTL and integrity validation
55
+
56
+ ### Fixed
57
+ - **YAML parser colon truncation** — Fixed regex-based parser that incorrectly truncated values containing colons. Changed from `split(':', 2)` to `indexOf(':')` for proper value extraction.
58
+ - **Tree-sitter rules directory resolution** — Fixed path resolution to use `ctx.cwd` instead of hardcoded `.pi-lens/rules/` path. Rules now load correctly from the actual project root regardless of where pi is invoked.
59
+ - **Tree-sitter post_filter support** — Implemented missing `post_filter` functionality for tree-sitter queries. Rules with post-filters (e.g., semantic validation for `bare-except` vs specific exception handlers) now work correctly instead of being silently skipped.
60
+ - **Event handler silent crashes** — Wrapped all event handlers in try/catch to prevent unhandled exceptions from crashing the extension silently. Errors are now logged to stderr instead of terminating the process.
61
+ - **Latency logging restored** — Fixed missing latency logging in `tool_result` handler. Runner timing data now correctly flows to `~/.pi-lens/latency.log` again.
62
+
63
+ ### Removed
64
+ - **Broken ast-grep rules** — Removed overlapping rules that were causing false positives or conflicts with tree-sitter coverage.
65
+
66
+ ---
67
+
68
+ ## [3.4.0] - 2026-04-02
69
+
70
+ ### Fixed
71
+ - **Delta mode was broken** — `dispatchLint()` created a fresh empty baseline store on every call, making delta filtering a complete no-op. Every issue looked "new" every time. Now uses a persistent session-level baseline store. First write captures baseline, subsequent writes only show NEW issues.
72
+ - **Duplicate type-checking with `--lens-lsp`** — Both the `lsp` runner (priority 4) and `ts-lsp` runner (priority 5) were calling the same LSP service for TypeScript files. `ts-lsp` now skips when `--lens-lsp` is active.
73
+
74
+ ### Added
75
+ - **Inline security rules via ast-grep-napi** — Re-enabled the ast-grep-napi runner for real-time blocking on security violations (`no-eval`, `jwt-no-verify`, `no-hardcoded-secrets`, `weak-rsa-key`, `no-open-redirect`, etc.). Only error-severity rules fire inline; warnings remain in `/lens-booboo`. Skips 5 rules already covered by tree-sitter to avoid duplicates. ~9ms execution time.
76
+ - **Pre-write duplicate detection (two layers):**
77
+ - **Exact name match** — Checks exported names in new content against the session’s cached export index. If a function/class/type already exists in another file, blocks the write: `🔴 STOP — function X already exists in utils.ts. Import instead.`
78
+ - **Structural similarity** — Parses new functions, builds AST state matrices, compares against the project index (built at session start). Functions with ≥80% structural similarity trigger a warning with the match location. Non-blocking.
79
+ - **Project similarity index at session start** — Builds 57×72 state matrices for all TS functions at session start (cached to `.pi-lens/index.json`). Makes pre-write similarity checks ~50ms instead of seconds.
80
+
81
+ ### Changed
82
+ - **Extracted post-write pipeline** — Moved the entire post-write pipeline (secrets, format, autofix, dispatch, tests, cascade diagnostics) from `index.ts` into `clients/pipeline.ts`. `index.ts` reduced from 1764 to 1439 lines.
83
+ - **Removed inline complexity warnings** — `⚠️ Complexity increased: +4 cognitive` no longer shown on every write. No agent acts on this mid-task. Complexity data still captured for `/lens-booboo` and `/lens-tdi`.
84
+ - **Simplified pre-write handler** — Removed pre-write TypeScript and LSP diagnostics checks (checked old content before write landed — post-write catches everything). Kept only complexity baseline capture and duplicate detection.
85
+
86
+ ---
87
+
5
88
  ## [3.3.1] - 2026-04-02
6
89
 
7
90
  ### Fixed
package/README.md CHANGED
@@ -202,12 +202,12 @@ pi-lens uses a **dispatcher-runner architecture** for extensible multi-language
202
202
  | **ruff** | Python | 10 | Warning | Python linting (delta-tracked) |
203
203
  | **oxlint** | TS/JS | 12 | Warning | Fast Rust-based JS/TS linter |
204
204
  | **tree-sitter** | TS/JS, Python | 14 | Mixed | AST-based structural analysis (21 patterns) — **singleton WASM client** |
205
- | **ast-grep-napi** | TS/JS | 15 | | **Disabled by default** heavy; use `/lens-booboo` for full analysis |
205
+ | **ast-grep-napi** | TS/JS | 15 | Blocking | Security rules inline (no-eval, jwt-no-verify, no-hardcoded-secrets, etc.) |
206
206
  | **type-safety** | TS | 20 | Mixed | Switch exhaustiveness (blocking), other (warning) |
207
207
  | **shellcheck** | Shell | 20 | Warning | Bash/sh/zsh/fish linting |
208
208
  | **python-slop** | Python | 25 | Warning | AI slop detection (~40 patterns) |
209
209
  | **spellcheck** | Markdown | 30 | Warning | Typo detection in docs |
210
- | **similarity** | TS | 35 | Silent | Semantic duplicate detection (metrics only) |
210
+ | **similarity** | TS | 35 | Warning | Semantic duplicate detection (structural similarity) |
211
211
  | **architect** | All | 40 | Warning | Architectural rule violations |
212
212
  | **go-vet** | Go | 50 | Warning | Go static analysis |
213
213
  | **rust-clippy** | Rust | 50 | Warning | Rust linting |
@@ -224,7 +224,7 @@ pi-lens uses a **dispatcher-runner architecture** for extensible multi-language
224
224
  - **Warning** — Shown in `/lens-booboo`, not inline (noise reduction)
225
225
  - **Silent** — Tracked in metrics only, never shown
226
226
 
227
- **Consolidated runners:** `ts-slop` merged into `ast-grep-napi` (disabled by default) — CLI ast-grep used for full linter only
227
+ **Consolidated runners:** `ts-slop` merged into `ast-grep-napi` — CLI ast-grep used for full linter via `/lens-booboo`
228
228
 
229
229
  **Tree-sitter runner patterns** (priority 14, AST-based structural analysis):
230
230
 
@@ -244,7 +244,7 @@ Python (6 patterns):
244
244
 
245
245
  **AI Slop Detection:**
246
246
  - `python-slop` runner (priority 25): ~40 patterns for Python code quality
247
- - `ast-grep-napi` runner (priority 15): 33 slop patterns + 68 security/architecture rules for TypeScript/JavaScript (disabled by default — use `/lens-booboo` for full ast-grep analysis via CLI)
247
+ - `ast-grep-napi` runner (priority 15): Security rules fire inline (blocking); slop/architecture warnings via `/lens-booboo` only. Skips 5 rules already covered by tree-sitter.
248
248
 
249
249
  ---
250
250
 
@@ -402,13 +402,112 @@ pi-lens calculates comprehensive code quality metrics for every source file:
402
402
  | **Cyclomatic Complexity** | 1+ | Independent code paths (branch points + 1) | >10: 🟡 Complex function, >20: 🔴 Highly complex |
403
403
  | **Max Cyclomatic** | 1+ | Worst function in file | >10 flagged |
404
404
  | **Nesting Depth** | 0+ | Maximum block nesting level | >4: 🟡 Deep nesting, >6: 🔴 Excessive |
405
- | **Code Entropy** | 0-8+ bits | Shannon entropy — unpredictability of code patterns | >3.5: 🟡 Risky AI-induced complexity |
405
+ | **Code Entropy** | 0-8+ bits | Shannon entropy — unpredictability of code patterns | >4.0: 🟡 Risky, >7.0: 🔴 Very unpredictable |
406
406
  | **Halstead Volume** | 0+ | Vocabulary × length — unique ops/operands | High = many different operations |
407
407
 
408
- **AI Slop Indicators:**
409
- - Low MI + high cognitive complexity + high entropy = potential AI-generated spaghetti code
410
- - Excessive comments (>40%) + low MI = hand-holding anti-patterns
411
- - Single-use helpers with high entropy = over-abstraction
408
+ #### Metric Calculations
409
+
410
+ **Maintainability Index (Microsoft's Formula)**
411
+ ```
412
+ MI = max(0, (171 - 5.2*ln(Halstead) - 0.23*Cyclomatic - 16.2*ln(LOC)) * 100/171) + commentBonus
413
+
414
+ Where:
415
+ - Halstead = Halstead Volume (vocabulary-based complexity)
416
+ - Cyclomatic = Average cyclomatic complexity across functions
417
+ - LOC = Lines of code
418
+ - commentBonus = up to +10% for well-commented code
419
+ ```
420
+
421
+ **Cognitive Complexity (SonarSource Spec)**
422
+ - +1 for each structural node (if, for, while, case, catch, switch)
423
+ - +1 for each level of nesting (nested structures add nesting penalty)
424
+ - +1 for each && and || in binary expressions (logical operators)
425
+ - Exception: else-if chains don't add nesting
426
+
427
+ **Cyclomatic Complexity (McCabe's Formula)**
428
+ ```
429
+ M = E - N + 2P
430
+
431
+ Where:
432
+ - E = Number of edges in control flow graph
433
+ - N = Number of nodes
434
+ - P = Number of connected components (usually 1)
435
+
436
+ Simplified: Count branch points (if, while, for, case, &&, ||) + 1
437
+ ```
438
+
439
+ **Max Cyclomatic**
440
+ - Single value: the highest cyclomatic complexity of any function in the file
441
+ - Catches "worst offender" functions that average metrics hide
442
+
443
+ **Code Entropy (Shannon Entropy in Bits)**
444
+ ```
445
+ H = -Σ(p(i) * log2(p(i)))
446
+
447
+ Where:
448
+ - p(i) = frequency of token i / total tokens
449
+ - Measures unpredictability/vocabulary richness
450
+
451
+ Thresholds:
452
+ - ≤4.0 bits: Predictable, conventional code ✅
453
+ - 4.0-7.0 bits: Moderate complexity 🟡
454
+ - ≥7.0 bits: Unpredictable, hard to maintain 🔴
455
+ ```
456
+
457
+ **Halstead Volume**
458
+ ```
459
+ V = N * log2(n)
460
+
461
+ Where:
462
+ - N = total operators + operands (program length)
463
+ - n = unique operators + operands (vocabulary size)
464
+
465
+ Measures: How much information the reader must absorb
466
+ ```
467
+
468
+ ---
469
+
470
+ ### Technical Debt Index (TDI)
471
+
472
+ The TDI provides a single score (0-100) representing overall codebase health. Lower is better.
473
+
474
+ **TDI Formula (5-Factor Weighted)**
475
+ ```
476
+ TDI = MI-debt(45%) + cognitive(30%) + nesting(10%) + maxCyc(10%) + entropy(5%)
477
+ ```
478
+
479
+ **Debt Calculation for Each Factor:**
480
+
481
+ | Factor | Debt Formula | Good | Bad |
482
+ |--------|---------------|------|-----|
483
+ | **MI** | `(100 - MI) / 100` | 100 | 0 |
484
+ | **Cognitive** | `min(1, cognitive / 200)` | 0 | ≥500 |
485
+ | **Nesting** | `max(0, nesting - 3) / 7` | ≤3 | ≥10 |
486
+ | **Max Cyclomatic** | `max(0, maxCyc - 10) / 20` | ≤10 | ≥30 |
487
+ | **Entropy** | `max(0, entropy - 4.0) / 3.0` | ≤4.0 | ≥7.0 |
488
+
489
+ **Grades:**
490
+ - **A** (0-15%): Excellent codebase health
491
+ - **B** (16-30%): Good, minor improvements possible
492
+ - **C** (31-50%): Moderate debt, consider refactoring
493
+ - **D** (51-70%): Significant debt, plan refactoring
494
+ - **F** (71%+): High debt, immediate attention needed
495
+
496
+ **Usage:**
497
+ ```bash
498
+ /lens-tdi # Display TDI score with breakdown by category
499
+ ```
500
+
501
+ ---
502
+
503
+ ### AI Slop Indicators
504
+
505
+ Metrics that suggest potentially AI-generated low-quality code:
506
+
507
+ - **Low MI + high cognitive + high entropy** = potential spaghetti code
508
+ - **Excessive comments (>40%) + low MI** = hand-holding anti-patterns
509
+ - **Single-use helpers with high entropy** = over-abstraction
510
+ - **Many small functions with high cyclomatic** = fragmented complexity
412
511
 
413
512
  **Usage:**
414
513
  - `/lens-booboo` — Shows complexity table for all files
@@ -476,7 +575,6 @@ pi-lens works out of the box for TypeScript/JavaScript. For full language suppor
476
575
  |------|---------|--------------|
477
576
  | **Standard** (default) | `pi` | Auto-formatting, TS/Python type-checking, sequential execution |
478
577
  | **Full LSP** | `pi --lens-lsp` | Real LSP servers (31 languages), sequential execution |
479
- | **Fastest** | `pi --lens-lsp` | Real LSP + full runner suite |
480
578
 
481
579
 
482
580
  ### Flag Reference
@@ -502,7 +600,6 @@ pi-lens works out of the box for TypeScript/JavaScript. For full language suppor
502
600
  ```bash
503
601
  pi # Default: auto-format, auto-fix, built-in type-checking
504
602
  pi --lens-lsp # LSP type-checking (31 languages)
505
- pi --lens-lsp # LSP mode (recommended)
506
603
  ```
507
604
 
508
605
  ---
@@ -639,7 +736,8 @@ Tracks which files were edited in the current agent turn for:
639
736
 
640
737
  | Runner | Cache | Notes |
641
738
  |--------|-------|-------|
642
- | `ast-grep-napi` | Rule descriptions | Loaded once per session (disabled by default) |
739
+ | `tree-sitter` | Compiled query cache | `.wasm-cache` files with mtime-based invalidation. 10× faster startup. |
740
+ | `ast-grep-napi` | Rule descriptions | Loaded once per session |
643
741
  | `biome` | Tool availability | Checked once, cached |
644
742
  | `pyright` | Command path | Venv lookup cached |
645
743
  | `ruff` | Command path | Venv lookup cached |
@@ -655,10 +753,12 @@ pi-lens/
655
753
  │ │ ├── bus.ts
656
754
  │ │ ├── events.ts
657
755
  │ │ └── integration.ts
756
+ │ ├── cache/ # Rule compilation cache
757
+ │ │ └── rule-cache.ts # Disk-backed cache with mtime invalidation
658
758
  │ ├── dispatch/ # Dispatcher and runners
659
759
  │ │ ├── dispatcher.ts
660
760
  │ │ └── runners/ # Individual runners
661
- │ │ ├── ast-grep-napi.ts # Fast TS/JS runner (disabled by default)
761
+ │ │ ├── ast-grep-napi.ts # Security rules inline, warnings in booboo
662
762
  │ │ ├── python-slop.ts # Python slop detection
663
763
  │ │ ├── ts-lsp.ts # TS type checking
664
764
  │ │ ├── biome.ts
@@ -693,12 +793,74 @@ pi-lens/
693
793
 
694
794
  ---
695
795
 
796
+ ## Skills
797
+
798
+ pi-lens includes two built-in skills that guide the LLM on when to use specific tools:
799
+
800
+ ### ast-grep
801
+
802
+ **Purpose:** Guide AST-aware pattern matching for semantic code search/replace.
803
+
804
+ **When to load:** Use `/skill:ast-grep` when performing structural code searches (finding function calls, class methods, imports) or replacements across files.
805
+
806
+ **Key guidance:**
807
+ - Use `$VAR` for single nodes, `$$$` for multiple
808
+ - Patterns must be **complete valid code** (not fragments)
809
+ - **Workflow:** Search → Dry-run (`apply: false`) → Apply (`apply: true`)
810
+ - **Error "Multiple AST nodes":** Use metavariables like `it($TEST)` not raw text like `it"test"`
811
+
812
+ ```typescript
813
+ // ✅ GOOD: Complete code with metavariables
814
+ ast_grep_search
815
+ pattern: "console.log($MSG)"
816
+ lang: typescript
817
+ paths: ["src/"]
818
+
819
+ // ❌ BAD: Incomplete pattern
820
+ pattern: "console.log(" // Missing args/body
821
+ ```
822
+
823
+ ### lsp-navigation
824
+
825
+ **Purpose:** Guide code intelligence via Language Server Protocol.
826
+
827
+ **When to load:** Use `/skill:lsp-navigation` for understanding code structure — definitions, references, types, call hierarchy.
828
+
829
+ **Key guidance:**
830
+ - **LSP is PRIMARY** for code intelligence — NOT grep/glob/ast-grep
831
+ - Requires `--lens-lsp` flag
832
+ - Call hierarchy: `prepareCallHierarchy` → `incomingCalls`/`outgoingCalls`
833
+
834
+ | Task | Use LSP | Use Other |
835
+ |------|---------|-----------|
836
+ | "Where is this defined?" | `definition` | — |
837
+ | "Find all usages" | `references` | — |
838
+ | "What type is this?" | `hover` | — |
839
+ | "Who calls this function?" | `prepareCallHierarchy` → `incomingCalls` | — |
840
+ | Find patterns (console.log) | — | `ast_grep_search` |
841
+ | Find TODO comments | — | `grep` |
842
+
843
+ ```typescript
844
+ // ✅ Code intelligence → LSP
845
+ lsp_navigation
846
+ operation: "references"
847
+ filePath: "src/utils.ts"
848
+ line: 42
849
+ character: 10
850
+
851
+ // ❌ Don't use LSP for text patterns
852
+ pattern: "TODO" // Use grep instead
853
+ ```
854
+
855
+ ---
856
+
696
857
  ## Changelog
697
858
 
698
859
  See [CHANGELOG.md](CHANGELOG.md) for full history.
699
860
 
700
861
  ### Latest Highlights
701
862
 
863
+ - **Tree-sitter Query Cache:** Compiled query cache with mtime-based invalidation — 10× faster structural analysis startup
702
864
  - **LSP Support:** 31 Language Server Protocol clients (4 core auto-installed, others via npx or manual)
703
865
  - **NAPI Runner:** 100x faster TypeScript/JavaScript structural analysis (~9ms vs ~1200ms) — currently disabled in realtime due to stability
704
866
  - **Slop Detection:** 33+ TypeScript and 40+ Python patterns for AI-generated code quality issues
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Rule Cache for pi-lens
3
+ *
4
+ * Provides disk-based caching for parsed tree-sitter rules with
5
+ * automatic invalidation based on rule file modification times.
6
+ */
7
+ import * as crypto from "node:crypto";
8
+ import * as fs from "node:fs";
9
+ import * as path from "node:path";
10
+ const CACHE_DIR = path.join(process.cwd(), ".pi-lens", "cache");
11
+ const CACHE_VERSION = "v1";
12
+ export class RuleCache {
13
+ constructor(language) {
14
+ this.cacheFile = path.join(CACHE_DIR, `${language}-rules-${CACHE_VERSION}.json`);
15
+ }
16
+ ensureCacheDir() {
17
+ if (!fs.existsSync(CACHE_DIR)) {
18
+ fs.mkdirSync(CACHE_DIR, { recursive: true });
19
+ }
20
+ }
21
+ computeRuleHash(ruleFiles) {
22
+ const hash = crypto.createHash("sha256");
23
+ for (const file of ruleFiles.sort()) {
24
+ if (fs.existsSync(file)) {
25
+ const stat = fs.statSync(file);
26
+ hash.update(`${file}:${stat.mtimeMs}:${stat.size}`);
27
+ }
28
+ }
29
+ return hash.digest("hex").slice(0, 16);
30
+ }
31
+ get(ruleFiles) {
32
+ try {
33
+ this.ensureCacheDir();
34
+ if (!fs.existsSync(this.cacheFile))
35
+ return null;
36
+ const cached = JSON.parse(fs.readFileSync(this.cacheFile, "utf-8"));
37
+ const currentHash = this.computeRuleHash(ruleFiles);
38
+ if (cached.version !== CACHE_VERSION || cached.ruleHash !== currentHash) {
39
+ return null; // Cache invalid
40
+ }
41
+ return cached;
42
+ }
43
+ catch {
44
+ return null;
45
+ }
46
+ }
47
+ set(ruleFiles, queries) {
48
+ try {
49
+ this.ensureCacheDir();
50
+ const entry = {
51
+ version: CACHE_VERSION,
52
+ timestamp: Date.now(),
53
+ ruleHash: this.computeRuleHash(ruleFiles),
54
+ queries,
55
+ };
56
+ fs.writeFileSync(this.cacheFile, JSON.stringify(entry, null, 2));
57
+ }
58
+ catch {
59
+ // Cache write failure is non-fatal
60
+ }
61
+ }
62
+ clear() {
63
+ try {
64
+ if (fs.existsSync(this.cacheFile)) {
65
+ fs.unlinkSync(this.cacheFile);
66
+ }
67
+ }
68
+ catch {
69
+ // Ignore
70
+ }
71
+ }
72
+ }
@@ -0,0 +1,104 @@
1
+ /**
2
+ * Rule Cache for pi-lens
3
+ *
4
+ * Provides disk-based caching for parsed tree-sitter rules with
5
+ * automatic invalidation based on rule file modification times.
6
+ */
7
+
8
+ import * as crypto from "node:crypto";
9
+ import * as fs from "node:fs";
10
+ import * as path from "node:path";
11
+
12
+ const CACHE_DIR = path.join(process.cwd(), ".pi-lens", "cache");
13
+ const CACHE_VERSION = "v1";
14
+
15
+ export interface QueryCacheEntry {
16
+ version: string;
17
+ timestamp: number;
18
+ ruleHash: string;
19
+ queries: Array<{
20
+ id: string;
21
+ name: string;
22
+ severity: string;
23
+ language: string;
24
+ message: string;
25
+ query: string;
26
+ metavars: string[];
27
+ post_filter?: string;
28
+ // biome-ignore lint/suspicious/noExplicitAny: Flexible filter params
29
+ post_filter_params?: Record<string, any>;
30
+ }>;
31
+ }
32
+
33
+ export class RuleCache {
34
+ private cacheFile: string;
35
+
36
+ constructor(language: string) {
37
+ this.cacheFile = path.join(
38
+ CACHE_DIR,
39
+ `${language}-rules-${CACHE_VERSION}.json`,
40
+ );
41
+ }
42
+
43
+ private ensureCacheDir(): void {
44
+ if (!fs.existsSync(CACHE_DIR)) {
45
+ fs.mkdirSync(CACHE_DIR, { recursive: true });
46
+ }
47
+ }
48
+
49
+ private computeRuleHash(ruleFiles: string[]): string {
50
+ const hash = crypto.createHash("sha256");
51
+ for (const file of ruleFiles.sort()) {
52
+ if (fs.existsSync(file)) {
53
+ const stat = fs.statSync(file);
54
+ hash.update(`${file}:${stat.mtimeMs}:${stat.size}`);
55
+ }
56
+ }
57
+ return hash.digest("hex").slice(0, 16);
58
+ }
59
+
60
+ get(ruleFiles: string[]): QueryCacheEntry | null {
61
+ try {
62
+ this.ensureCacheDir();
63
+ if (!fs.existsSync(this.cacheFile)) return null;
64
+
65
+ const cached = JSON.parse(
66
+ fs.readFileSync(this.cacheFile, "utf-8"),
67
+ ) as QueryCacheEntry;
68
+ const currentHash = this.computeRuleHash(ruleFiles);
69
+
70
+ if (cached.version !== CACHE_VERSION || cached.ruleHash !== currentHash) {
71
+ return null; // Cache invalid
72
+ }
73
+
74
+ return cached;
75
+ } catch {
76
+ return null;
77
+ }
78
+ }
79
+
80
+ set(ruleFiles: string[], queries: QueryCacheEntry["queries"]): void {
81
+ try {
82
+ this.ensureCacheDir();
83
+ const entry: QueryCacheEntry = {
84
+ version: CACHE_VERSION,
85
+ timestamp: Date.now(),
86
+ ruleHash: this.computeRuleHash(ruleFiles),
87
+ queries,
88
+ };
89
+ fs.writeFileSync(this.cacheFile, JSON.stringify(entry, null, 2));
90
+ } catch {
91
+ // Cache write failure is non-fatal
92
+ }
93
+ }
94
+
95
+ clear(): void {
96
+ try {
97
+ if (fs.existsSync(this.cacheFile)) {
98
+ fs.unlinkSync(this.cacheFile);
99
+ }
100
+ } catch {
101
+ // Ignore
102
+ }
103
+ }
104
+ }
@@ -10,6 +10,19 @@ import { clearLatencyReports, createBaselineStore, createDispatchContext, format
10
10
  export { clearLatencyReports, formatLatencyReport, getLatencyReports };
11
11
  // Import runners to register them
12
12
  import "./runners/index.js";
13
+ // --- Persistent Baseline Store ---
14
+ // Survives across dispatchLint calls within a session.
15
+ // Without this, delta mode is a no-op: every call creates a fresh empty
16
+ // store, so baselines.get() always returns undefined and every issue
17
+ // looks "new" every time.
18
+ const sessionBaselines = createBaselineStore();
19
+ /**
20
+ * Reset baselines — call on session_start so a new session
21
+ * starts with a clean slate.
22
+ */
23
+ export function resetDispatchBaselines() {
24
+ sessionBaselines.clear();
25
+ }
13
26
  /**
14
27
  * Run linting for a file using the declarative dispatch system
15
28
  *
@@ -20,7 +33,9 @@ import "./runners/index.js";
20
33
  */
21
34
  export async function dispatchLint(filePath, cwd, pi) {
22
35
  // By default, only run BLOCKING rules for fast feedback on file write
23
- const ctx = createDispatchContext(filePath, cwd, pi, undefined, true);
36
+ // Uses persistent sessionBaselines so delta mode actually filters
37
+ // pre-existing issues after the first write.
38
+ const ctx = createDispatchContext(filePath, cwd, pi, sessionBaselines, true);
24
39
  // Import dispatchForFile dynamically to avoid circular deps
25
40
  const { dispatchForFile } = await import("./dispatcher.js");
26
41
  const { getRunnersForKind } = await import("./dispatcher.js");
@@ -34,6 +49,38 @@ export async function dispatchLint(filePath, cwd, pi) {
34
49
  const result = await dispatchForFile(ctx, plan.groups);
35
50
  return result.output;
36
51
  }
52
+ /**
53
+ * Run linting and return full result (including diagnostics)
54
+ */
55
+ export async function dispatchLintWithResult(filePath, cwd, pi) {
56
+ const ctx = createDispatchContext(filePath, cwd, pi, sessionBaselines, true);
57
+ const { dispatchForFile } = await import("./dispatcher.js");
58
+ const { getRunnersForKind } = await import("./dispatcher.js");
59
+ const { TOOL_PLANS } = await import("./plan.js");
60
+ const kind = ctx.kind;
61
+ if (!kind) {
62
+ return {
63
+ diagnostics: [],
64
+ blockers: [],
65
+ warnings: [],
66
+ fixed: [],
67
+ output: "",
68
+ hasBlockers: false,
69
+ };
70
+ }
71
+ const plan = TOOL_PLANS[kind];
72
+ if (!plan) {
73
+ return {
74
+ diagnostics: [],
75
+ blockers: [],
76
+ warnings: [],
77
+ fixed: [],
78
+ output: "",
79
+ hasBlockers: false,
80
+ };
81
+ }
82
+ return await dispatchForFile(ctx, plan.groups);
83
+ }
37
84
  /**
38
85
  * Create a baseline store for delta mode tracking
39
86
  */
@@ -15,7 +15,7 @@ import {
15
15
  getLatencyReports,
16
16
  type RunnerLatency,
17
17
  } from "./dispatcher.js";
18
- import type { PiAgentAPI } from "./types.js";
18
+ import type { BaselineStore, DispatchResult, PiAgentAPI } from "./types.js";
19
19
 
20
20
  export type { DispatchLatencyReport, RunnerLatency };
21
21
  // Re-export latency tracking types and functions
@@ -24,6 +24,21 @@ export { clearLatencyReports, formatLatencyReport, getLatencyReports };
24
24
  // Import runners to register them
25
25
  import "./runners/index.js";
26
26
 
27
+ // --- Persistent Baseline Store ---
28
+ // Survives across dispatchLint calls within a session.
29
+ // Without this, delta mode is a no-op: every call creates a fresh empty
30
+ // store, so baselines.get() always returns undefined and every issue
31
+ // looks "new" every time.
32
+ const sessionBaselines: BaselineStore = createBaselineStore();
33
+
34
+ /**
35
+ * Reset baselines — call on session_start so a new session
36
+ * starts with a clean slate.
37
+ */
38
+ export function resetDispatchBaselines(): void {
39
+ sessionBaselines.clear();
40
+ }
41
+
27
42
  /**
28
43
  * Run linting for a file using the declarative dispatch system
29
44
  *
@@ -38,7 +53,9 @@ export async function dispatchLint(
38
53
  pi: PiAgentAPI,
39
54
  ): Promise<string> {
40
55
  // By default, only run BLOCKING rules for fast feedback on file write
41
- const ctx = createDispatchContext(filePath, cwd, pi, undefined, true);
56
+ // Uses persistent sessionBaselines so delta mode actually filters
57
+ // pre-existing issues after the first write.
58
+ const ctx = createDispatchContext(filePath, cwd, pi, sessionBaselines, true);
42
59
 
43
60
  // Import dispatchForFile dynamically to avoid circular deps
44
61
  const { dispatchForFile } = await import("./dispatcher.js");
@@ -55,6 +72,47 @@ export async function dispatchLint(
55
72
  return result.output;
56
73
  }
57
74
 
75
+ /**
76
+ * Run linting and return full result (including diagnostics)
77
+ */
78
+ export async function dispatchLintWithResult(
79
+ filePath: string,
80
+ cwd: string,
81
+ pi: PiAgentAPI,
82
+ ): Promise<DispatchResult> {
83
+ const ctx = createDispatchContext(filePath, cwd, pi, sessionBaselines, true);
84
+
85
+ const { dispatchForFile } = await import("./dispatcher.js");
86
+ const { getRunnersForKind } = await import("./dispatcher.js");
87
+ const { TOOL_PLANS } = await import("./plan.js");
88
+
89
+ const kind = ctx.kind;
90
+ if (!kind) {
91
+ return {
92
+ diagnostics: [],
93
+ blockers: [],
94
+ warnings: [],
95
+ fixed: [],
96
+ output: "",
97
+ hasBlockers: false,
98
+ };
99
+ }
100
+
101
+ const plan = TOOL_PLANS[kind];
102
+ if (!plan) {
103
+ return {
104
+ diagnostics: [],
105
+ blockers: [],
106
+ warnings: [],
107
+ fixed: [],
108
+ output: "",
109
+ hasBlockers: false,
110
+ };
111
+ }
112
+
113
+ return await dispatchForFile(ctx, plan.groups);
114
+ }
115
+
58
116
  /**
59
117
  * Create a baseline store for delta mode tracking
60
118
  */