pi-lens 3.0.1 → 3.1.1
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 +105 -0
- package/README.md +5 -4
- package/clients/bus/integration.js +6 -6
- package/clients/bus/integration.ts +36 -23
- package/clients/dispatch/dispatcher.js +114 -1
- package/clients/dispatch/dispatcher.ts +170 -1
- package/clients/dispatch/integration.js +3 -1
- package/clients/dispatch/integration.ts +13 -1
- package/clients/dispatch/plan.js +2 -3
- package/clients/dispatch/plan.ts +2 -3
- package/clients/dispatch/runners/ast-grep-napi.js +158 -59
- package/clients/dispatch/runners/ast-grep-napi.ts +244 -97
- package/clients/dispatch/runners/ast-grep.js +5 -7
- package/clients/dispatch/runners/ast-grep.ts +13 -12
- package/clients/dispatch/runners/biome.js +3 -3
- package/clients/dispatch/runners/biome.ts +3 -3
- package/clients/dispatch/runners/index.js +5 -3
- package/clients/dispatch/runners/index.ts +5 -3
- package/clients/dispatch/runners/oxlint.js +41 -115
- package/clients/dispatch/runners/oxlint.ts +53 -134
- package/clients/dispatch/runners/pyright.js +2 -2
- package/clients/dispatch/runners/pyright.ts +2 -2
- package/clients/dispatch/runners/ruff.js +2 -2
- package/clients/dispatch/runners/ruff.ts +2 -2
- package/clients/format-service.js +39 -27
- package/clients/format-service.ts +52 -34
- package/clients/latency-logger.js +40 -0
- package/clients/latency-logger.ts +59 -0
- package/clients/safe-spawn-async.js +163 -0
- package/clients/safe-spawn-async.ts +220 -0
- package/clients/safe-spawn.js +170 -27
- package/clients/safe-spawn.ts +208 -29
- package/clients/services/runner-service.js +36 -7
- package/clients/services/runner-service.ts +64 -18
- package/clients/symbol-types.js +5 -0
- package/clients/symbol-types.ts +77 -0
- package/clients/tree-sitter-client.js +4 -0
- package/clients/tree-sitter-client.ts +5 -0
- package/clients/tree-sitter-symbol-extractor.js +289 -0
- package/clients/tree-sitter-symbol-extractor.ts +331 -0
- package/index.ts +48 -1
- package/package.json +1 -1
- package/rules/ts-slop-rules/.sgconfig.yml +0 -4
- package/rules/ts-slop-rules/rules/in-correct-optional-input-type.yml +0 -10
- package/rules/ts-slop-rules/rules/jwt-no-verify.yml +0 -13
- package/rules/ts-slop-rules/rules/no-architecture-violation.yml +0 -10
- package/rules/ts-slop-rules/rules/no-case-declarations.yml +0 -10
- package/rules/ts-slop-rules/rules/no-dangerously-set-inner-html.yml +0 -10
- package/rules/ts-slop-rules/rules/no-debugger.yml +0 -10
- package/rules/ts-slop-rules/rules/no-dupe-args.yml +0 -10
- package/rules/ts-slop-rules/rules/no-dupe-class-members.yml +0 -10
- package/rules/ts-slop-rules/rules/no-dupe-keys.yml +0 -10
- package/rules/ts-slop-rules/rules/no-eval.yml +0 -13
- package/rules/ts-slop-rules/rules/no-hardcoded-secrets.yml +0 -12
- package/rules/ts-slop-rules/rules/no-implied-eval.yml +0 -12
- package/rules/ts-slop-rules/rules/no-inner-html.yml +0 -13
- package/rules/ts-slop-rules/rules/no-javascript-url.yml +0 -10
- package/rules/ts-slop-rules/rules/no-mutable-default.yml +0 -10
- package/rules/ts-slop-rules/rules/no-nested-links.yml +0 -12
- package/rules/ts-slop-rules/rules/no-new-symbol.yml +0 -10
- package/rules/ts-slop-rules/rules/no-new-wrappers.yml +0 -13
- package/rules/ts-slop-rules/rules/no-open-redirect.yml +0 -16
- package/rules/ts-slop-rules/rules/weak-rsa-key.yml +0 -12
- /package/rules/{ts-slop-rules/rules/slop-rules.yml → ast-grep-rules/slop-patterns.yml} +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,111 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to pi-lens will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## [3.1.1] - 2026-04-01
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
- **File-based latency logging** — Performance analysis via `~/.pi-lens/latency.log`
|
|
9
|
+
- New `latency-logger.ts` module for centralized logging
|
|
10
|
+
- Logs every runner's timing (ts-lsp, ast-grep-napi, biome, test-runner, etc.)
|
|
11
|
+
- Logs tool_result overall timing with result status (completed/blocked/no_output)
|
|
12
|
+
- JSON Lines format for easy analysis with `jq`
|
|
13
|
+
- Read with: `cat ~/.pi-lens/latency.log | jq -s '.[] | select(.type=="runner")'`
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## [3.1.0] - 2026-04-01
|
|
18
|
+
|
|
19
|
+
### Changed
|
|
20
|
+
- **Consolidated ast-grep runners** — Unified CLI and NAPI runners with shared rule set
|
|
21
|
+
- NAPI runner now primary for dispatch (100x faster than CLI spawn)
|
|
22
|
+
- Merged ts-slop-rules (21 files) into ast-grep-rules/slop-patterns.yml (33 patterns)
|
|
23
|
+
- Removed 20 duplicate rule files with conflicting IDs (e.g., `ts-jwt-no-verify` vs `jwt-no-verify`)
|
|
24
|
+
- Total: 104 unified rules (71 security/architecture + 33 slop patterns)
|
|
25
|
+
- CLI ast-grep kept only for `ast_grep_search` / `ast_grep_replace` tools
|
|
26
|
+
|
|
27
|
+
### Fixed
|
|
28
|
+
- **ast-grep-napi stability** — Fixed stack overflow crashes in AST traversal
|
|
29
|
+
- Added `_MAX_AST_DEPTH = 50` depth limit to `findByKind()` and `getAllNodes()`
|
|
30
|
+
- Added `_MAX_RULE_DEPTH = 5` recursion limit for structured rules
|
|
31
|
+
- Added `MAX_MATCHES_PER_RULE = 10` to prevent false positive explosions
|
|
32
|
+
- Added `MAX_TOTAL_DIAGNOSTICS = 50` to prevent output spam
|
|
33
|
+
- NAPI runner now safely handles deeply nested TypeScript files
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## [3.0.1] - 2026-03-31
|
|
38
|
+
|
|
39
|
+
### Changed
|
|
40
|
+
- **Documentation refresh**: Updated npm and README descriptions for v3.0.0 features
|
|
41
|
+
- New tagline: "pi extension for real-time code quality"
|
|
42
|
+
- Highlights 31 LSP servers, tree-sitter analysis, auto-install capability
|
|
43
|
+
- Clarified blockers vs warnings split (inline vs `/lens-booboo`)
|
|
44
|
+
|
|
45
|
+
### Fixed
|
|
46
|
+
- **Entropy threshold**: Increased from 3.5 → 5.5 bits to reduce false positives
|
|
47
|
+
- Previous threshold was too sensitive for tooling codebases
|
|
48
|
+
- Eliminates ~70-80% of "High entropy" warnings on legitimate complex code
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
## [3.0.0] - 2026-03-31
|
|
53
|
+
|
|
54
|
+
### Breaking Changes
|
|
55
|
+
|
|
56
|
+
#### Removed - Deprecated Commands
|
|
57
|
+
The following deprecated commands have been removed:
|
|
58
|
+
- `/lens-booboo-fix` → Use `/lens-booboo` with autofix capability
|
|
59
|
+
- `/lens-booboo-delta` → Delta mode now automatic
|
|
60
|
+
- `/lens-booboo-refactor` → Use `/lens-booboo` findings
|
|
61
|
+
- `/lens-metrics` → Metrics now in `/lens-booboo` report
|
|
62
|
+
- `/lens-rate` → Use `/lens-booboo` quality scoring
|
|
63
|
+
|
|
64
|
+
#### Changed - Blockers vs Warnings Architecture
|
|
65
|
+
- **🔴 Blockers** (type errors, secrets, empty catch blocks) → Appear **inline** and stop the agent
|
|
66
|
+
- **🟡 Warnings** (complexity, code smells) → Go to **`/lens-booboo`** only (not inline)
|
|
67
|
+
- Tree-sitter rules with `severity: error` now properly block inline
|
|
68
|
+
- Dispatcher checks individual diagnostic semantic, not just group default
|
|
69
|
+
|
|
70
|
+
### Added - Tree-Sitter Runner
|
|
71
|
+
New structural analysis runner at priority 14:
|
|
72
|
+
- **18 YAML query files** for TypeScript and Python patterns
|
|
73
|
+
- TypeScript: empty-catch, eval, debugger, console-statement, hardcoded-secrets, deep-nesting, deep-promise-chain, mixed-async-styles, nested-ternary, long-parameter-list, await-in-loop, dangerously-set-inner-html
|
|
74
|
+
- Python: bare-except, eval-exec, wildcard-import, is-vs-equals, mutable-default-arg, unreachable-except
|
|
75
|
+
- Blockers appear inline (severity: error), warnings go to `/lens-booboo` (severity: warning)
|
|
76
|
+
|
|
77
|
+
### Added - Auto-Install for Core Tools
|
|
78
|
+
Four tools now auto-install on first use (no manual setup required):
|
|
79
|
+
1. **TypeScript Language Server** (`typescript-language-server`) — TS/JS type checking
|
|
80
|
+
2. **Pyright** — Python type checking (`pip install pyright`)
|
|
81
|
+
3. **Ruff** — Python linting (`pip install ruff`)
|
|
82
|
+
4. **Biome** — JS/TS/JSON linting and formatting
|
|
83
|
+
|
|
84
|
+
Installs to `.pi-lens/tools/` with verification step (`--version` check).
|
|
85
|
+
|
|
86
|
+
### Added - NAPI Security Rules
|
|
87
|
+
Migrated 20 critical security rules to NAPI (fast native execution):
|
|
88
|
+
- Rules with `weight >= 4` are **blocking** (stop the agent)
|
|
89
|
+
- Includes: no-eval, no-hardcoded-secrets, no-implied-eval, no-inner-html, no-dangerously-set-inner-html, no-debugger, no-javascript-url, no-open-redirect, no-mutable-default, weak-rsa-key, jwt-no-verify, and more
|
|
90
|
+
- NAPI runs at priority 15 (after tree-sitter, before slop rules)
|
|
91
|
+
|
|
92
|
+
### Fixed
|
|
93
|
+
- **Tree-sitter query loading**: Added missing `loadQueries()` call before `getAllQueries()`
|
|
94
|
+
- **Windows path handling**: Changed from `lastIndexOf("/")` to `path.dirname()` for cross-platform compatibility
|
|
95
|
+
- **Dispatcher blocker detection**: Now checks if any individual diagnostic has `semantic === "blocking"`
|
|
96
|
+
- **Biome runner npx fallback**: Uses `npx biome` when `biome` not in PATH directly
|
|
97
|
+
- **LSP ENOENT crashes**: Added `_attachErrorHandler()` to all 23 manual-install LSP servers
|
|
98
|
+
- **LSP initialization timeout**: Increased to 120s (was 45s)
|
|
99
|
+
- **ESLint scope reduction**: Removed `.ts/.tsx` from ESLint LSP (now JS/framework files only)
|
|
100
|
+
- **Biome/Prettier race**: Biome is now default (priority 10), Prettier is fallback only
|
|
101
|
+
|
|
102
|
+
### Changed
|
|
103
|
+
- **README reorganization**: Removed redundant sections (Architecture, Language Support, Rules, Delta-mode, Slop Detection)
|
|
104
|
+
- **Consolidated Additional Safeguards** into Features section with Runners table
|
|
105
|
+
- **Updated .gitignore**: Local tracking files stay out of repo
|
|
106
|
+
- **Tuned thresholds**: 70-80% false positive reduction in booboo reports
|
|
107
|
+
|
|
108
|
+
---
|
|
109
|
+
|
|
5
110
|
## [2.7.0] - 2026-03-31
|
|
6
111
|
|
|
7
112
|
### Added - New Lint Runners
|
package/README.md
CHANGED
|
@@ -206,12 +206,11 @@ pi-lens uses a **dispatcher-runner architecture** for extensible multi-language
|
|
|
206
206
|
| **ruff** | Python | 10 | Warning | Python linting (delta-tracked) |
|
|
207
207
|
| **oxlint** | TS/JS | 12 | Warning | Fast Rust-based JS/TS linter |
|
|
208
208
|
| **tree-sitter** | TS/JS, Python | 14 | Mixed | AST-based structural analysis (17 patterns) |
|
|
209
|
-
| **ast-grep-napi** | TS/JS | 15 | Warning | **
|
|
209
|
+
| **ast-grep-napi** | TS/JS | 15 | Warning | **Unified structural analysis** (104 rules) |
|
|
210
210
|
| **type-safety** | TS | 20 | Mixed | Switch exhaustiveness (blocking), other (warning) |
|
|
211
211
|
| **shellcheck** | Shell | 20 | Warning | Bash/sh/zsh/fish linting |
|
|
212
212
|
| **python-slop** | Python | 25 | Warning | AI slop detection (~40 patterns) |
|
|
213
213
|
| **spellcheck** | Markdown | 30 | Warning | Typo detection in docs |
|
|
214
|
-
| **ast-grep** | Go, Rust, Python, etc. | 30 | Warning | Structural analysis via CLI (fallback for non-TS/JS) |
|
|
215
214
|
| **similarity** | TS | 35 | Silent | Semantic duplicate detection (metrics only) |
|
|
216
215
|
| **architect** | All | 40 | Warning | Architectural rule violations |
|
|
217
216
|
| **go-vet** | Go | 50 | Warning | Go static analysis |
|
|
@@ -229,7 +228,7 @@ pi-lens uses a **dispatcher-runner architecture** for extensible multi-language
|
|
|
229
228
|
- **Warning** — Shown in `/lens-booboo`, not inline (noise reduction)
|
|
230
229
|
- **Silent** — Tracked in metrics only, never shown
|
|
231
230
|
|
|
232
|
-
**
|
|
231
|
+
**Consolidated runners:** `ast-grep` (CLI) and `ts-slop` merged into `ast-grep-napi` — unified 104-rule set
|
|
233
232
|
|
|
234
233
|
**Tree-sitter runner patterns** (priority 14, AST-based structural analysis):
|
|
235
234
|
|
|
@@ -243,7 +242,9 @@ Python (6 patterns):
|
|
|
243
242
|
|
|
244
243
|
**Custom tree-sitter queries:** Add `.yml` files to `.pi-lens/rules/tree-sitter-queries/{typescript,python}/`
|
|
245
244
|
|
|
246
|
-
**AI Slop Detection:**
|
|
245
|
+
**AI Slop Detection:**
|
|
246
|
+
- `python-slop` runner (priority 25): ~40 patterns for Python code quality
|
|
247
|
+
- `ast-grep-napi` runner (priority 15): 33 slop patterns + 71 security/architecture rules for TypeScript/JavaScript
|
|
247
248
|
|
|
248
249
|
---
|
|
249
250
|
|
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
* - Real-time progress tracking
|
|
8
8
|
* - Hook integration for tool_result handler
|
|
9
9
|
*/
|
|
10
|
-
import { RunnerStarted, RunnerCompleted, ReportReady, FileModified, DiagnosticAggregator, } from "./events.js";
|
|
11
10
|
import { enableDebug } from "./bus.js";
|
|
11
|
+
import { DiagnosticAggregator, FileModified, ReportReady, RunnerCompleted, RunnerStarted, } from "./events.js";
|
|
12
12
|
const state = {
|
|
13
13
|
aggregator: new DiagnosticAggregator(),
|
|
14
14
|
runnerInProgress: new Set(),
|
|
@@ -21,7 +21,7 @@ let unsubscribers = [];
|
|
|
21
21
|
* Initialize the bus integration
|
|
22
22
|
* Call this from session_start handler
|
|
23
23
|
*/
|
|
24
|
-
export function initBusIntegration(
|
|
24
|
+
export function initBusIntegration(_pi, options) {
|
|
25
25
|
if (state.isEnabled)
|
|
26
26
|
return; // Already initialized
|
|
27
27
|
if (options?.debug) {
|
|
@@ -37,13 +37,13 @@ export function initBusIntegration(pi, options) {
|
|
|
37
37
|
const unsubRunnerCompleted = RunnerCompleted.subscribe((event) => {
|
|
38
38
|
const { runnerId, filePath, durationMs, diagnosticCount } = event.properties;
|
|
39
39
|
state.runnerInProgress.delete(`${runnerId}:${filePath}`);
|
|
40
|
-
// Log slow runners in debug mode
|
|
40
|
+
// Log slow runners in debug mode only
|
|
41
41
|
if (options?.debug && durationMs > 5000) {
|
|
42
42
|
console.error(`[bus] Slow runner: ${runnerId} took ${durationMs}ms for ${filePath}`);
|
|
43
43
|
}
|
|
44
|
-
// Log runners that found issues
|
|
45
|
-
if (diagnosticCount >
|
|
46
|
-
console.error(`[bus] ${runnerId} found ${diagnosticCount} issues in ${filePath}`);
|
|
44
|
+
// Log runners that found excessive issues (indicates rule misconfiguration)
|
|
45
|
+
if (diagnosticCount > 100) {
|
|
46
|
+
console.error(`[bus] ${runnerId} found ${diagnosticCount} issues in ${filePath} (rules may be too broad)`);
|
|
47
47
|
}
|
|
48
48
|
});
|
|
49
49
|
// Cache reports for quick retrieval
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Bus Integration for pi-lens
|
|
3
|
-
*
|
|
3
|
+
*
|
|
4
4
|
* Connects the event bus system to the existing pi-lens architecture.
|
|
5
5
|
* This provides:
|
|
6
6
|
* - Event aggregation for diagnostic collection
|
|
@@ -8,19 +8,16 @@
|
|
|
8
8
|
* - Hook integration for tool_result handler
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
+
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
12
|
+
import { enableDebug } from "./bus.js";
|
|
11
13
|
import {
|
|
12
|
-
DiagnosticFound,
|
|
13
|
-
RunnerStarted,
|
|
14
|
-
RunnerCompleted,
|
|
15
|
-
ReportReady,
|
|
16
|
-
FileModified,
|
|
17
|
-
SessionStarted,
|
|
18
|
-
TurnEnded,
|
|
19
|
-
DiagnosticAggregator,
|
|
20
14
|
type Diagnostic,
|
|
15
|
+
DiagnosticAggregator,
|
|
16
|
+
FileModified,
|
|
17
|
+
ReportReady,
|
|
18
|
+
RunnerCompleted,
|
|
19
|
+
RunnerStarted,
|
|
21
20
|
} from "./events.js";
|
|
22
|
-
import { subscribe, enableDebug } from "./bus.js";
|
|
23
|
-
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
24
21
|
|
|
25
22
|
// --- Integration State ---
|
|
26
23
|
|
|
@@ -46,7 +43,10 @@ let unsubscribers: Array<() => void> = [];
|
|
|
46
43
|
* Initialize the bus integration
|
|
47
44
|
* Call this from session_start handler
|
|
48
45
|
*/
|
|
49
|
-
export function initBusIntegration(
|
|
46
|
+
export function initBusIntegration(
|
|
47
|
+
_pi: ExtensionAPI,
|
|
48
|
+
options?: { debug?: boolean },
|
|
49
|
+
): void {
|
|
50
50
|
if (state.isEnabled) return; // Already initialized
|
|
51
51
|
|
|
52
52
|
if (options?.debug) {
|
|
@@ -63,24 +63,29 @@ export function initBusIntegration(pi: ExtensionAPI, options?: { debug?: boolean
|
|
|
63
63
|
});
|
|
64
64
|
|
|
65
65
|
const unsubRunnerCompleted = RunnerCompleted.subscribe((event) => {
|
|
66
|
-
const { runnerId, filePath, durationMs, diagnosticCount } =
|
|
66
|
+
const { runnerId, filePath, durationMs, diagnosticCount } =
|
|
67
|
+
event.properties;
|
|
67
68
|
state.runnerInProgress.delete(`${runnerId}:${filePath}`);
|
|
68
69
|
|
|
69
|
-
// Log slow runners in debug mode
|
|
70
|
+
// Log slow runners in debug mode only
|
|
70
71
|
if (options?.debug && durationMs > 5000) {
|
|
71
|
-
console.error(
|
|
72
|
+
console.error(
|
|
73
|
+
`[bus] Slow runner: ${runnerId} took ${durationMs}ms for ${filePath}`,
|
|
74
|
+
);
|
|
72
75
|
}
|
|
73
76
|
|
|
74
|
-
// Log runners that found issues
|
|
75
|
-
if (diagnosticCount >
|
|
76
|
-
console.error(
|
|
77
|
+
// Log runners that found excessive issues (indicates rule misconfiguration)
|
|
78
|
+
if (diagnosticCount > 100) {
|
|
79
|
+
console.error(
|
|
80
|
+
`[bus] ${runnerId} found ${diagnosticCount} issues in ${filePath} (rules may be too broad)`,
|
|
81
|
+
);
|
|
77
82
|
}
|
|
78
83
|
});
|
|
79
84
|
|
|
80
85
|
// Cache reports for quick retrieval
|
|
81
86
|
const unsubReportReady = ReportReady.subscribe((event) => {
|
|
82
87
|
const { filePath, report, durationMs } = event.properties;
|
|
83
|
-
|
|
88
|
+
|
|
84
89
|
// Store the report
|
|
85
90
|
state.lastReport.set(filePath, {
|
|
86
91
|
output: formatReport(report, durationMs),
|
|
@@ -91,10 +96,10 @@ export function initBusIntegration(pi: ExtensionAPI, options?: { debug?: boolean
|
|
|
91
96
|
// Track file modifications to clear stale data
|
|
92
97
|
const unsubFileModified = FileModified.subscribe((event) => {
|
|
93
98
|
const { filePath } = event.properties;
|
|
94
|
-
|
|
99
|
+
|
|
95
100
|
// Clear cached report for modified file
|
|
96
101
|
state.lastReport.delete(filePath);
|
|
97
|
-
|
|
102
|
+
|
|
98
103
|
// Clear diagnostics aggregator for this file (will be repopulated)
|
|
99
104
|
state.aggregator.clear(filePath);
|
|
100
105
|
});
|
|
@@ -139,7 +144,12 @@ export function shutdownBusIntegration(): void {
|
|
|
139
144
|
// --- Helper Functions ---
|
|
140
145
|
|
|
141
146
|
function formatReport(
|
|
142
|
-
report: {
|
|
147
|
+
report: {
|
|
148
|
+
blockers: Diagnostic[];
|
|
149
|
+
warnings: Diagnostic[];
|
|
150
|
+
fixed: Diagnostic[];
|
|
151
|
+
silent: Diagnostic[];
|
|
152
|
+
},
|
|
143
153
|
durationMs: number,
|
|
144
154
|
): string {
|
|
145
155
|
const lines: string[] = [];
|
|
@@ -198,7 +208,10 @@ export function hasRunnersInProgress(filePath?: string): boolean {
|
|
|
198
208
|
/**
|
|
199
209
|
* Get list of runners in progress
|
|
200
210
|
*/
|
|
201
|
-
export function getRunnersInProgress(): Array<{
|
|
211
|
+
export function getRunnersInProgress(): Array<{
|
|
212
|
+
runnerId: string;
|
|
213
|
+
filePath: string;
|
|
214
|
+
}> {
|
|
202
215
|
return Array.from(state.runnerInProgress).map((key) => {
|
|
203
216
|
const [runnerId, filePath] = key.split(":");
|
|
204
217
|
return { runnerId, filePath };
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
import { detectFileKind } from "../file-kinds.js";
|
|
17
17
|
import { isTestFile } from "../file-utils.js";
|
|
18
18
|
import { safeSpawn } from "../safe-spawn.js";
|
|
19
|
+
import { logLatency } from "../latency-logger.js";
|
|
19
20
|
import { formatDiagnostics } from "./utils/format-utils.js";
|
|
20
21
|
// --- In-Memory Baseline Store ---
|
|
21
22
|
export function createBaselineStore() {
|
|
@@ -117,11 +118,53 @@ function filterDelta(after, before, keyFn) {
|
|
|
117
118
|
const newItems = after.filter((d) => !beforeSet.has(keyFn(d)));
|
|
118
119
|
return { new: newItems, fixed };
|
|
119
120
|
}
|
|
121
|
+
const latencyReports = [];
|
|
122
|
+
export function getLatencyReports() {
|
|
123
|
+
return [...latencyReports];
|
|
124
|
+
}
|
|
125
|
+
export function clearLatencyReports() {
|
|
126
|
+
latencyReports.length = 0;
|
|
127
|
+
}
|
|
128
|
+
export function formatLatencyReport(report) {
|
|
129
|
+
const lines = [];
|
|
130
|
+
lines.push(`\n═══════════════════════════════════════════════════════════════`);
|
|
131
|
+
lines.push(`📊 DISPATCH LATENCY REPORT: ${report.filePath.split("/").pop()}`);
|
|
132
|
+
lines.push(` Kind: ${report.fileKind || "unknown"} | Total: ${report.totalDurationMs}ms`);
|
|
133
|
+
lines.push(`───────────────────────────────────────────────────────────────`);
|
|
134
|
+
lines.push(`Runner Duration Status Issues Semantic`);
|
|
135
|
+
lines.push(`───────────────────────────────────────────────────────────────`);
|
|
136
|
+
for (const r of report.runners) {
|
|
137
|
+
const name = r.runnerId.padEnd(30);
|
|
138
|
+
const dur = `${r.durationMs}ms`.padStart(8);
|
|
139
|
+
const status = r.status.padStart(9);
|
|
140
|
+
const issues = String(r.diagnosticCount).padStart(6);
|
|
141
|
+
const sem = r.semantic.padStart(8);
|
|
142
|
+
const slowMarker = r.durationMs > 500 ? " 🔥" : r.durationMs > 100 ? " ⚡" : "";
|
|
143
|
+
lines.push(`${name}${dur}${status}${issues}${sem}${slowMarker}`);
|
|
144
|
+
}
|
|
145
|
+
lines.push(`───────────────────────────────────────────────────────────────`);
|
|
146
|
+
lines.push(`Total: ${report.runners.length} runners | Stopped early: ${report.stoppedEarly}`);
|
|
147
|
+
lines.push(`Diagnostics: ${report.totalDiagnostics} (${report.blockers} blockers, ${report.warnings} warnings)`);
|
|
148
|
+
// Show top 3 slowest
|
|
149
|
+
const sorted = [...report.runners].sort((a, b) => b.durationMs - a.durationMs);
|
|
150
|
+
if (sorted.length > 0 && sorted[0].durationMs > 100) {
|
|
151
|
+
lines.push(`\n🐌 Slowest runners:`);
|
|
152
|
+
for (const r of sorted.slice(0, 3)) {
|
|
153
|
+
if (r.durationMs > 50) {
|
|
154
|
+
lines.push(` ${r.runnerId}: ${r.durationMs}ms (${r.status})`);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
lines.push(`═══════════════════════════════════════════════════════════════`);
|
|
159
|
+
return lines.join("\n");
|
|
160
|
+
}
|
|
120
161
|
// --- Main Dispatch Function ---
|
|
121
162
|
export async function dispatchForFile(ctx, groups) {
|
|
163
|
+
const _overallStart = Date.now();
|
|
122
164
|
const allDiagnostics = [];
|
|
123
165
|
const _fixed = [];
|
|
124
166
|
let stopped = false;
|
|
167
|
+
const runnerLatencies = [];
|
|
125
168
|
for (const group of groups) {
|
|
126
169
|
if (stopped && ctx.pi.getFlag("stop-on-error")) {
|
|
127
170
|
break;
|
|
@@ -135,14 +178,50 @@ export async function dispatchForFile(ctx, groups) {
|
|
|
135
178
|
: group.runnerIds;
|
|
136
179
|
const semantic = group.semantic ?? "warning";
|
|
137
180
|
for (const runnerId of runnerIds) {
|
|
181
|
+
const runnerStart = Date.now();
|
|
138
182
|
const runner = getRunner(runnerId);
|
|
139
|
-
if (!runner)
|
|
183
|
+
if (!runner) {
|
|
184
|
+
runnerLatencies.push({
|
|
185
|
+
runnerId,
|
|
186
|
+
startTime: runnerStart,
|
|
187
|
+
endTime: Date.now(),
|
|
188
|
+
durationMs: 0,
|
|
189
|
+
status: "skipped",
|
|
190
|
+
diagnosticCount: 0,
|
|
191
|
+
semantic: "unknown",
|
|
192
|
+
});
|
|
140
193
|
continue;
|
|
194
|
+
}
|
|
141
195
|
// Check preconditions
|
|
142
196
|
if (runner.when && !(await runner.when(ctx))) {
|
|
197
|
+
runnerLatencies.push({
|
|
198
|
+
runnerId,
|
|
199
|
+
startTime: runnerStart,
|
|
200
|
+
endTime: Date.now(),
|
|
201
|
+
durationMs: Date.now() - runnerStart,
|
|
202
|
+
status: "when_skipped",
|
|
203
|
+
diagnosticCount: 0,
|
|
204
|
+
semantic: runner.id,
|
|
205
|
+
});
|
|
143
206
|
continue;
|
|
144
207
|
}
|
|
145
208
|
const result = await runRunner(ctx, runner, semantic);
|
|
209
|
+
const runnerEnd = Date.now();
|
|
210
|
+
const duration = runnerEnd - runnerStart;
|
|
211
|
+
// Track latency for this runner
|
|
212
|
+
runnerLatencies.push({
|
|
213
|
+
runnerId,
|
|
214
|
+
startTime: runnerStart,
|
|
215
|
+
endTime: runnerEnd,
|
|
216
|
+
durationMs: duration,
|
|
217
|
+
status: result.status,
|
|
218
|
+
diagnosticCount: result.diagnostics.length,
|
|
219
|
+
semantic: result.semantic ?? semantic,
|
|
220
|
+
});
|
|
221
|
+
// Log slow runners immediately for real-time debugging
|
|
222
|
+
if (duration > 500) {
|
|
223
|
+
ctx.log(`⚠️ SLOW RUNNER: ${runnerId} took ${duration}ms (${result.status}, ${result.diagnostics.length} issues)`);
|
|
224
|
+
}
|
|
146
225
|
// Apply delta mode filtering
|
|
147
226
|
let diagnostics = result.diagnostics;
|
|
148
227
|
if (ctx.deltaMode && result.semantic !== "silent") {
|
|
@@ -175,6 +254,40 @@ export async function dispatchForFile(ctx, groups) {
|
|
|
175
254
|
// Warnings tracked but not shown (noise) — surfaced via /lens-booboo
|
|
176
255
|
let output = formatDiagnostics(blockers, "blocking");
|
|
177
256
|
output += formatDiagnostics(fixedItems, "fixed");
|
|
257
|
+
// Generate and store latency report
|
|
258
|
+
const overallEnd = Date.now();
|
|
259
|
+
const latencyReport = {
|
|
260
|
+
filePath: ctx.filePath,
|
|
261
|
+
fileKind: ctx.kind,
|
|
262
|
+
overallStartMs: _overallStart,
|
|
263
|
+
overallEndMs: overallEnd,
|
|
264
|
+
totalDurationMs: overallEnd - _overallStart,
|
|
265
|
+
runners: runnerLatencies,
|
|
266
|
+
stoppedEarly: stopped,
|
|
267
|
+
totalDiagnostics: allDiagnostics.length,
|
|
268
|
+
blockers: blockers.length,
|
|
269
|
+
warnings: warnings.length,
|
|
270
|
+
};
|
|
271
|
+
// Store for later analysis
|
|
272
|
+
latencyReports.push(latencyReport);
|
|
273
|
+
// Keep only last 100 reports to prevent memory bloat
|
|
274
|
+
if (latencyReports.length > 100) {
|
|
275
|
+
latencyReports.shift();
|
|
276
|
+
}
|
|
277
|
+
// Log each runner as separate entry for detailed analysis
|
|
278
|
+
for (const runner of runnerLatencies) {
|
|
279
|
+
logLatency({
|
|
280
|
+
type: "runner",
|
|
281
|
+
filePath: ctx.filePath,
|
|
282
|
+
runnerId: runner.runnerId,
|
|
283
|
+
durationMs: runner.durationMs,
|
|
284
|
+
status: runner.status,
|
|
285
|
+
diagnosticCount: runner.diagnosticCount,
|
|
286
|
+
semantic: runner.semantic,
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
// Log summary to stderr for real-time monitoring
|
|
290
|
+
console.error(formatLatencyReport(latencyReport));
|
|
178
291
|
return {
|
|
179
292
|
diagnostics: allDiagnostics,
|
|
180
293
|
blockers,
|
|
@@ -18,6 +18,7 @@ import type { FileKind } from "../file-kinds.js";
|
|
|
18
18
|
import { detectFileKind } from "../file-kinds.js";
|
|
19
19
|
import { isTestFile } from "../file-utils.js";
|
|
20
20
|
import { safeSpawn } from "../safe-spawn.js";
|
|
21
|
+
import { logLatency, type LatencyEntry } from "../latency-logger.js";
|
|
21
22
|
import type {
|
|
22
23
|
BaselineStore,
|
|
23
24
|
Diagnostic,
|
|
@@ -164,15 +165,103 @@ function filterDelta<T extends { id: string }>(
|
|
|
164
165
|
return { new: newItems, fixed };
|
|
165
166
|
}
|
|
166
167
|
|
|
168
|
+
// --- Latency Logger ---
|
|
169
|
+
|
|
170
|
+
export interface RunnerLatency {
|
|
171
|
+
runnerId: string;
|
|
172
|
+
startTime: number;
|
|
173
|
+
endTime: number;
|
|
174
|
+
durationMs: number;
|
|
175
|
+
status: "succeeded" | "failed" | "skipped" | "when_skipped";
|
|
176
|
+
diagnosticCount: number;
|
|
177
|
+
semantic: string;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export interface DispatchLatencyReport {
|
|
181
|
+
filePath: string;
|
|
182
|
+
fileKind: string | undefined;
|
|
183
|
+
overallStartMs: number;
|
|
184
|
+
overallEndMs: number;
|
|
185
|
+
totalDurationMs: number;
|
|
186
|
+
runners: RunnerLatency[];
|
|
187
|
+
stoppedEarly: boolean;
|
|
188
|
+
totalDiagnostics: number;
|
|
189
|
+
blockers: number;
|
|
190
|
+
warnings: number;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const latencyReports: DispatchLatencyReport[] = [];
|
|
194
|
+
|
|
195
|
+
export function getLatencyReports(): DispatchLatencyReport[] {
|
|
196
|
+
return [...latencyReports];
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export function clearLatencyReports(): void {
|
|
200
|
+
latencyReports.length = 0;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export function formatLatencyReport(report: DispatchLatencyReport): string {
|
|
204
|
+
const lines: string[] = [];
|
|
205
|
+
lines.push(
|
|
206
|
+
`\n═══════════════════════════════════════════════════════════════`,
|
|
207
|
+
);
|
|
208
|
+
lines.push(`📊 DISPATCH LATENCY REPORT: ${report.filePath.split("/").pop()}`);
|
|
209
|
+
lines.push(
|
|
210
|
+
` Kind: ${report.fileKind || "unknown"} | Total: ${report.totalDurationMs}ms`,
|
|
211
|
+
);
|
|
212
|
+
lines.push(`───────────────────────────────────────────────────────────────`);
|
|
213
|
+
lines.push(
|
|
214
|
+
`Runner Duration Status Issues Semantic`,
|
|
215
|
+
);
|
|
216
|
+
lines.push(`───────────────────────────────────────────────────────────────`);
|
|
217
|
+
|
|
218
|
+
for (const r of report.runners) {
|
|
219
|
+
const name = r.runnerId.padEnd(30);
|
|
220
|
+
const dur = `${r.durationMs}ms`.padStart(8);
|
|
221
|
+
const status = r.status.padStart(9);
|
|
222
|
+
const issues = String(r.diagnosticCount).padStart(6);
|
|
223
|
+
const sem = r.semantic.padStart(8);
|
|
224
|
+
const slowMarker =
|
|
225
|
+
r.durationMs > 500 ? " 🔥" : r.durationMs > 100 ? " ⚡" : "";
|
|
226
|
+
lines.push(`${name}${dur}${status}${issues}${sem}${slowMarker}`);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
lines.push(`───────────────────────────────────────────────────────────────`);
|
|
230
|
+
lines.push(
|
|
231
|
+
`Total: ${report.runners.length} runners | Stopped early: ${report.stoppedEarly}`,
|
|
232
|
+
);
|
|
233
|
+
lines.push(
|
|
234
|
+
`Diagnostics: ${report.totalDiagnostics} (${report.blockers} blockers, ${report.warnings} warnings)`,
|
|
235
|
+
);
|
|
236
|
+
|
|
237
|
+
// Show top 3 slowest
|
|
238
|
+
const sorted = [...report.runners].sort(
|
|
239
|
+
(a, b) => b.durationMs - a.durationMs,
|
|
240
|
+
);
|
|
241
|
+
if (sorted.length > 0 && sorted[0].durationMs > 100) {
|
|
242
|
+
lines.push(`\n🐌 Slowest runners:`);
|
|
243
|
+
for (const r of sorted.slice(0, 3)) {
|
|
244
|
+
if (r.durationMs > 50) {
|
|
245
|
+
lines.push(` ${r.runnerId}: ${r.durationMs}ms (${r.status})`);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
lines.push(`═══════════════════════════════════════════════════════════════`);
|
|
251
|
+
return lines.join("\n");
|
|
252
|
+
}
|
|
253
|
+
|
|
167
254
|
// --- Main Dispatch Function ---
|
|
168
255
|
|
|
169
256
|
export async function dispatchForFile(
|
|
170
257
|
ctx: DispatchContext,
|
|
171
258
|
groups: RunnerGroup[],
|
|
172
259
|
): Promise<DispatchResult> {
|
|
260
|
+
const _overallStart = Date.now();
|
|
173
261
|
const allDiagnostics: Diagnostic[] = [];
|
|
174
262
|
const _fixed: Diagnostic[] = [];
|
|
175
263
|
let stopped = false;
|
|
264
|
+
const runnerLatencies: RunnerLatency[] = [];
|
|
176
265
|
|
|
177
266
|
for (const group of groups) {
|
|
178
267
|
if (stopped && ctx.pi.getFlag("stop-on-error")) {
|
|
@@ -190,15 +279,56 @@ export async function dispatchForFile(
|
|
|
190
279
|
const semantic = group.semantic ?? "warning";
|
|
191
280
|
|
|
192
281
|
for (const runnerId of runnerIds) {
|
|
282
|
+
const runnerStart = Date.now();
|
|
193
283
|
const runner = getRunner(runnerId);
|
|
194
|
-
if (!runner)
|
|
284
|
+
if (!runner) {
|
|
285
|
+
runnerLatencies.push({
|
|
286
|
+
runnerId,
|
|
287
|
+
startTime: runnerStart,
|
|
288
|
+
endTime: Date.now(),
|
|
289
|
+
durationMs: 0,
|
|
290
|
+
status: "skipped",
|
|
291
|
+
diagnosticCount: 0,
|
|
292
|
+
semantic: "unknown",
|
|
293
|
+
});
|
|
294
|
+
continue;
|
|
295
|
+
}
|
|
195
296
|
|
|
196
297
|
// Check preconditions
|
|
197
298
|
if (runner.when && !(await runner.when(ctx))) {
|
|
299
|
+
runnerLatencies.push({
|
|
300
|
+
runnerId,
|
|
301
|
+
startTime: runnerStart,
|
|
302
|
+
endTime: Date.now(),
|
|
303
|
+
durationMs: Date.now() - runnerStart,
|
|
304
|
+
status: "when_skipped",
|
|
305
|
+
diagnosticCount: 0,
|
|
306
|
+
semantic: runner.id,
|
|
307
|
+
});
|
|
198
308
|
continue;
|
|
199
309
|
}
|
|
200
310
|
|
|
201
311
|
const result = await runRunner(ctx, runner, semantic);
|
|
312
|
+
const runnerEnd = Date.now();
|
|
313
|
+
const duration = runnerEnd - runnerStart;
|
|
314
|
+
|
|
315
|
+
// Track latency for this runner
|
|
316
|
+
runnerLatencies.push({
|
|
317
|
+
runnerId,
|
|
318
|
+
startTime: runnerStart,
|
|
319
|
+
endTime: runnerEnd,
|
|
320
|
+
durationMs: duration,
|
|
321
|
+
status: result.status,
|
|
322
|
+
diagnosticCount: result.diagnostics.length,
|
|
323
|
+
semantic: result.semantic ?? semantic,
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
// Log slow runners immediately for real-time debugging
|
|
327
|
+
if (duration > 500) {
|
|
328
|
+
ctx.log(
|
|
329
|
+
`⚠️ SLOW RUNNER: ${runnerId} took ${duration}ms (${result.status}, ${result.diagnostics.length} issues)`,
|
|
330
|
+
);
|
|
331
|
+
}
|
|
202
332
|
|
|
203
333
|
// Apply delta mode filtering
|
|
204
334
|
let diagnostics = result.diagnostics;
|
|
@@ -243,6 +373,45 @@ export async function dispatchForFile(
|
|
|
243
373
|
let output = formatDiagnostics(blockers, "blocking");
|
|
244
374
|
output += formatDiagnostics(fixedItems, "fixed");
|
|
245
375
|
|
|
376
|
+
// Generate and store latency report
|
|
377
|
+
const overallEnd = Date.now();
|
|
378
|
+
const latencyReport: DispatchLatencyReport = {
|
|
379
|
+
filePath: ctx.filePath,
|
|
380
|
+
fileKind: ctx.kind,
|
|
381
|
+
overallStartMs: _overallStart,
|
|
382
|
+
overallEndMs: overallEnd,
|
|
383
|
+
totalDurationMs: overallEnd - _overallStart,
|
|
384
|
+
runners: runnerLatencies,
|
|
385
|
+
stoppedEarly: stopped,
|
|
386
|
+
totalDiagnostics: allDiagnostics.length,
|
|
387
|
+
blockers: blockers.length,
|
|
388
|
+
warnings: warnings.length,
|
|
389
|
+
};
|
|
390
|
+
|
|
391
|
+
// Store for later analysis
|
|
392
|
+
latencyReports.push(latencyReport);
|
|
393
|
+
|
|
394
|
+
// Keep only last 100 reports to prevent memory bloat
|
|
395
|
+
if (latencyReports.length > 100) {
|
|
396
|
+
latencyReports.shift();
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// Log each runner as separate entry for detailed analysis
|
|
400
|
+
for (const runner of runnerLatencies) {
|
|
401
|
+
logLatency({
|
|
402
|
+
type: "runner",
|
|
403
|
+
filePath: ctx.filePath,
|
|
404
|
+
runnerId: runner.runnerId,
|
|
405
|
+
durationMs: runner.durationMs,
|
|
406
|
+
status: runner.status,
|
|
407
|
+
diagnosticCount: runner.diagnosticCount,
|
|
408
|
+
semantic: runner.semantic,
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// Log summary to stderr for real-time monitoring
|
|
413
|
+
console.error(formatLatencyReport(latencyReport));
|
|
414
|
+
|
|
246
415
|
return {
|
|
247
416
|
diagnostics: allDiagnostics,
|
|
248
417
|
blockers,
|
|
@@ -5,7 +5,9 @@
|
|
|
5
5
|
* with the existing index.ts tool_result handler.
|
|
6
6
|
*/
|
|
7
7
|
import { detectFileKind } from "../file-kinds.js";
|
|
8
|
-
import { createBaselineStore, createDispatchContext } from "./dispatcher.js";
|
|
8
|
+
import { clearLatencyReports, createBaselineStore, createDispatchContext, formatLatencyReport, getLatencyReports, } from "./dispatcher.js";
|
|
9
|
+
// Re-export latency tracking types and functions
|
|
10
|
+
export { clearLatencyReports, formatLatencyReport, getLatencyReports };
|
|
9
11
|
// Import runners to register them
|
|
10
12
|
import "./runners/index.js";
|
|
11
13
|
/**
|