pi-lens 3.6.6 → 3.7.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.
- package/CHANGELOG.md +110 -0
- package/README.md +39 -2
- package/clients/ast-grep-client.ts +31 -9
- package/clients/dispatch/dispatcher.ts +165 -116
- package/clients/dispatch/integration.ts +4 -11
- package/clients/dispatch/runners/lsp.ts +5 -1
- package/clients/dispatch/runners/pyright.ts +5 -1
- package/clients/dispatch/runners/similarity.ts +100 -1
- package/clients/dispatch/runners/type-safety.ts +5 -1
- package/clients/formatters.ts +223 -22
- package/clients/installer/index.ts +37 -16
- package/clients/latency-logger.ts +9 -0
- package/clients/lsp/client.ts +249 -110
- package/clients/lsp/interactive-install.ts +177 -22
- package/clients/lsp/server.ts +142 -50
- package/clients/native-rust-client.ts +531 -0
- package/clients/pipeline.ts +16 -11
- package/clients/test-runner-client.ts +86 -1
- package/commands/booboo.ts +2 -2
- package/index.ts +32 -0
- package/package.json +14 -2
- package/rust/Cargo.toml +34 -0
- package/rust/src/cache.rs +127 -0
- package/rust/src/index.rs +407 -0
- package/rust/src/lib.rs +209 -0
- package/rust/src/main.rs +24 -0
- package/rust/src/scan.rs +116 -0
- package/rust/src/similarity.rs +387 -0
- package/skills/ast-grep/SKILL.md +65 -4
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,116 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to pi-lens will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## [3.7.0] - 2026-04-05
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
- **Test runner in pipeline** — After every file write/edit, pi-lens now automatically detects and
|
|
9
|
+
runs the corresponding test file (vitest, jest, pytest). Results surface inline so the agent sees
|
|
10
|
+
failures immediately without a separate test step. Supports TypeScript/JS/Python; file-level
|
|
11
|
+
targeted — only the test for the edited file runs, not the full suite.
|
|
12
|
+
|
|
13
|
+
- **Parallel dispatch groups** — Lint runners now execute in parallel across independent groups
|
|
14
|
+
(e.g. `lsp`, `tree-sitter`, `ast-grep-napi`, `type-safety`, `similarity` all fire at once).
|
|
15
|
+
Typical wall-clock savings: 500–1500ms per file write (`parallelGainMs` logged in latency log).
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
- **`semantic: "none"` when 0 diagnostics** — LSP, Pyright, and type-safety runners were returning
|
|
19
|
+
`semantic: "warning"` even when `diagnosticCount` was 0 (clean file). Now correctly returns
|
|
20
|
+
`"none"` when no diagnostics are present, `"warning"` when warnings exist, `"blocking"` on errors.
|
|
21
|
+
|
|
22
|
+
- **`ast_grep_replace` with `apply=true` not writing files** — Replaced tool was silently
|
|
23
|
+
discarding the rewritten content instead of persisting it to disk.
|
|
24
|
+
|
|
25
|
+
- **Pipeline event loop blocked during test execution** — `spawnSync` in the test runner was
|
|
26
|
+
blocking the Node.js event loop for the duration of the test run. Switched to async spawn.
|
|
27
|
+
|
|
28
|
+
- **Formatters: venv/vendor/node_modules awareness** — Formatters now skip files inside virtual
|
|
29
|
+
environments, vendor directories, and `node_modules` instead of attempting to format them.
|
|
30
|
+
CSharpier detection also improved.
|
|
31
|
+
|
|
32
|
+
- **Formatter nearest-wins resolution** — When multiple formatter configs exist at different
|
|
33
|
+
directory levels, the one closest to the edited file is now used (was previously using the
|
|
34
|
+
root-level config regardless of nesting).
|
|
35
|
+
|
|
36
|
+
- **Prettier auto-install** — Prettier is now auto-installed when detected as the project
|
|
37
|
+
formatter but not present, consistent with the Biome/Ruff auto-install behaviour.
|
|
38
|
+
|
|
39
|
+
- **6 missing formatters added** — `clang-format` (C/C++/ObjC), `ktlint` (Kotlin), `scalafmt`
|
|
40
|
+
(Scala), `mix format` (Elixir), `dart format` (Dart), `terraform fmt` (HCL) now detected
|
|
41
|
+
and invoked automatically.
|
|
42
|
+
|
|
43
|
+
- **LSP tier-4 install prompts** — Corrected missing interactive-install prompts for tier-4
|
|
44
|
+
language servers (less common languages). Users now see the install suggestion instead of a
|
|
45
|
+
silent skip.
|
|
46
|
+
|
|
47
|
+
### Changed
|
|
48
|
+
- **`startedAt` added to latency log runner entries** — Every runner entry now records when it
|
|
49
|
+
started, making wall-clock vs. sequential comparisons accurate. `dispatch_complete` also logs
|
|
50
|
+
`parallelGainMs = sumMs - wallClockMs` to quantify parallelism benefit.
|
|
51
|
+
|
|
52
|
+
- **Dynamic imports removed from hot path** — Dispatch module no longer uses `await import()`
|
|
53
|
+
for runner loading; all imports are static, eliminating ~50ms warm-up latency on first dispatch.
|
|
54
|
+
|
|
55
|
+
### Tests
|
|
56
|
+
- Added formatter venv/vendor resolution and interactive-install coverage
|
|
57
|
+
- Added LSP lifecycle test suite with mock LSP server (process spawn, open/change/close, shutdown)
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
## [3.6.7] - 2026-04-04
|
|
62
|
+
|
|
63
|
+
### Fixed
|
|
64
|
+
- **LSP `ERR_STREAM_DESTROYED` crash** — When an LSP process (e.g. rust-analyzer) exits, Node.js emits
|
|
65
|
+
`'error'` events on the destroyed stdio streams. Without listeners these became uncaught exceptions
|
|
66
|
+
that crashed the extension. Added persistent `error` listeners to `stdin`, `stdout`, and `stderr`
|
|
67
|
+
before handing them to `vscode-jsonrpc`, covering the post-`connection.dispose()` window.
|
|
68
|
+
Same guard added to `NativeRustCoreClient` stdin writes.
|
|
69
|
+
|
|
70
|
+
### Added
|
|
71
|
+
- **Rust performance core (`pi-lens-core`)** — Optional Rust binary for CPU-intensive operations.
|
|
72
|
+
All features fall back to TypeScript automatically if the binary is not available (it is **not**
|
|
73
|
+
built automatically on `npm install` — run `npm run rust:build` once if you have Rust installed).
|
|
74
|
+
- **File scanning** — ripgrep’s `ignore` crate for `.gitignore`-aware project scanning
|
|
75
|
+
- **Similarity detection** — parallel 57×72 state-matrix index, persisted to
|
|
76
|
+
`.pi-lens/rust-index.json` between invocations (fixes in-memory cache that reset on every
|
|
77
|
+
process spawn)
|
|
78
|
+
- **Tree-sitter queries** — TypeScript and Rust AST queries via the binary
|
|
79
|
+
- **`NativeRustCoreClient`** — TypeScript wrapper with `isBinaryStale()` freshness detection,
|
|
80
|
+
JSON-IPC over stdin/stdout
|
|
81
|
+
- **Integration tests** — `npm run rust:test:integration` (37 assertions across all commands)
|
|
82
|
+
|
|
83
|
+
- **Rust similarity fast-path in dispatch runner** — `similarity.ts` now tries the Rust binary
|
|
84
|
+
first (scan → build index → query), falls through to the TypeScript implementation on any
|
|
85
|
+
failure. Feature flag `USE_RUST = true` at top of file.
|
|
86
|
+
|
|
87
|
+
### Changed
|
|
88
|
+
- **Similarity threshold raised from 0.75 → 0.90** — Empirical evaluation showed that below 0.90
|
|
89
|
+
false positives (structurally similar but semantically unrelated functions) outnumber true
|
|
90
|
+
positives with the current 57×72 matrix resolution. Applies to both the dispatch runner and
|
|
91
|
+
`/lens-booboo`.
|
|
92
|
+
|
|
93
|
+
- **Rust `kind_id` mapping improved** — Replaced `kind % dim` modulo (caused up to 4 unrelated
|
|
94
|
+
node types to share one matrix slot) with even-distribution across named slots plus a dedicated
|
|
95
|
+
last slot for anonymous punctuation tokens. Max named-slot collisions reduced from 4 to 3;
|
|
96
|
+
unnamed tokens no longer pollute named slots.
|
|
97
|
+
|
|
98
|
+
### Fixed (Rust)
|
|
99
|
+
- `tree_sitter_rust::language_rust()` → `language()` (correct API for tree-sitter-rust 0.21)
|
|
100
|
+
- `FunctionInfo` missing `#[derive(Clone)]` — caused compile error in `find_similar_to`
|
|
101
|
+
- `export function foo()` was missed by the index builder — TypeScript wraps exported functions
|
|
102
|
+
in `export_statement`; replaced flat top-level walk with recursive `collect_functions()`
|
|
103
|
+
- `find_similar_to` returned only the first function in a file — changed `find` to `filter`
|
|
104
|
+
- `tempfile` moved from `[dependencies]` to `[dev-dependencies]`
|
|
105
|
+
- Deleted orphan `test_lsp.rs` (intentional type errors caused rust-analyzer to crash the LSP stream)
|
|
106
|
+
|
|
107
|
+
### Repository
|
|
108
|
+
- Rust source (`rust/src/`, `rust/Cargo.toml`) added to npm `files` whitelist so users can build
|
|
109
|
+
the binary from an npm-installed package
|
|
110
|
+
- Removed stale `src/main.rs` rule from root `.gitignore` (no such file at repo root)
|
|
111
|
+
- Untracked `docs/plans/2025-04-03-auto-install-logging.md` (committed before `*.md` exclusion rule)
|
|
112
|
+
|
|
113
|
+
---
|
|
114
|
+
|
|
5
115
|
## [3.6.3] - 2026-04-03
|
|
6
116
|
|
|
7
117
|
### 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 (
|
|
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:
|
|
@@ -96,21 +96,43 @@ export class AstGrepClient {
|
|
|
96
96
|
paths: string[],
|
|
97
97
|
apply = false,
|
|
98
98
|
): Promise<{ matches: AstGrepMatch[]; applied: boolean; error?: string }> {
|
|
99
|
-
const
|
|
99
|
+
const baseArgs = ["run", "-p", pattern, "-r", rewrite, "--lang", lang];
|
|
100
|
+
|
|
101
|
+
if (!apply) {
|
|
102
|
+
// Dry-run: --json=compact shows what would change without writing
|
|
103
|
+
const result = await this.runner.exec([
|
|
104
|
+
...baseArgs,
|
|
105
|
+
"--json=compact",
|
|
106
|
+
...paths,
|
|
107
|
+
]);
|
|
108
|
+
return { matches: result.matches, applied: false, error: result.error };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Apply: --update-all and --json are MUTUALLY EXCLUSIVE in sg.
|
|
112
|
+
// Run twice:
|
|
113
|
+
// 1. --update-all to actually write the files
|
|
114
|
+
// 2. --json=compact (without rewrite) to collect matches for display
|
|
115
|
+
const applyResult = await this.runner.exec([
|
|
116
|
+
...baseArgs,
|
|
117
|
+
"--update-all",
|
|
118
|
+
...paths,
|
|
119
|
+
]);
|
|
120
|
+
if (applyResult.error) {
|
|
121
|
+
return { matches: [], applied: false, error: applyResult.error };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Search for what was changed (pattern no longer matches after rewrite,
|
|
125
|
+
// so search for the rewrite pattern to show what was applied)
|
|
126
|
+
const searchResult = await this.runner.exec([
|
|
100
127
|
"run",
|
|
101
128
|
"-p",
|
|
102
|
-
pattern,
|
|
103
|
-
"-r",
|
|
104
129
|
rewrite,
|
|
105
130
|
"--lang",
|
|
106
131
|
lang,
|
|
107
132
|
"--json=compact",
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
const result = await this.runner.exec(args);
|
|
113
|
-
return { matches: result.matches, applied: apply, error: result.error };
|
|
133
|
+
...paths,
|
|
134
|
+
]);
|
|
135
|
+
return { matches: searchResult.matches, applied: true, error: undefined };
|
|
114
136
|
}
|
|
115
137
|
|
|
116
138
|
/**
|
|
@@ -254,6 +254,135 @@ export function formatLatencyReport(report: DispatchLatencyReport): string {
|
|
|
254
254
|
return lines.join("\n");
|
|
255
255
|
}
|
|
256
256
|
|
|
257
|
+
// --- Group runner (used by dispatchForFile for parallel execution) ---
|
|
258
|
+
|
|
259
|
+
interface GroupResult {
|
|
260
|
+
diagnostics: Diagnostic[];
|
|
261
|
+
latencies: RunnerLatency[];
|
|
262
|
+
hadBlocker: boolean;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Execute all runners in a single group.
|
|
267
|
+
*
|
|
268
|
+
* - mode "fallback": run runners sequentially and stop at the first
|
|
269
|
+
* one that succeeds (returns status !== "skipped").
|
|
270
|
+
* - mode "all" (default): run all runners in the group sequentially
|
|
271
|
+
* and collect every diagnostic.
|
|
272
|
+
*
|
|
273
|
+
* Groups themselves are run in parallel by dispatchForFile, so this
|
|
274
|
+
* function must NOT mutate shared state.
|
|
275
|
+
*/
|
|
276
|
+
async function runGroup(
|
|
277
|
+
ctx: DispatchContext,
|
|
278
|
+
group: RunnerGroup,
|
|
279
|
+
): Promise<GroupResult> {
|
|
280
|
+
const diagnostics: Diagnostic[] = [];
|
|
281
|
+
const latencies: RunnerLatency[] = [];
|
|
282
|
+
let hadBlocker = false;
|
|
283
|
+
|
|
284
|
+
// Filter runners by kind if specified
|
|
285
|
+
const runnerIds = group.filterKinds
|
|
286
|
+
? group.runnerIds.filter((id) => {
|
|
287
|
+
const runner = getRunner(id);
|
|
288
|
+
return runner && ctx.kind && group.filterKinds?.includes(ctx.kind);
|
|
289
|
+
})
|
|
290
|
+
: group.runnerIds;
|
|
291
|
+
|
|
292
|
+
const semantic = group.semantic ?? "warning";
|
|
293
|
+
|
|
294
|
+
for (const runnerId of runnerIds) {
|
|
295
|
+
const runnerStart = Date.now();
|
|
296
|
+
const runner = getRunner(runnerId);
|
|
297
|
+
|
|
298
|
+
if (!runner) {
|
|
299
|
+
latencies.push({
|
|
300
|
+
runnerId,
|
|
301
|
+
startTime: runnerStart,
|
|
302
|
+
endTime: Date.now(),
|
|
303
|
+
durationMs: 0,
|
|
304
|
+
status: "skipped",
|
|
305
|
+
diagnosticCount: 0,
|
|
306
|
+
semantic: "unknown",
|
|
307
|
+
});
|
|
308
|
+
logLatency({
|
|
309
|
+
type: "runner",
|
|
310
|
+
filePath: ctx.filePath,
|
|
311
|
+
runnerId,
|
|
312
|
+
durationMs: 0,
|
|
313
|
+
status: "not_registered",
|
|
314
|
+
diagnosticCount: 0,
|
|
315
|
+
semantic: "unknown",
|
|
316
|
+
});
|
|
317
|
+
continue;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// Check preconditions
|
|
321
|
+
if (runner.when && !(await runner.when(ctx))) {
|
|
322
|
+
latencies.push({
|
|
323
|
+
runnerId,
|
|
324
|
+
startTime: runnerStart,
|
|
325
|
+
endTime: Date.now(),
|
|
326
|
+
durationMs: Date.now() - runnerStart,
|
|
327
|
+
status: "when_skipped",
|
|
328
|
+
diagnosticCount: 0,
|
|
329
|
+
semantic: runner.id,
|
|
330
|
+
});
|
|
331
|
+
logLatency({
|
|
332
|
+
type: "runner",
|
|
333
|
+
filePath: ctx.filePath,
|
|
334
|
+
runnerId,
|
|
335
|
+
durationMs: 0,
|
|
336
|
+
status: "when_skipped",
|
|
337
|
+
diagnosticCount: 0,
|
|
338
|
+
semantic: "when_condition",
|
|
339
|
+
});
|
|
340
|
+
continue;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
const result = await runRunner(ctx, runner, semantic);
|
|
344
|
+
const runnerEnd = Date.now();
|
|
345
|
+
const duration = runnerEnd - runnerStart;
|
|
346
|
+
|
|
347
|
+
latencies.push({
|
|
348
|
+
runnerId,
|
|
349
|
+
startTime: runnerStart,
|
|
350
|
+
endTime: runnerEnd,
|
|
351
|
+
durationMs: duration,
|
|
352
|
+
status: result.status,
|
|
353
|
+
diagnosticCount: result.diagnostics.length,
|
|
354
|
+
semantic: result.semantic ?? semantic,
|
|
355
|
+
});
|
|
356
|
+
logLatency({
|
|
357
|
+
type: "runner",
|
|
358
|
+
filePath: ctx.filePath,
|
|
359
|
+
runnerId,
|
|
360
|
+
startedAt: new Date(runnerStart).toISOString(),
|
|
361
|
+
durationMs: duration,
|
|
362
|
+
status: result.status,
|
|
363
|
+
diagnosticCount: result.diagnostics.length,
|
|
364
|
+
semantic: result.semantic ?? semantic,
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
diagnostics.push(...result.diagnostics);
|
|
368
|
+
|
|
369
|
+
const resultSemantic = result.semantic ?? semantic;
|
|
370
|
+
if (
|
|
371
|
+
(resultSemantic === "blocking" && result.diagnostics.length > 0) ||
|
|
372
|
+
result.diagnostics.some((d) => d.semantic === "blocking")
|
|
373
|
+
) {
|
|
374
|
+
hadBlocker = true;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// mode:"fallback" — stop at first runner that produced results
|
|
378
|
+
if (group.mode === "fallback" && result.status !== "skipped") {
|
|
379
|
+
break;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
return { diagnostics, latencies, hadBlocker };
|
|
384
|
+
}
|
|
385
|
+
|
|
257
386
|
// --- Main Dispatch Function ---
|
|
258
387
|
|
|
259
388
|
export async function dispatchForFile(
|
|
@@ -280,124 +409,38 @@ export async function dispatchForFile(
|
|
|
280
409
|
},
|
|
281
410
|
});
|
|
282
411
|
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
? group.runnerIds.filter((id) => {
|
|
291
|
-
const runner = getRunner(id);
|
|
292
|
-
return runner && ctx.kind && group.filterKinds?.includes(ctx.kind);
|
|
293
|
-
})
|
|
294
|
-
: group.runnerIds;
|
|
295
|
-
|
|
296
|
-
const semantic = group.semantic ?? "warning";
|
|
297
|
-
|
|
298
|
-
for (const runnerId of runnerIds) {
|
|
299
|
-
const runnerStart = Date.now();
|
|
300
|
-
const runner = getRunner(runnerId);
|
|
301
|
-
if (!runner) {
|
|
302
|
-
runnerLatencies.push({
|
|
303
|
-
runnerId,
|
|
304
|
-
startTime: runnerStart,
|
|
305
|
-
endTime: Date.now(),
|
|
306
|
-
durationMs: 0,
|
|
307
|
-
status: "skipped",
|
|
308
|
-
diagnosticCount: 0,
|
|
309
|
-
semantic: "unknown",
|
|
310
|
-
});
|
|
311
|
-
logLatency({
|
|
312
|
-
type: "runner",
|
|
313
|
-
filePath: ctx.filePath,
|
|
314
|
-
runnerId,
|
|
315
|
-
durationMs: 0,
|
|
316
|
-
status: "not_registered",
|
|
317
|
-
diagnosticCount: 0,
|
|
318
|
-
semantic: "unknown",
|
|
319
|
-
});
|
|
320
|
-
continue;
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
// Check preconditions
|
|
324
|
-
if (runner.when && !(await runner.when(ctx))) {
|
|
325
|
-
runnerLatencies.push({
|
|
326
|
-
runnerId,
|
|
327
|
-
startTime: runnerStart,
|
|
328
|
-
endTime: Date.now(),
|
|
329
|
-
durationMs: Date.now() - runnerStart,
|
|
330
|
-
status: "when_skipped",
|
|
331
|
-
diagnosticCount: 0,
|
|
332
|
-
semantic: runner.id,
|
|
333
|
-
});
|
|
334
|
-
logLatency({
|
|
335
|
-
type: "runner",
|
|
336
|
-
filePath: ctx.filePath,
|
|
337
|
-
runnerId,
|
|
338
|
-
durationMs: 0,
|
|
339
|
-
status: "when_skipped",
|
|
340
|
-
diagnosticCount: 0,
|
|
341
|
-
semantic: "when_condition",
|
|
342
|
-
});
|
|
343
|
-
continue;
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
const result = await runRunner(ctx, runner, semantic);
|
|
347
|
-
const runnerEnd = Date.now();
|
|
348
|
-
const duration = runnerEnd - runnerStart;
|
|
349
|
-
|
|
350
|
-
// Track latency for this runner
|
|
351
|
-
runnerLatencies.push({
|
|
352
|
-
runnerId,
|
|
353
|
-
startTime: runnerStart,
|
|
354
|
-
endTime: runnerEnd,
|
|
355
|
-
durationMs: duration,
|
|
356
|
-
status: result.status,
|
|
357
|
-
diagnosticCount: result.diagnostics.length,
|
|
358
|
-
semantic: result.semantic ?? semantic,
|
|
359
|
-
});
|
|
360
|
-
|
|
361
|
-
// IMMEDIATE LOG: Each runner result (for debugging)
|
|
362
|
-
logLatency({
|
|
363
|
-
type: "runner",
|
|
364
|
-
filePath: ctx.filePath,
|
|
365
|
-
runnerId,
|
|
366
|
-
durationMs: duration,
|
|
367
|
-
status: result.status,
|
|
368
|
-
diagnosticCount: result.diagnostics.length,
|
|
369
|
-
semantic: result.semantic ?? semantic,
|
|
370
|
-
});
|
|
371
|
-
|
|
372
|
-
// Apply delta mode filtering
|
|
373
|
-
let diagnostics = result.diagnostics;
|
|
374
|
-
if (ctx.deltaMode && result.semantic !== "silent") {
|
|
375
|
-
const before = ctx.baselines.get(ctx.filePath);
|
|
376
|
-
if (before) {
|
|
377
|
-
const filtered = filterDelta(
|
|
378
|
-
diagnostics,
|
|
379
|
-
before as Diagnostic[],
|
|
380
|
-
(d) => d.id,
|
|
381
|
-
);
|
|
382
|
-
diagnostics = filtered.new;
|
|
383
|
-
// TODO: Track fixed diagnostics
|
|
384
|
-
}
|
|
385
|
-
// Update baseline
|
|
386
|
-
ctx.baselines.set(ctx.filePath, [...allDiagnostics, ...diagnostics]);
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
allDiagnostics.push(...diagnostics);
|
|
412
|
+
// Run all groups in parallel — they are independent and don't depend on
|
|
413
|
+
// each other's results. Within each group, mode:"fallback" semantics are
|
|
414
|
+
// preserved (sequential first-success). Results are merged in original
|
|
415
|
+
// group order so output is deterministic.
|
|
416
|
+
const groupResults = await Promise.all(
|
|
417
|
+
groups.map((group) => runGroup(ctx, group)),
|
|
418
|
+
);
|
|
390
419
|
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
420
|
+
for (const {
|
|
421
|
+
diagnostics: groupDiags,
|
|
422
|
+
latencies,
|
|
423
|
+
hadBlocker,
|
|
424
|
+
} of groupResults) {
|
|
425
|
+
runnerLatencies.push(...latencies);
|
|
426
|
+
|
|
427
|
+
// Apply delta mode filtering across the accumulated set
|
|
428
|
+
let diagnostics = groupDiags;
|
|
429
|
+
if (ctx.deltaMode) {
|
|
430
|
+
const before = ctx.baselines.get(ctx.filePath);
|
|
431
|
+
if (before) {
|
|
432
|
+
const filtered = filterDelta(
|
|
433
|
+
diagnostics,
|
|
434
|
+
before as Diagnostic[],
|
|
435
|
+
(d) => d.id,
|
|
436
|
+
);
|
|
437
|
+
diagnostics = filtered.new;
|
|
399
438
|
}
|
|
439
|
+
ctx.baselines.set(ctx.filePath, [...allDiagnostics, ...diagnostics]);
|
|
400
440
|
}
|
|
441
|
+
|
|
442
|
+
allDiagnostics.push(...diagnostics);
|
|
443
|
+
if (hadBlocker) stopped = true;
|
|
401
444
|
}
|
|
402
445
|
|
|
403
446
|
// Categorize results
|
|
@@ -440,14 +483,20 @@ export async function dispatchForFile(
|
|
|
440
483
|
// No need to log again here - would create duplicates in the log
|
|
441
484
|
|
|
442
485
|
// Log summary to latency log only (not console - avoid noise)
|
|
486
|
+
const sumMs = runnerLatencies.reduce((s, r) => s + r.durationMs, 0);
|
|
487
|
+
const wallClockMs = latencyReport.totalDurationMs;
|
|
443
488
|
logLatency({
|
|
444
489
|
type: "tool_result",
|
|
445
490
|
filePath: ctx.filePath,
|
|
446
|
-
durationMs:
|
|
491
|
+
durationMs: wallClockMs,
|
|
492
|
+
wallClockMs,
|
|
493
|
+
sumMs,
|
|
494
|
+
parallelGainMs: Math.max(0, sumMs - wallClockMs),
|
|
447
495
|
result: "dispatch_complete",
|
|
448
496
|
metadata: {
|
|
449
497
|
runners: runnerLatencies.map((r) => ({
|
|
450
498
|
id: r.runnerId,
|
|
499
|
+
startedAt: new Date(r.startTime).toISOString(),
|
|
451
500
|
duration: r.durationMs,
|
|
452
501
|
status: r.status,
|
|
453
502
|
})),
|
|
@@ -11,10 +11,13 @@ import {
|
|
|
11
11
|
createBaselineStore,
|
|
12
12
|
createDispatchContext,
|
|
13
13
|
type DispatchLatencyReport,
|
|
14
|
+
dispatchForFile,
|
|
14
15
|
formatLatencyReport,
|
|
15
16
|
getLatencyReports,
|
|
17
|
+
getRunnersForKind,
|
|
16
18
|
type RunnerLatency,
|
|
17
19
|
} from "./dispatcher.js";
|
|
20
|
+
import { TOOL_PLANS } from "./plan.js";
|
|
18
21
|
import type { BaselineStore, DispatchResult, PiAgentAPI } from "./types.js";
|
|
19
22
|
|
|
20
23
|
export type { DispatchLatencyReport, RunnerLatency };
|
|
@@ -57,11 +60,6 @@ export async function dispatchLint(
|
|
|
57
60
|
// pre-existing issues after the first write.
|
|
58
61
|
const ctx = createDispatchContext(filePath, cwd, pi, sessionBaselines, true);
|
|
59
62
|
|
|
60
|
-
// Import dispatchForFile dynamically to avoid circular deps
|
|
61
|
-
const { dispatchForFile } = await import("./dispatcher.js");
|
|
62
|
-
const { getRunnersForKind } = await import("./dispatcher.js");
|
|
63
|
-
const { TOOL_PLANS } = await import("./plan.js");
|
|
64
|
-
|
|
65
63
|
const kind = ctx.kind;
|
|
66
64
|
if (!kind) return "";
|
|
67
65
|
|
|
@@ -82,10 +80,6 @@ export async function dispatchLintWithResult(
|
|
|
82
80
|
): Promise<DispatchResult> {
|
|
83
81
|
const ctx = createDispatchContext(filePath, cwd, pi, sessionBaselines, true);
|
|
84
82
|
|
|
85
|
-
const { dispatchForFile } = await import("./dispatcher.js");
|
|
86
|
-
const { getRunnersForKind } = await import("./dispatcher.js");
|
|
87
|
-
const { TOOL_PLANS } = await import("./plan.js");
|
|
88
|
-
|
|
89
83
|
const kind = ctx.kind;
|
|
90
84
|
if (!kind) {
|
|
91
85
|
return {
|
|
@@ -110,7 +104,7 @@ export async function dispatchLintWithResult(
|
|
|
110
104
|
};
|
|
111
105
|
}
|
|
112
106
|
|
|
113
|
-
return
|
|
107
|
+
return dispatchForFile(ctx, plan.groups);
|
|
114
108
|
}
|
|
115
109
|
|
|
116
110
|
/**
|
|
@@ -136,7 +130,6 @@ export async function getAvailableRunners(filePath: string): Promise<string[]> {
|
|
|
136
130
|
const kind = detectFileKind(filePath);
|
|
137
131
|
if (!kind) return [];
|
|
138
132
|
|
|
139
|
-
const { getRunnersForKind } = await import("./dispatcher.js");
|
|
140
133
|
const runners = getRunnersForKind(kind);
|
|
141
134
|
return runners.map((r) => r.id);
|
|
142
135
|
}
|
|
@@ -117,7 +117,11 @@ const lspRunner: RunnerDefinition = {
|
|
|
117
117
|
return {
|
|
118
118
|
status: hasErrors ? "failed" : "succeeded",
|
|
119
119
|
diagnostics,
|
|
120
|
-
semantic: hasErrors
|
|
120
|
+
semantic: hasErrors
|
|
121
|
+
? "blocking"
|
|
122
|
+
: diagnostics.length > 0
|
|
123
|
+
? "warning"
|
|
124
|
+
: "none",
|
|
121
125
|
};
|
|
122
126
|
},
|
|
123
127
|
};
|
|
@@ -82,7 +82,11 @@ const pyrightRunner: RunnerDefinition = {
|
|
|
82
82
|
return {
|
|
83
83
|
status: hasErrors ? "failed" : "succeeded",
|
|
84
84
|
diagnostics,
|
|
85
|
-
semantic: hasErrors
|
|
85
|
+
semantic: hasErrors
|
|
86
|
+
? "blocking"
|
|
87
|
+
: diagnostics.length > 0
|
|
88
|
+
? "warning"
|
|
89
|
+
: "none",
|
|
86
90
|
};
|
|
87
91
|
} catch {
|
|
88
92
|
// JSON parse error
|