pi-lens 3.8.72 → 3.8.73

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -10,6 +10,50 @@ All notable changes to pi-lens will be documented in this file.
10
10
 
11
11
  ### Fixed
12
12
 
13
+ ## [3.8.73] - 2026-07-28
14
+
15
+ ### Added
16
+
17
+ ### Changed
18
+
19
+ - **`@types/js-yaml` removed; js-yaml 5 migration complete** (closes #391) — with
20
+ js-yaml 5.2.2 in (#843) the package ships its own declarations, so the stub
21
+ types dev-dependency is gone and the lockfile regenerated. This completes the
22
+ v5 migration started with the namespace-import compatibility pass below.
23
+
24
+ - **`js-yaml` imports are compatible with v5** (refs #391, #843) — use the
25
+ package's namespace exports so the central accessor and YAML rule tooling work
26
+ with both the current v4 dependency and the upcoming v5 release. `@types/js-yaml`
27
+ remains temporarily because js-yaml 4.3.0 needs its declarations; remove it
28
+ with the v5 Dependabot upgrade once the bundled declarations are active.
29
+
30
+ - **`build:dist` now compiles with TypeScript 7.0.2** (refs #809) — the dist emit pin moves from `typescript@6` to the same major as the dev dependency (#600); emitted output was verified byte-identical across all 328 files before the bump.
31
+
32
+ ### Fixed
33
+
34
+ - **ast-grep Gate-B now follows LSP liveness and launcher resolution** (refs #868) — the NAPI fallback no longer runs beside a healthy ast-grep LSP when the bundled native binary is resolvable but `ast-grep` is absent from PATH; it still engages when neither resolution path nor a live client is available.
35
+
36
+ - **NAPI fallback unsupported-rule skips no longer spam the session log** (refs #282) — the per-rule terminal lines (one for each of the ~30 non-jsts catalog rules) are replaced by a single aggregated `astgrep_napi_unsupported_rules_skipped` entry in `latency.log` carrying per-language counts and rule ids.
37
+
38
+ - **Missing tree-sitter defect-class metadata is now covered** (refs #863) — loop-termination and `finally` control-flow rules now declare their taxonomy classes, keeping the rule audit complete.
39
+
40
+ - **MCP smoke deadlines scale under CI load (refs #860)** — the shared MCP
41
+ test harness keeps its 20s local default, accepts the
42
+ `PI_LENS_TEST_TIMEOUT_SCALE` multiplier, and the analyze-graph smoke pays
43
+ server startup through a throwaway health call before its assertions.
44
+
45
+ - **TypeScript 7 compatibility fixes (refs #809)** — narrowed ast-grep search test details and made the LSP `workspace/applyEdit` response overload-compatible, unblocking Dependabot PR #600.
46
+
47
+ - **ast-grep NAPI fallback now runs TSX-tagged rules and reports unsupported-language skips** (refs #282) — `.tsx` files use the `tsx` grammar for rule-language scoping, preserving the exact TypeScript/JavaScript twin behavior from #657; rules for languages the fallback cannot evaluate are logged once per rule instead of disappearing as zero matches.
48
+
49
+ - **CSS server root-policy coverage is hermetic on Windows** (refs #855) — the
50
+ server-policy fixture now pins its nearest `package.json` inside the temporary
51
+ workspace, so a marker in the host user's profile cannot change the result.
52
+
53
+ - **Multi-client LSP shutdown no longer serializes the grace-period tail** (#851) — retiring clients now tear down concurrently while generation handoff still waits for every client to settle; instance-registry child removals serialize their read-modify-write without serializing process kills.
54
+
55
+ - **LSP singleton resets no longer overlap live server generations** (closes #850) — `resetLSPService()` still returns synchronously and may allocate a replacement service immediately, but that replacement's first spawn now waits for every older generation's asynchronous teardown to settle. Repeated resets destroy an intermediate waiting generation before it can spawn, and post-root/post-client-cleanup guards prevent a retired service from starting a late process outside `state.inFlight`. The handoff is cleared after its first wait, preserving normal within-generation warm reuse and its hot path; concurrent-secondary sessions still skip destructive reset through the existing ownership guard. New deterministic coverage holds the old client's shutdown open and proves current `master`'s pre-fix second spawn/three-generation accumulation cannot recur.
56
+
13
57
  ## [3.8.72] - 2026-07-26
14
58
 
15
59
  ### Added
@@ -42,6 +86,8 @@ All notable changes to pi-lens will be documented in this file.
42
86
 
43
87
  ### Changed
44
88
 
89
+ - **Explicit `lsp_diagnostics` file batches now expose a consolidated contract** (refs #837) — runtime path lists are normalized and rejected above the 100-file cap (never silently truncated), preserve input order, enforce bounded per-file deadlines and abort-aware completion, and return honest per-file outcomes (`clean`, `findings`, `unsupported`, `unavailable`, `failed`, `inconclusive`) with aggregate counts. Primary-language diagnostics remain separated from auxiliary findings, and incomplete batches are rendered as unconfirmed.
90
+
45
91
  - **Incumbent LSP wait policy now has one process-neutral module boundary** (#822) — per-server strategies, tier classification, and capability-snapshot types moved under `clients/lsp/wait-policy/` with compatibility re-exports at the old paths; this is an internal refactor with no behavior change.
46
92
 
47
93
  - **Review-graph file budget now tapers instead of scaling flatly or cliff-dropping above 1,000 files** (refs #775 R1) — `project-scale.ts`'s `getReviewGraphMaxFilesDerived` keeps today's exact 0.5× `maxProjectFiles` ratio (1,000 files at the 2,000 default base) unchanged for every project at or below a 4,000-file `maxProjectFiles` base, then tapers growth asymptotically toward a 5,000-file hard ceiling for larger bases instead of continuing linearly forever. The ceiling is bound FIRST by the review graph's own disk-persist circuit-breaker (`GRAPH_PERSIST_MAX_ELEMENTS_DEFAULT` = 200,000 graph elements in `review-graph/builder.ts` — above it the graph is never persisted, logged + skipped, so a repo over that line pays a full cold build every session instead of once): at ~30 elements/file for a symbol-dense repo, 5,000 files ≈ 150,000 elements, a ~25% margin below the persist cap (a 6,000-file ceiling considered during review left only ~10% margin — too little for a denser-than-average repo). SECOND and independently, it's consistent with the graph's measured per-file build cost (CHANGELOG 3.8.43: "2-4 s" full cold build at the then-1,000-file cap, ~2-4 ms/file; 5,000 files ≈ 10-20 s worst-case, one-time and disk-cached thereafter). New `.pi-lens.json#reviewGraph.maxFiles` knob (R2) lets a monorepo opt into a bigger graph explicitly, bypassing the taper (tolerant numeric parse, clamped to `[100, 20_000]`, warn-once on invalid — mirrors `maxProjectFiles`'s parsing); the pre-existing `PI_LENS_REVIEW_GRAPH_MAX_FILES` env override still wins outright over both. Truncation (files found over the cap) now always logs a `review_graph_size_skip` latency phase directly from `review-graph/builder.ts`'s skip branch (R3), independent of which caller triggered the build, instead of relying on `dispatch/integration.ts`'s cascade-only `graph_build` log — closing a silent-cap gap for the other four `buildOrUpdateGraph` call sites (`lens-map.ts`, `project-report.ts`, `mcp/analyze.ts`, `runtime-session.ts`, `dispatch/runners/tree-sitter.ts`). Documented in `docs/globalconfig.md`'s project-config section. New tests: `taperedReviewGraphMaxFiles` shape (below/at/above the linear boundary, half-gap-at-taper-scale property, ceiling asymptote, monotonicity), the config knob's parse/clamp/warn-once behavior, and the new latency phase.
package/README.md CHANGED
@@ -79,8 +79,8 @@ Thanks goes to these wonderful people:
79
79
  <!-- prettier-ignore-start -->
80
80
  <!-- markdownlint-disable -->
81
81
  <table>
82
- <tbody>
83
- <tr>
82
+ <tbody>
83
+ <tr>
84
84
  <td align="center" valign="top" width="14.28%"><a href="https://github.com/apmantza"><img src="https://avatars.githubusercontent.com/u/247365598?v=4" width="100px;" alt=""/><br /><sub><b>Apostolos Mantzaris</b></sub></a><br /><a href="#code-apmantza" title="Code">💻</a> <a href="#doc-apmantza" title="Documentation">📖</a> <a href="#ideas-apmantza" title="Ideas & Planning">🤔</a> <a href="#maintenance-apmantza" title="Maintenance">🚧</a> <a href="#review-apmantza" title="Reviewed Pull Requests">👀</a></td>
85
85
  <td align="center" valign="top" width="14.28%"><a href="https://github.com/anh-chu"><img src="https://avatars.githubusercontent.com/u/34973633?v=4" width="100px;" alt=""/><br /><sub><b>Anh Chu</b></sub></a><br /><a href="#code-anh-chu" title="Code">💻</a></td>
86
86
  <td align="center" valign="top" width="14.28%"><a href="https://github.com/loss-and-quick"><img src="https://avatars.githubusercontent.com/u/39405619?v=4" width="100px;" alt=""/><br /><sub><b>minicx</b></sub></a><br /><a href="#code-loss-and-quick" title="Code">💻</a></td>
@@ -88,7 +88,7 @@ Thanks goes to these wonderful people:
88
88
  <td align="center" valign="top" width="14.28%"><a href="https://github.com/tifandotme"><img src="https://avatars.githubusercontent.com/u/33323177?v=4" width="100px;" alt=""/><br /><sub><b>Tifan Dwi Avianto</b></sub></a><br /><a href="#code-tifandotme" title="Code">💻</a></td>
89
89
  <td align="center" valign="top" width="14.28%"><a href="https://github.com/amit-gshe"><img src="https://avatars.githubusercontent.com/u/7383028?v=4" width="100px;" alt=""/><br /><sub><b>Amit</b></sub></a><br /><a href="#code-amit-gshe" title="Code">💻</a> <a href="#bug-amit-gshe" title="Bug reports">🐛</a></td>
90
90
  <td align="center" valign="top" width="14.28%"><a href="https://github.com/fractiunate"><img src="https://avatars.githubusercontent.com/u/78024279?v=4" width="100px;" alt=""/><br /><sub><b>Fractiunate // David Rahäuser</b></sub></a><br /><a href="#code-fractiunate" title="Code">💻</a></td>
91
- </tr><br />
91
+ </tr>
92
92
  <tr>
93
93
  <td align="center" valign="top" width="14.28%"><a href="https://github.com/vkarasen"><img src="https://avatars.githubusercontent.com/u/7932536?v=4" width="100px;" alt=""/><br /><sub><b>Vitali Karasenko</b></sub></a><br /><a href="#code-vkarasen" title="Code">💻</a></td>
94
94
  <td align="center" valign="top" width="14.28%"><a href="https://github.com/rfxlamia"><img src="https://avatars.githubusercontent.com/u/222023708?v=4" width="100px;" alt=""/><br /><sub><b>V</b></sub></a><br /><a href="#code-rfxlamia" title="Code">💻</a></td>
@@ -97,7 +97,7 @@ Thanks goes to these wonderful people:
97
97
  <td align="center" valign="top" width="14.28%"><a href="https://github.com/mehalter"><img src="https://avatars.githubusercontent.com/u/1591837?v=4" width="100px;" alt=""/><br /><sub><b>Micah Halter</b></sub></a><br /><a href="#code-mehalter" title="Code">💻</a></td>
98
98
  <td align="center" valign="top" width="14.28%"><a href="https://github.com/JulienEllie"><img src="https://avatars.githubusercontent.com/u/670518?v=4" width="100px;" alt=""/><br /><sub><b>Julien Ellie</b></sub></a><br /><a href="#code-JulienEllie" title="Code">💻</a></td>
99
99
  <td align="center" valign="top" width="14.28%"><a href="https://github.com/Istar-Eldritch"><img src="https://avatars.githubusercontent.com/u/3746468?v=4" width="100px;" alt=""/><br /><sub><b>Ruben Paz</b></sub></a><br /><a href="#code-Istar-Eldritch" title="Code">💻</a> <a href="#bug-Istar-Eldritch" title="Bug reports">🐛</a></td>
100
- </tr><br />
100
+ </tr>
101
101
  <tr>
102
102
  <td align="center" valign="top" width="14.28%"><a href="https://github.com/cjunxiang"><img src="https://avatars.githubusercontent.com/u/26619858?v=4" width="100px;" alt=""/><br /><sub><b>C.Junxiang</b></sub></a><br /><a href="#code-cjunxiang" title="Code">💻</a> <a href="#bug-cjunxiang" title="Bug reports">🐛</a></td>
103
103
  <td align="center" valign="top" width="14.28%"><a href="https://github.com/trvon"><img src="https://avatars.githubusercontent.com/u/6031322?v=4" width="100px;" alt=""/><br /><sub><b>Trevon</b></sub></a><br /><a href="#code-trvon" title="Code">💻</a></td>
@@ -106,7 +106,7 @@ Thanks goes to these wonderful people:
106
106
  <td align="center" valign="top" width="14.28%"><a href="https://github.com/spyrosbazios"><img src="https://avatars.githubusercontent.com/u/37960233?v=4" width="100px;" alt=""/><br /><sub><b>spyrosbazios</b></sub></a><br /><a href="#code-spyrosbazios" title="Code">💻</a></td>
107
107
  <td align="center" valign="top" width="14.28%"><a href="https://github.com/Roman-Galeev"><img src="https://avatars.githubusercontent.com/u/40388226?v=4" width="100px;" alt=""/><br /><sub><b>Roman Galeev</b></sub></a><br /><a href="#code-Roman-Galeev" title="Code">💻</a></td>
108
108
  <td align="center" valign="top" width="14.28%"><a href="https://github.com/jerryfan"><img src="https://avatars.githubusercontent.com/u/2540814?v=4" width="100px;" alt=""/><br /><sub><b>jerryfan</b></sub></a><br /><a href="#code-jerryfan" title="Code">💻</a></td>
109
- </tr><br />
109
+ </tr>
110
110
  <tr>
111
111
  <td align="center" valign="top" width="14.28%"><a href="https://github.com/shaDmx"><img src="https://avatars.githubusercontent.com/u/91132641?v=4" width="100px;" alt=""/><br /><sub><b>Max L.</b></sub></a><br /><a href="#code-shaDmx" title="Code">💻</a></td>
112
112
  <td align="center" valign="top" width="14.28%"><a href="https://github.com/radge"><img src="https://avatars.githubusercontent.com/u/129205?v=4" width="100px;" alt=""/><br /><sub><b>David Ryan</b></sub></a><br /><a href="#code-radge" title="Code">💻</a></td>
@@ -115,90 +115,91 @@ Thanks goes to these wonderful people:
115
115
  <td align="center" valign="top" width="14.28%"><a href="https://github.com/feoh"><img src="https://avatars.githubusercontent.com/u/330070?v=4" width="100px;" alt=""/><br /><sub><b>Chris Patti</b></sub></a><br /><a href="#code-feoh" title="Code">💻</a> <a href="#bug-feoh" title="Bug reports">🐛</a></td>
116
116
  <td align="center" valign="top" width="14.28%"><a href="https://github.com/chenxin-yan"><img src="https://avatars.githubusercontent.com/u/71162231?v=4" width="100px;" alt=""/><br /><sub><b>Chenxin Yan</b></sub></a><br /><a href="#code-chenxin-yan" title="Code">💻</a> <a href="#doc-chenxin-yan" title="Documentation">📖</a> <a href="#bug-chenxin-yan" title="Bug reports">🐛</a></td>
117
117
  <td align="center" valign="top" width="14.28%"><a href="https://github.com/flowing-abyss"><img src="https://avatars.githubusercontent.com/u/98622217?v=4" width="100px;" alt=""/><br /><sub><b>flowing-abyss</b></sub></a><br /><a href="#code-flowing-abyss" title="Code">💻</a></td>
118
- </tr><br />
118
+ </tr>
119
119
  <tr>
120
+ <td align="center" valign="top" width="14.28%"><a href="https://github.com/shaochong-theoai"><img src="https://avatars.githubusercontent.com/u/305592060?v=4" width="100px;" alt=""/><br /><sub><b>shaochong-theoai</b></sub></a><br /><a href="#code-shaochong-theoai" title="Code">💻</a></td>
120
121
  <td align="center" valign="top" width="14.28%"><a href="https://github.com/xuli500177"><img src="https://avatars.githubusercontent.com/u/62830942?v=4" width="100px;" alt=""/><br /><sub><b>Xu Yili</b></sub></a><br /><a href="#bug-xuli500177" title="Bug reports">🐛</a></td>
121
122
  <td align="center" valign="top" width="14.28%"><a href="https://github.com/ricardoraposo"><img src="https://avatars.githubusercontent.com/u/50217712?v=4" width="100px;" alt=""/><br /><sub><b>Ricardo Raposo</b></sub></a><br /><a href="#code-ricardoraposo" title="Code">💻</a></td>
122
123
  <td align="center" valign="top" width="14.28%"><a href="https://github.com/jsmestad"><img src="https://avatars.githubusercontent.com/u/16150?v=4" width="100px;" alt=""/><br /><sub><b>Justin Smestad</b></sub></a><br /><a href="#code-jsmestad" title="Code">💻</a></td>
123
124
  <td align="center" valign="top" width="14.28%"><a href="https://github.com/anasalsbey-glitch"><img src="https://avatars.githubusercontent.com/u/283609702?v=4" width="100px;" alt=""/><br /><sub><b>anasalsbey-glitch</b></sub></a><br /><a href="#code-anasalsbey-glitch" title="Code">💻</a> <a href="#bug-anasalsbey-glitch" title="Bug reports">🐛</a></td>
124
125
  <td align="center" valign="top" width="14.28%"><a href="https://github.com/PeraSite"><img src="https://avatars.githubusercontent.com/u/19837403?v=4" width="100px;" alt=""/><br /><sub><b>정제훈</b></sub></a><br /><a href="#code-PeraSite" title="Code">💻</a></td>
125
126
  <td align="center" valign="top" width="14.28%"><a href="https://github.com/carolitascl"><img src="https://avatars.githubusercontent.com/u/26188349?v=4" width="100px;" alt=""/><br /><sub><b>Carolina</b></sub></a><br /><a href="#bug-carolitascl" title="Bug reports">🐛</a></td>
126
- <td align="center" valign="top" width="14.28%"><a href="https://github.com/leakedby"><img src="https://avatars.githubusercontent.com/u/4213260?v=4" width="100px;" alt=""/><br /><sub><b>LeakedBy</b></sub></a><br /><a href="#bug-leakedby" title="Bug reports">🐛</a></td>
127
- </tr><br />
127
+ </tr>
128
128
  <tr>
129
+ <td align="center" valign="top" width="14.28%"><a href="https://github.com/leakedby"><img src="https://avatars.githubusercontent.com/u/4213260?v=4" width="100px;" alt=""/><br /><sub><b>LeakedBy</b></sub></a><br /><a href="#bug-leakedby" title="Bug reports">🐛</a></td>
129
130
  <td align="center" valign="top" width="14.28%"><a href="https://github.com/mdbooth"><img src="https://avatars.githubusercontent.com/u/1318691?v=4" width="100px;" alt=""/><br /><sub><b>Matthew Booth</b></sub></a><br /><a href="#bug-mdbooth" title="Bug reports">🐛</a></td>
130
131
  <td align="center" valign="top" width="14.28%"><a href="https://github.com/Wiedzmin89"><img src="https://avatars.githubusercontent.com/u/61706855?v=4" width="100px;" alt=""/><br /><sub><b>Wiedzmin89</b></sub></a><br /><a href="#ideas-Wiedzmin89" title="Ideas & Planning">🤔</a></td>
131
132
  <td align="center" valign="top" width="14.28%"><a href="https://github.com/grodingo"><img src="https://avatars.githubusercontent.com/u/244184972?v=4" width="100px;" alt=""/><br /><sub><b>Virgile</b></sub></a><br /><a href="#bug-grodingo" title="Bug reports">🐛</a></td>
132
133
  <td align="center" valign="top" width="14.28%"><a href="https://github.com/finnvyrn"><img src="https://avatars.githubusercontent.com/u/90801772?v=4" width="100px;" alt=""/><br /><sub><b>Finn</b></sub></a><br /><a href="#ideas-finnvyrn" title="Ideas & Planning">🤔</a></td>
133
134
  <td align="center" valign="top" width="14.28%"><a href="https://github.com/ywh555hhh"><img src="https://avatars.githubusercontent.com/u/121592812?v=4" width="100px;" alt=""/><br /><sub><b>Wayne E</b></sub></a><br /><a href="#bug-ywh555hhh" title="Bug reports">🐛</a></td>
134
135
  <td align="center" valign="top" width="14.28%"><a href="https://github.com/RimuruW"><img src="https://avatars.githubusercontent.com/u/59136309?v=4" width="100px;" alt=""/><br /><sub><b>RimuruW</b></sub></a><br /><a href="#ideas-RimuruW" title="Ideas & Planning">🤔</a></td>
135
- <td align="center" valign="top" width="14.28%"><a href="https://github.com/Bjynt"><img src="https://avatars.githubusercontent.com/u/22177300?v=4" width="100px;" alt=""/><br /><sub><b>Bjynt</b></sub></a><br /><a href="#bug-Bjynt" title="Bug reports">🐛</a> <a href="#ideas-Bjynt" title="Ideas & Planning">🤔</a></td>
136
- </tr><br />
136
+ </tr>
137
137
  <tr>
138
+ <td align="center" valign="top" width="14.28%"><a href="https://github.com/Bjynt"><img src="https://avatars.githubusercontent.com/u/22177300?v=4" width="100px;" alt=""/><br /><sub><b>Bjynt</b></sub></a><br /><a href="#bug-Bjynt" title="Bug reports">🐛</a> <a href="#ideas-Bjynt" title="Ideas & Planning">🤔</a></td>
138
139
  <td align="center" valign="top" width="14.28%"><a href="https://github.com/pvtri96"><img src="https://avatars.githubusercontent.com/u/28696888?v=4" width="100px;" alt=""/><br /><sub><b>Tri Van Pham</b></sub></a><br /><a href="#bug-pvtri96" title="Bug reports">🐛</a></td>
139
140
  <td align="center" valign="top" width="14.28%"><a href="https://github.com/BingWuJ"><img src="https://avatars.githubusercontent.com/u/117666511?v=4" width="100px;" alt=""/><br /><sub><b>BingWuJ</b></sub></a><br /><a href="#bug-BingWuJ" title="Bug reports">🐛</a></td>
140
141
  <td align="center" valign="top" width="14.28%"><a href="https://github.com/tackleberrybey"><img src="https://avatars.githubusercontent.com/u/156954032?v=4" width="100px;" alt=""/><br /><sub><b>tackleberrybey</b></sub></a><br /><a href="#bug-tackleberrybey" title="Bug reports">🐛</a></td>
141
142
  <td align="center" valign="top" width="14.28%"><a href="https://github.com/TaterDoge"><img src="https://avatars.githubusercontent.com/u/121467933?v=4" width="100px;" alt=""/><br /><sub><b>Mariann Abshire</b></sub></a><br /><a href="#bug-TaterDoge" title="Bug reports">🐛</a></td>
142
143
  <td align="center" valign="top" width="14.28%"><a href="https://github.com/ProbabilityEngineer"><img src="https://avatars.githubusercontent.com/u/38498804?v=4" width="100px;" alt=""/><br /><sub><b>ProbabilityEngineer</b></sub></a><br /><a href="#ideas-ProbabilityEngineer" title="Ideas & Planning">🤔</a></td>
143
144
  <td align="center" valign="top" width="14.28%"><a href="https://github.com/LeonardoRick"><img src="https://avatars.githubusercontent.com/u/17517057?v=4" width="100px;" alt=""/><br /><sub><b>Leonardo Rick</b></sub></a><br /><a href="#bug-LeonardoRick" title="Bug reports">🐛</a> <a href="#ideas-LeonardoRick" title="Ideas & Planning">🤔</a></td>
144
- <td align="center" valign="top" width="14.28%"><a href="https://github.com/kenbanks-peng"><img src="https://avatars.githubusercontent.com/u/26904200?v=4" width="100px;" alt=""/><br /><sub><b>Ken Banks</b></sub></a><br /><a href="#bug-kenbanks-peng" title="Bug reports">🐛</a></td>
145
- </tr><br />
145
+ </tr>
146
146
  <tr>
147
+ <td align="center" valign="top" width="14.28%"><a href="https://github.com/kenbanks-peng"><img src="https://avatars.githubusercontent.com/u/26904200?v=4" width="100px;" alt=""/><br /><sub><b>Ken Banks</b></sub></a><br /><a href="#bug-kenbanks-peng" title="Bug reports">🐛</a></td>
147
148
  <td align="center" valign="top" width="14.28%"><a href="https://github.com/rstacruz"><img src="https://avatars.githubusercontent.com/u/74385?v=4" width="100px;" alt=""/><br /><sub><b>Rico Sta. Cruz</b></sub></a><br /><a href="#ideas-rstacruz" title="Ideas & Planning">🤔</a></td>
148
149
  <td align="center" valign="top" width="14.28%"><a href="https://github.com/acidnik"><img src="https://avatars.githubusercontent.com/u/1227955?v=4" width="100px;" alt=""/><br /><sub><b>Nikita Bilous</b></sub></a><br /><a href="#bug-acidnik" title="Bug reports">🐛</a></td>
149
150
  <td align="center" valign="top" width="14.28%"><a href="https://github.com/av1155"><img src="https://avatars.githubusercontent.com/u/117413846?v=4" width="100px;" alt=""/><br /><sub><b>Andrea Arturo Venti Fuentes</b></sub></a><br /><a href="#bug-av1155" title="Bug reports">🐛</a></td>
150
151
  <td align="center" valign="top" width="14.28%"><a href="https://github.com/dacec354"><img src="https://avatars.githubusercontent.com/u/90093629?v=4" width="100px;" alt=""/><br /><sub><b>dacec354</b></sub></a><br /><a href="#bug-dacec354" title="Bug reports">🐛</a></td>
151
152
  <td align="center" valign="top" width="14.28%"><a href="https://github.com/nodnarbnitram"><img src="https://avatars.githubusercontent.com/u/44812862?v=4" width="100px;" alt=""/><br /><sub><b>Brandon Martin</b></sub></a><br /><a href="#bug-nodnarbnitram" title="Bug reports">🐛</a></td>
152
153
  <td align="center" valign="top" width="14.28%"><a href="https://github.com/alpertarhan"><img src="https://avatars.githubusercontent.com/u/50966980?v=4" width="100px;" alt=""/><br /><sub><b>Alper Tarhan</b></sub></a><br /><a href="#bug-alpertarhan" title="Bug reports">🐛</a></td>
153
- <td align="center" valign="top" width="14.28%"><a href="https://github.com/asolopovas"><img src="https://avatars.githubusercontent.com/u/6893216?v=4" width="100px;" alt=""/><br /><sub><b>Andrius Solopovas</b></sub></a><br /><a href="#bug-asolopovas" title="Bug reports">🐛</a></td>
154
- </tr><br />
154
+ </tr>
155
155
  <tr>
156
+ <td align="center" valign="top" width="14.28%"><a href="https://github.com/asolopovas"><img src="https://avatars.githubusercontent.com/u/6893216?v=4" width="100px;" alt=""/><br /><sub><b>Andrius Solopovas</b></sub></a><br /><a href="#bug-asolopovas" title="Bug reports">🐛</a></td>
156
157
  <td align="center" valign="top" width="14.28%"><a href="https://github.com/GrahamJenkins"><img src="https://avatars.githubusercontent.com/u/6607975?v=4" width="100px;" alt=""/><br /><sub><b>Graham Jenkins</b></sub></a><br /><a href="#bug-GrahamJenkins" title="Bug reports">🐛</a></td>
157
158
  <td align="center" valign="top" width="14.28%"><a href="https://github.com/wings1848"><img src="https://avatars.githubusercontent.com/u/120104016?v=4" width="100px;" alt=""/><br /><sub><b>Wings Butterfly</b></sub></a><br /><a href="#bug-wings1848" title="Bug reports">🐛</a></td>
158
159
  <td align="center" valign="top" width="14.28%"><a href="https://github.com/JohannesKlauss"><img src="https://avatars.githubusercontent.com/u/6214415?v=4" width="100px;" alt=""/><br /><sub><b>Johannes Klauss</b></sub></a><br /><a href="#ideas-JohannesKlauss" title="Ideas & Planning">🤔</a></td>
159
160
  <td align="center" valign="top" width="14.28%"><a href="https://github.com/tan-yong-sheng"><img src="https://avatars.githubusercontent.com/u/64836390?v=4" width="100px;" alt=""/><br /><sub><b>Tan Yong Sheng</b></sub></a><br /><a href="#ideas-tan-yong-sheng" title="Ideas & Planning">🤔</a></td>
160
161
  <td align="center" valign="top" width="14.28%"><a href="https://github.com/fmatray"><img src="https://avatars.githubusercontent.com/u/8267716?v=4" width="100px;" alt=""/><br /><sub><b>Frédéric</b></sub></a><br /><a href="#bug-fmatray" title="Bug reports">🐛</a></td>
161
162
  <td align="center" valign="top" width="14.28%"><a href="https://github.com/fuentesjr"><img src="https://avatars.githubusercontent.com/u/9240?v=4" width="100px;" alt=""/><br /><sub><b>Salvador Fuentes Jr</b></sub></a><br /><a href="#bug-fuentesjr" title="Bug reports">🐛</a></td>
162
- <td align="center" valign="top" width="14.28%"><a href="https://github.com/Viterkim"><img src="https://avatars.githubusercontent.com/u/17838985?v=4" width="100px;" alt=""/><br /><sub><b>Viktor</b></sub></a><br /><a href="#bug-Viterkim" title="Bug reports">🐛</a></td>
163
- </tr><br />
163
+ </tr>
164
164
  <tr>
165
+ <td align="center" valign="top" width="14.28%"><a href="https://github.com/Viterkim"><img src="https://avatars.githubusercontent.com/u/17838985?v=4" width="100px;" alt=""/><br /><sub><b>Viktor</b></sub></a><br /><a href="#bug-Viterkim" title="Bug reports">🐛</a></td>
165
166
  <td align="center" valign="top" width="14.28%"><a href="https://github.com/ortonomy"><img src="https://avatars.githubusercontent.com/u/6688676?v=4" width="100px;" alt=""/><br /><sub><b>Gregory Orton</b></sub></a><br /><a href="#bug-ortonomy" title="Bug reports">🐛</a></td>
166
167
  <td align="center" valign="top" width="14.28%"><a href="https://github.com/jimallen"><img src="https://avatars.githubusercontent.com/u/868773?v=4" width="100px;" alt=""/><br /><sub><b>Jim Allen</b></sub></a><br /><a href="#bug-jimallen" title="Bug reports">🐛</a></td>
167
168
  <td align="center" valign="top" width="14.28%"><a href="https://github.com/quantfiction"><img src="https://avatars.githubusercontent.com/u/49965454?v=4" width="100px;" alt=""/><br /><sub><b>quantfiction</b></sub></a><br /><a href="#bug-quantfiction" title="Bug reports">🐛</a></td>
168
169
  <td align="center" valign="top" width="14.28%"><a href="https://github.com/Artawower"><img src="https://avatars.githubusercontent.com/u/16963833?v=4" width="100px;" alt=""/><br /><sub><b>Art</b></sub></a><br /><a href="#bug-Artawower" title="Bug reports">🐛</a></td>
169
170
  <td align="center" valign="top" width="14.28%"><a href="https://github.com/chozandrias76"><img src="https://avatars.githubusercontent.com/u/2087677?v=4" width="100px;" alt=""/><br /><sub><b>Colin Swenson-Healey</b></sub></a><br /><a href="#bug-chozandrias76" title="Bug reports">🐛</a></td>
170
171
  <td align="center" valign="top" width="14.28%"><a href="https://github.com/JustlyAI"><img src="https://avatars.githubusercontent.com/u/12634140?v=4" width="100px;" alt=""/><br /><sub><b>Laurent Wiesel</b></sub></a><br /><a href="#bug-JustlyAI" title="Bug reports">🐛</a></td>
171
- <td align="center" valign="top" width="14.28%"><a href="https://github.com/stark-bit"><img src="https://avatars.githubusercontent.com/u/44064758?v=4" width="100px;" alt=""/><br /><sub><b>Rei Starks</b></sub></a><br /><a href="#bug-stark-bit" title="Bug reports">🐛</a></td>
172
- </tr><br />
172
+ </tr>
173
173
  <tr>
174
+ <td align="center" valign="top" width="14.28%"><a href="https://github.com/stark-bit"><img src="https://avatars.githubusercontent.com/u/44064758?v=4" width="100px;" alt=""/><br /><sub><b>Rei Starks</b></sub></a><br /><a href="#bug-stark-bit" title="Bug reports">🐛</a></td>
174
175
  <td align="center" valign="top" width="14.28%"><a href="https://github.com/robertoecf"><img src="https://avatars.githubusercontent.com/u/54923863?v=4" width="100px;" alt=""/><br /><sub><b>Roberto Freitas</b></sub></a><br /><a href="#bug-robertoecf" title="Bug reports">🐛</a></td>
175
176
  <td align="center" valign="top" width="14.28%"><a href="https://github.com/tmttodd"><img src="https://avatars.githubusercontent.com/u/160077416?v=4" width="100px;" alt=""/><br /><sub><b>tmttodd</b></sub></a><br /><a href="#bug-tmttodd" title="Bug reports">🐛</a></td>
176
177
  <td align="center" valign="top" width="14.28%"><a href="https://github.com/smaharnav"><img src="https://avatars.githubusercontent.com/u/263328627?v=4" width="100px;" alt=""/><br /><sub><b>smaharnav</b></sub></a><br /><a href="#bug-smaharnav" title="Bug reports">🐛</a></td>
177
178
  <td align="center" valign="top" width="14.28%"><a href="https://github.com/MzaGuille"><img src="https://avatars.githubusercontent.com/u/151482698?v=4" width="100px;" alt=""/><br /><sub><b>Mza.Guille</b></sub></a><br /><a href="#bug-MzaGuille" title="Bug reports">🐛</a></td>
178
179
  <td align="center" valign="top" width="14.28%"><a href="http://jasonrimmer.com/"><img src="https://avatars.githubusercontent.com/u/629?v=4" width="100px;" alt=""/><br /><sub><b>Jason Rimmer</b></sub></a><br /><a href="#bug-jrimmer" title="Bug reports">🐛</a></td>
179
180
  <td align="center" valign="top" width="14.28%"><a href="https://github.com/ArthurHeymans"><img src="https://avatars.githubusercontent.com/u/15137817?v=4" width="100px;" alt=""/><br /><sub><b>Arthur Heymans</b></sub></a><br /><a href="#ideas-ArthurHeymans" title="Ideas & Planning">🤔</a></td>
180
- <td align="center" valign="top" width="14.28%"><a href="https://github.com/Blue-B"><img src="https://avatars.githubusercontent.com/u/55532956?v=4" width="100px;" alt=""/><br /><sub><b>Blue-B</b></sub></a><br /><a href="#bug-Blue-B" title="Bug reports">🐛</a></td>
181
- </tr><br />
181
+ </tr>
182
182
  <tr>
183
+ <td align="center" valign="top" width="14.28%"><a href="https://github.com/Blue-B"><img src="https://avatars.githubusercontent.com/u/55532956?v=4" width="100px;" alt=""/><br /><sub><b>Blue-B</b></sub></a><br /><a href="#bug-Blue-B" title="Bug reports">🐛</a></td>
183
184
  <td align="center" valign="top" width="14.28%"><a href="https://github.com/bisko"><img src="https://avatars.githubusercontent.com/u/184938?v=4" width="100px;" alt=""/><br /><sub><b>Biser Perchinkov</b></sub></a><br /><a href="#bug-bisko" title="Bug reports">🐛</a></td>
184
185
  <td align="center" valign="top" width="14.28%"><a href="https://github.com/xificurC"><img src="https://avatars.githubusercontent.com/u/4800719?v=4" width="100px;" alt=""/><br /><sub><b>Peter Nagy</b></sub></a><br /><a href="#bug-xificurC" title="Bug reports">🐛</a></td>
185
186
  <td align="center" valign="top" width="14.28%"><a href="https://github.com/orest-tokovenko-block"><img src="https://avatars.githubusercontent.com/u/190554467?v=4" width="100px;" alt=""/><br /><sub><b>Orest Tokovenko</b></sub></a><br /><a href="#bug-orest-tokovenko-block" title="Bug reports">🐛</a></td>
186
187
  <td align="center" valign="top" width="14.28%"><a href="https://eisterman.dev/"><img src="https://avatars.githubusercontent.com/u/3028953?v=4" width="100px;" alt=""/><br /><sub><b>Federico Pasqua</b></sub></a><br /><a href="#bug-eisterman" title="Bug reports">🐛</a></td>
187
188
  <td align="center" valign="top" width="14.28%"><a href="https://github.com/ymek"><img src="https://avatars.githubusercontent.com/u/300569?v=4" width="100px;" alt=""/><br /><sub><b>Myke Stubbs</b></sub></a><br /><a href="#code-ymek" title="Code">💻</a> <a href="#bug-ymek" title="Bug reports">🐛</a></td>
188
189
  <td align="center" valign="top" width="14.28%"><a href="https://github.com/black-snow"><img src="https://avatars.githubusercontent.com/u/579733?v=4" width="100px;" alt=""/><br /><sub><b>Ronald</b></sub></a><br /><a href="#bug-black-snow" title="Bug reports">🐛</a></td>
189
- <td align="center" valign="top" width="14.28%"><a href="https://github.com/JSap0914"><img src="https://avatars.githubusercontent.com/u/116227558?v=4" width="100px;" alt=""/><br /><sub><b>JSap0914</b></sub></a><br /><a href="#code-JSap0914" title="Code">💻</a></td>
190
- </tr><br />
190
+ </tr>
191
191
  <tr>
192
+ <td align="center" valign="top" width="14.28%"><a href="https://github.com/JSap0914"><img src="https://avatars.githubusercontent.com/u/116227558?v=4" width="100px;" alt=""/><br /><sub><b>JSap0914</b></sub></a><br /><a href="#code-JSap0914" title="Code">💻</a></td>
192
193
  <td align="center" valign="top" width="14.28%"><a href="https://github.com/AngriestBird"><img src="https://avatars.githubusercontent.com/u/12663269?v=4" width="100px;" alt=""/><br /><sub><b>Kenneth McCormick</b></sub></a><br /><a href="#bug-AngriestBird" title="Bug reports">🐛</a></td>
193
194
  <td align="center" valign="top" width="14.28%"><a href="https://github.com/knthmn"><img src="https://avatars.githubusercontent.com/u/60438008?v=4" width="100px;" alt=""/><br /><sub><b>Kent Hou Man</b></sub></a><br /><a href="#code-knthmn" title="Code">💻</a> <a href="#bug-knthmn" title="Bug reports">🐛</a></td>
194
195
  <td align="center" valign="top" width="14.28%"><a href="https://github.com/aaronkyriesenbach"><img src="https://avatars.githubusercontent.com/u/12665860?v=4" width="100px;" alt=""/><br /><sub><b>Aaron Ky-Riesenbach</b></sub></a><br /><a href="#bug-aaronkyriesenbach" title="Bug reports">🐛</a></td>
195
196
  <td align="center" valign="top" width="14.28%"><a href="https://github.com/LovelyLoong"><img src="https://avatars.githubusercontent.com/u/54889641?v=4" width="100px;" alt=""/><br /><sub><b>可爱的龙</b></sub></a><br /><a href="#bug-LovelyLoong" title="Bug reports">🐛</a></td>
196
197
  <td align="center" valign="top" width="14.28%"><a href="https://github.com/pppobear"><img src="https://avatars.githubusercontent.com/u/21952175?v=4" width="100px;" alt=""/><br /><sub><b>pppobear</b></sub></a><br /><a href="#bug-pppobear" title="Bug reports">🐛</a></td>
198
+ <td align="center" valign="top" width="14.28%"><a href="https://neumie.dev/"><img src="https://avatars.githubusercontent.com/u/23665482?v=4" width="100px;" alt=""/><br /><sub><b>Jakub Neumann</b></sub></a><br /><a href="#code-neumie" title="Code">💻</a></td>
197
199
  </tr>
198
- </tbody>
200
+ </tbody>
199
201
  </table>
200
202
 
201
-
202
203
  <!-- markdownlint-restore -->
203
204
  <!-- prettier-ignore-end -->
204
205
 
@@ -3,7 +3,7 @@
3
3
  * Exposes both the default export (callers that do `import yaml from …`) and the
4
4
  * named `dump`/`load` helpers, sourced from the one import.
5
5
  */
6
- import yaml from "js-yaml";
6
+ import * as yaml from "js-yaml";
7
7
  export default yaml;
8
8
  export const dump = yaml.dump;
9
9
  export const load = yaml.load;
@@ -10,9 +10,12 @@ import * as fs from "node:fs";
10
10
  import * as path from "node:path";
11
11
  import { loadAstGrepNapi, } from "../../deps/ast-grep-napi.js";
12
12
  import { getAstGrepRuleSources, } from "../../sgconfig.js";
13
+ import { logLatency } from "../../latency-logger.js";
13
14
  import { hasEslintConfig } from "../../tool-policy.js";
14
15
  import { enabledAuxiliaryLspServerIds } from "../auxiliary-lsp.js";
15
16
  import { classifyDefect } from "../diagnostic-taxonomy.js";
17
+ import { isAuxiliaryLspAlive } from "../../lsp/index.js";
18
+ import { resolveAstGrepNativeExe } from "../../lsp/wait-policy/index.js";
16
19
  import { PRIORITY } from "../priorities.js";
17
20
  import { calculateRuleComplexity, isOverlyBroadPattern, isStructuredRule, loadYamlRules, loadYamlRulesFresh, MAX_BLOCKING_RULE_COMPLEXITY, } from "./yaml-rule-parser.js";
18
21
  // Lazy load the napi package
@@ -123,22 +126,26 @@ export function canHandle(filePath) {
123
126
  /**
124
127
  * The TypeScript grammar is a syntactic superset of JavaScript, so a
125
128
  * `JavaScript`-tagged rule using generic node kinds (`variable_declarator`,
126
- * `assignment_expression`, …) still matches against a parsed `.ts`/`.tsx`
127
- * root — and vice versa isn't an issue since JS files never parse
129
+ * `assignment_expression`, …) still matches against a parsed `.ts` root
130
+ * — and vice versa isn't an issue since JS files never parse
128
131
  * TS-only syntax, but a `TypeScript`-tagged rule with a plain-JS-compatible
129
132
  * body would equally double-fire alongside a `JavaScript` twin on a `.ts`
130
133
  * file. Without this, `language:` reads as a real filter but isn't one for
131
134
  * ts↔js pairs, so twin rules sharing a base name (e.g. `hardcoded-url` /
132
135
  * `hardcoded-url-js`) both match the same construct in the SAME runner
133
- * invocation (#657). Returns undefined for extensions this scoping doesn't
134
- * apply to (css/html), where no filtering is added.
136
+ * invocation (#657). TSX is deliberately its own grammar here: the primary
137
+ * ast-grep CLI/LSP also treats `tsx` as distinct from `typescript`, so a
138
+ * language-tagged rule only runs on the exact grammar it names. Returns
139
+ * undefined for extensions this scoping doesn't apply to (css/html), where
140
+ * no filtering is added.
135
141
  */
136
142
  export function ruleLanguageForFile(filePath) {
137
143
  const ext = path.extname(filePath).toLowerCase();
138
144
  switch (ext) {
139
145
  case ".ts":
140
- case ".tsx":
141
146
  return "typescript";
147
+ case ".tsx":
148
+ return "tsx";
142
149
  case ".js":
143
150
  case ".jsx":
144
151
  return "javascript";
@@ -213,10 +220,32 @@ export function evaluateAstGrepRules(filePath, rootNode, cwd, kind, options = {}
213
220
  const maxTotalDiagnostics = options.maxTotalDiagnostics ?? MAX_TOTAL_DIAGNOSTICS;
214
221
  const blockingOnly = options.blockingOnly === true;
215
222
  const log = options.log;
223
+ const unsupportedLanguageLog = options.unsupportedLanguageLog ?? new Set();
216
224
  const diagnostics = [];
217
225
  const seenRuleIds = new Set();
218
226
  const suppressLinterOverlap = kind === "jsts" && hasEslintConfig(cwd);
219
227
  const fileLang = ruleLanguageForFile(filePath);
228
+ // Unsupported-language skips are expected in bulk (every non-jsts rule in the
229
+ // catalog, e.g. ~30 Python rules) — aggregate them into ONE latency-log entry
230
+ // per evaluation instead of per-rule terminal lines (#282 follow-up).
231
+ const newlyUnsupported = new Map();
232
+ const flushUnsupportedRuleSkips = () => {
233
+ if (newlyUnsupported.size === 0)
234
+ return;
235
+ logLatency({
236
+ type: "phase",
237
+ phase: "astgrep_napi_unsupported_rules_skipped",
238
+ filePath,
239
+ durationMs: 0,
240
+ metadata: {
241
+ skippedByLanguage: Object.fromEntries(Array.from(newlyUnsupported.entries(), ([language, ruleIds]) => [
242
+ language,
243
+ { count: ruleIds.length, ruleIds },
244
+ ])),
245
+ },
246
+ });
247
+ newlyUnsupported.clear();
248
+ };
220
249
  // Shared with the raw sgconfig materializer so both surfaces walk the same
221
250
  // workspace-rooted sources in the same precedence order.
222
251
  const ruleSources = getAstGrepRuleSources(options.projectRoot ?? cwd);
@@ -233,6 +262,7 @@ export function evaluateAstGrepRules(filePath, rootNode, cwd, kind, options = {}
233
262
  }
234
263
  const duplicates = duplicateRuleIds(rules);
235
264
  if (appendDuplicateRuleDiagnostics(diagnostics, seenRuleIds, duplicates, source, filePath, maxTotalDiagnostics)) {
265
+ flushUnsupportedRuleSkips();
236
266
  return diagnostics;
237
267
  }
238
268
  const duplicateSet = new Set(duplicates);
@@ -258,7 +288,16 @@ export function evaluateAstGrepRules(filePath, rootNode, cwd, kind, options = {}
258
288
  continue;
259
289
  }
260
290
  const lang = rule.language?.toLowerCase();
261
- if (lang && lang !== "typescript" && lang !== "javascript") {
291
+ if (lang &&
292
+ lang !== "typescript" &&
293
+ lang !== "tsx" &&
294
+ lang !== "javascript") {
295
+ if (!unsupportedLanguageLog.has(rule.id)) {
296
+ unsupportedLanguageLog.add(rule.id);
297
+ const ids = newlyUnsupported.get(lang) ?? [];
298
+ ids.push(rule.id);
299
+ newlyUnsupported.set(lang, ids);
300
+ }
262
301
  continue;
263
302
  }
264
303
  // Scope TypeScript/JavaScript-tagged rules to the file's actual
@@ -338,6 +377,7 @@ export function evaluateAstGrepRules(filePath, rootNode, cwd, kind, options = {}
338
377
  }
339
378
  }
340
379
  }
380
+ flushUnsupportedRuleSkips();
341
381
  return diagnostics;
342
382
  }
343
383
  // --- Runner Definition ---
@@ -357,7 +397,20 @@ const astGrepNapiRunner = {
357
397
  // report against the LSP's `tool: ast-grep` diagnostics. Resume ONLY as the
358
398
  // fallback when the binary is absent / can't spawn (Gate B).
359
399
  const astGrepLspEnabled = enabledAuxiliaryLspServerIds((f) => ctx.pi?.getFlag?.(f)).includes("ast-grep");
360
- if (astGrepLspEnabled && (await ctx.hasTool("ast-grep"))) {
400
+ // Gate B asks whether the LSP will handle this file, not whether a bare
401
+ // `ast-grep` command happens to be on PATH. The launcher first tries the
402
+ // platform-native package binary, then PATH; mirror that resolution here.
403
+ // A live client covers the already-warm case, while a resolvable binary
404
+ // covers the cold case before the LSP has spawned for this root.
405
+ const astGrepLspAlive = astGrepLspEnabled
406
+ ? await isAuxiliaryLspAlive("ast-grep", ctx.filePath)
407
+ : false;
408
+ let astGrepBinaryResolvable = false;
409
+ if (astGrepLspEnabled && !astGrepLspAlive) {
410
+ astGrepBinaryResolvable =
411
+ Boolean(resolveAstGrepNativeExe()) || (await ctx.hasTool("ast-grep"));
412
+ }
413
+ if (astGrepLspEnabled && (astGrepLspAlive || astGrepBinaryResolvable)) {
361
414
  return { status: "skipped", diagnostics: [], semantic: "none" };
362
415
  }
363
416
  const sgModule = await loadSg();
@@ -226,8 +226,19 @@ export async function recordLspChild(entry) {
226
226
  }
227
227
  await writeRegistryAsync(file);
228
228
  }
229
+ // LSP client shutdown intentionally does not await registry removal (the
230
+ // process-exiting path must stay non-blocking), but concurrent removals still
231
+ // need to serialize their read-modify-write sequence or siblings can be lost
232
+ // to last-writer-wins. Process kills remain fully concurrent; this queue only
233
+ // covers the small best-effort registry mutation.
234
+ let lspChildRemovalTail = Promise.resolve();
229
235
  /** Remove an LSP child (by pid) from this process's entry. */
230
- export async function removeLspChild(pid) {
236
+ export function removeLspChild(pid) {
237
+ const removal = lspChildRemovalTail.then(() => removeLspChildNow(pid));
238
+ lspChildRemovalTail = removal.catch(() => { });
239
+ return removal;
240
+ }
241
+ async function removeLspChildNow(pid) {
231
242
  if (!isInstanceRegistryEnabled())
232
243
  return;
233
244
  const selfPid = process.pid;
@@ -440,7 +440,16 @@ export class LSPService {
440
440
  notifyWriteBackpressureStreak = new Map();
441
441
  /** True after shutdown() has been called; blocks new operations */
442
442
  isDestroyed = false;
443
- constructor() {
443
+ /**
444
+ * #850: teardown completion for every singleton generation retired before
445
+ * this service was published. Only replacement services receive one; direct
446
+ * `new LSPService()` callers and the first singleton generation stay hot-path
447
+ * identical. Cleared after the first completed wait so warm reuse never pays
448
+ * a permanent promise/microtask tax.
449
+ */
450
+ generationHandoff;
451
+ constructor(generationHandoff) {
452
+ this.generationHandoff = generationHandoff;
444
453
  this.state = {
445
454
  clients: new Map(),
446
455
  servers: new Map(),
@@ -738,9 +747,38 @@ export class LSPService {
738
747
  }
739
748
  return undefined;
740
749
  }
750
+ /**
751
+ * Read-only liveness check for one server/file pair. Unlike
752
+ * `getClientForFile`, this never creates or warms a client; it only resolves
753
+ * the server's root and checks the already-connected client map.
754
+ */
755
+ async isServerAliveForFile(serverId, filePath) {
756
+ if (this.checkDestroyed())
757
+ return false;
758
+ for (const server of getServersForFileWithConfig(filePath)) {
759
+ if (server.id !== serverId)
760
+ continue;
761
+ const root = await server.root(filePath);
762
+ if (!root)
763
+ continue;
764
+ const key = `${server.id}:${normalizeMapKey(root)}`;
765
+ if (this.state.clients.get(key)?.isAlive())
766
+ return true;
767
+ }
768
+ return false;
769
+ }
741
770
  async ensureClientForServer(filePath, server) {
771
+ const handoff = this.generationHandoff;
772
+ if (handoff) {
773
+ await handoff;
774
+ if (this.generationHandoff === handoff) {
775
+ this.generationHandoff = undefined;
776
+ }
777
+ if (this.checkDestroyed())
778
+ return undefined;
779
+ }
742
780
  const root = await server.root(filePath);
743
- if (!root)
781
+ if (!root || this.checkDestroyed())
744
782
  return undefined;
745
783
  const allowInstall = this.shouldAllowInstall(filePath, root);
746
784
  const normalizedRoot = normalizeMapKey(root);
@@ -831,6 +869,11 @@ export class LSPService {
831
869
  if (inFlight) {
832
870
  return inFlight;
833
871
  }
872
+ // `server.root()` and a dead client's shutdown above are both async. A
873
+ // reset during either gap may have completed without seeing this request in
874
+ // state.inFlight; never let that retired generation start a late spawn.
875
+ if (this.checkDestroyed())
876
+ return undefined;
834
877
  const spawnPromise = this.spawnClient(server, root, key, filePath, allowInstall);
835
878
  this.state.inFlight.set(key, spawnPromise);
836
879
  try {
@@ -3186,14 +3229,13 @@ export class LSPService {
3186
3229
  processExiting: !!options.processExiting,
3187
3230
  },
3188
3231
  });
3189
- for (const [_key, client] of this.state.clients) {
3190
- try {
3191
- await client.shutdown(options);
3192
- }
3193
- catch {
3194
- // pi-lens-ignore: missing-error-propagation — per-client shutdown failure, must not abort remaining shutdowns
3195
- }
3196
- }
3232
+ // Start every client teardown before awaiting any of them. A non-fast
3233
+ // process-tree kill can spend its grace period per client, so awaiting in
3234
+ // map order makes the reset tail O(clientCount * grace) instead of the
3235
+ // maximum individual teardown. allSettled preserves the per-client
3236
+ // best-effort contract: one failure must not prevent other clients from
3237
+ // finishing, and the caller still waits for every teardown to settle.
3238
+ await Promise.allSettled(Array.from(this.state.clients.values(), (client) => Promise.resolve().then(() => client.shutdown(options))));
3197
3239
  this.state.clients.clear();
3198
3240
  this.state.broken.clear();
3199
3241
  this.workspaceProbeLogged.clear();
@@ -3245,17 +3287,47 @@ export class LSPService {
3245
3287
  }
3246
3288
  // --- Singleton Instance ---
3247
3289
  let globalLSPService = null;
3290
+ /**
3291
+ * #850: all singleton generations whose teardown is still pending. A new
3292
+ * generation may be allocated synchronously, but its first spawn waits on this
3293
+ * handoff so two generations can never own the same server/root concurrently.
3294
+ */
3295
+ let globalLSPGenerationHandoff;
3248
3296
  export function getLSPService() {
3249
3297
  if (!globalLSPService) {
3250
- globalLSPService = new LSPService();
3298
+ globalLSPService = new LSPService(globalLSPGenerationHandoff);
3251
3299
  }
3252
3300
  return globalLSPService;
3253
3301
  }
3302
+ /**
3303
+ * Cross-layer liveness seam for dispatch-side auxiliary gates. This is a
3304
+ * liveness read only: it does not spawn, wait for initialization, or probe a
3305
+ * binary.
3306
+ */
3307
+ export async function isAuxiliaryLspAlive(serverId, filePath) {
3308
+ return getLSPService().isServerAliveForFile(serverId, filePath);
3309
+ }
3254
3310
  export function resetLSPService(options = {}) {
3255
- if (globalLSPService) {
3256
- globalLSPService.shutdown(options).catch(() => { });
3257
- }
3311
+ const retiringService = globalLSPService;
3258
3312
  globalLSPService = null;
3313
+ if (!retiringService)
3314
+ return;
3315
+ // shutdown() marks the service destroyed synchronously before its first
3316
+ // await. Include both that teardown and every earlier pending generation:
3317
+ // repeated resets may retire a replacement that is itself still waiting on
3318
+ // its predecessor. allSettled keeps teardown best-effort without ever
3319
+ // rejecting (and therefore permanently poisoning) the next generation.
3320
+ const teardown = retiringService.shutdown(options);
3321
+ const pending = globalLSPGenerationHandoff
3322
+ ? [globalLSPGenerationHandoff, teardown]
3323
+ : [teardown];
3324
+ const handoff = Promise.allSettled(pending).then(() => undefined);
3325
+ globalLSPGenerationHandoff = handoff;
3326
+ void handoff.then(() => {
3327
+ if (globalLSPGenerationHandoff === handoff) {
3328
+ globalLSPGenerationHandoff = undefined;
3329
+ }
3330
+ });
3259
3331
  }
3260
3332
  /**
3261
3333
  * Test-only: exposes the async workspace-diagnostics file walk so its