pi-lens 3.8.69 → 3.8.71

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (147) hide show
  1. package/CHANGELOG.md +99 -0
  2. package/README.md +31 -13
  3. package/dist/clients/bootstrap.js +3 -1
  4. package/dist/clients/bus-events-logger.js +53 -0
  5. package/dist/clients/bus-publish.js +45 -2
  6. package/dist/clients/cache/rule-cache.js +9 -5
  7. package/dist/clients/cache-manager.js +19 -13
  8. package/dist/clients/code-quality-warnings.js +27 -0
  9. package/dist/clients/dead-code-client.js +5 -23
  10. package/dist/clients/diagnostic-dispositions.js +298 -0
  11. package/dist/clients/diagnostics-publish.js +38 -2
  12. package/dist/clients/dispatch/auxiliary-lsp.js +144 -6
  13. package/dist/clients/dispatch/dispatcher.js +6 -1
  14. package/dist/clients/dispatch/facts/function-facts.js +83 -0
  15. package/dist/clients/dispatch/integration.js +19 -4
  16. package/dist/clients/dispatch/rules/async-noise.js +1 -1
  17. package/dist/clients/dispatch/rules/async-unnecessary-wrapper.js +1 -1
  18. package/dist/clients/dispatch/rules/cors-wildcard.js +1 -1
  19. package/dist/clients/dispatch/rules/error-obscuring.js +2 -1
  20. package/dist/clients/dispatch/rules/error-swallowing.js +2 -1
  21. package/dist/clients/dispatch/rules/framework-call-noise.js +67 -0
  22. package/dist/clients/dispatch/rules/high-complexity.js +12 -1
  23. package/dist/clients/dispatch/rules/high-fan-out.js +14 -3
  24. package/dist/clients/dispatch/rules/high-import-coupling.js +1 -1
  25. package/dist/clients/dispatch/rules/missing-error-propagation.js +1 -1
  26. package/dist/clients/dispatch/rules/no-commented-credentials.js +1 -1
  27. package/dist/clients/dispatch/rules/pass-through-wrappers.js +1 -1
  28. package/dist/clients/dispatch/rules/placeholder-comments.js +1 -1
  29. package/dist/clients/dispatch/rules/unsafe-boundary.js +1 -1
  30. package/dist/clients/dispatch/runners/ast-grep-napi.js +75 -18
  31. package/dist/clients/dispatch/runners/lsp.js +23 -34
  32. package/dist/clients/dispatch/runners/oxlint.js +1 -1
  33. package/dist/clients/dispatch/suppress-writer.js +68 -0
  34. package/dist/clients/dispatch/utils/lsp-diagnostics.js +4 -1
  35. package/dist/clients/disposition-logger.js +39 -0
  36. package/dist/clients/disposition-publish.js +109 -0
  37. package/dist/clients/file-utils.js +94 -33
  38. package/dist/clients/format-events-publish.js +299 -0
  39. package/dist/clients/git-tracked-ignore.js +208 -0
  40. package/dist/clients/gitleaks-client.js +40 -5
  41. package/dist/clients/installer/index.js +123 -6
  42. package/dist/clients/instance-reaper.js +229 -2
  43. package/dist/clients/instance-registry.js +141 -2
  44. package/dist/clients/json-cache-read.js +65 -0
  45. package/dist/clients/knip-client.js +35 -38
  46. package/dist/clients/language-profile.js +11 -11
  47. package/dist/clients/lens-engine.js +12 -0
  48. package/dist/clients/lens-map.js +1079 -0
  49. package/dist/clients/log-cleanup.js +61 -19
  50. package/dist/clients/lsp/aggregation.js +69 -2
  51. package/dist/clients/lsp/cascade-tier.js +31 -19
  52. package/dist/clients/lsp/client.js +124 -14
  53. package/dist/clients/lsp/config.js +27 -1
  54. package/dist/clients/lsp/index.js +1028 -68
  55. package/dist/clients/lsp/server-strategies.js +17 -8
  56. package/dist/clients/lsp/server.js +80 -28
  57. package/dist/clients/lsp/tsserver-sync.js +145 -0
  58. package/dist/clients/lsp/workspace-diagnostics-cache.js +172 -0
  59. package/dist/clients/lsp-budget.js +148 -0
  60. package/dist/clients/mcp/session.js +9 -0
  61. package/dist/clients/middle-man-analysis.js +260 -0
  62. package/dist/clients/module-report.js +173 -11
  63. package/dist/clients/ndjson-logger.js +24 -0
  64. package/dist/clients/opengrep-client.js +217 -0
  65. package/dist/clients/opengrep-config.js +2 -10
  66. package/dist/clients/path-utils.js +66 -0
  67. package/dist/clients/pipeline.js +5 -1
  68. package/dist/clients/project-diagnostics/cache.js +5 -12
  69. package/dist/clients/project-diagnostics/extractors.js +21 -1
  70. package/dist/clients/project-diagnostics/fresh-fetch.js +275 -0
  71. package/dist/clients/project-diagnostics/runner-adapters/opengrep.js +40 -0
  72. package/dist/clients/project-diagnostics/runner-adapters/runner-findings.js +46 -0
  73. package/dist/clients/project-snapshot.js +3 -6
  74. package/dist/clients/quiet-window.js +57 -2
  75. package/dist/clients/read-expansion.js +2 -3
  76. package/dist/clients/read-guard-tool-lines.js +10 -0
  77. package/dist/clients/read-guard.js +1 -1
  78. package/dist/clients/resource-sampler.js +254 -0
  79. package/dist/clients/review-graph/builder.js +322 -42
  80. package/dist/clients/review-graph/import-resolvers.js +53 -19
  81. package/dist/clients/review-graph/symbol-id.js +57 -0
  82. package/dist/clients/runtime-agent-end.js +26 -0
  83. package/dist/clients/runtime-coordinator.js +9 -1
  84. package/dist/clients/runtime-session.js +93 -17
  85. package/dist/clients/runtime-tool-call.js +789 -0
  86. package/dist/clients/runtime-tool-result.js +15 -2
  87. package/dist/clients/runtime-turn.js +62 -13
  88. package/dist/clients/safe-spawn.js +48 -2
  89. package/dist/clients/session-state-store.js +6 -9
  90. package/dist/clients/sgconfig.js +2 -9
  91. package/dist/clients/source-filter.js +17 -25
  92. package/dist/clients/source-walker.js +66 -0
  93. package/dist/clients/startup-scan.js +105 -43
  94. package/dist/clients/symbol-containment.js +24 -0
  95. package/dist/clients/test-runner-client.js +544 -29
  96. package/dist/clients/tree-sitter-symbol-extractor.js +12 -5
  97. package/dist/clients/typos-config.js +2 -11
  98. package/dist/clients/widget-state.js +83 -1
  99. package/dist/clients/write-ordering-guard.js +58 -0
  100. package/dist/clients/zizmor-config.js +29 -10
  101. package/dist/index.js +25511 -19414
  102. package/dist/mcp/server.js +95 -19
  103. package/dist/tools/activate-tools.js +72 -0
  104. package/dist/tools/ast-dump.js +2 -8
  105. package/dist/tools/lens-diagnostic-mark.js +158 -0
  106. package/dist/tools/lens-diagnostics.js +327 -33
  107. package/dist/tools/lsp-diagnostics.js +525 -105
  108. package/dist/tools/module-report.js +18 -5
  109. package/docs/agent-tools.md +136 -0
  110. package/docs/ast-grep_rules_catalog.md +564 -0
  111. package/docs/astplayground.md +178 -0
  112. package/docs/audit1.md +177 -0
  113. package/docs/custom-rules.md +231 -0
  114. package/docs/dependencies.md +54 -0
  115. package/docs/dispositions.md +85 -0
  116. package/docs/env_variables.md +10 -0
  117. package/docs/environment-variables.md +83 -0
  118. package/docs/fable.md +140 -0
  119. package/docs/features.md +338 -0
  120. package/docs/globalconfig.md +79 -0
  121. package/docs/language-coverage.md +48 -0
  122. package/docs/lsp-capability-matrix.md +157 -0
  123. package/docs/lsp-latency-benchmark.md +169 -0
  124. package/docs/mcp.md +438 -0
  125. package/docs/module-report-read-symbol.md +258 -0
  126. package/docs/servercapabilities.md +118 -0
  127. package/docs/subagent-compat.md +281 -0
  128. package/docs/tools.md +16 -0
  129. package/docs/tools_improvement2.md +345 -0
  130. package/docs/tree-sitter_rules_catalog.md +296 -0
  131. package/docs/usage.md +129 -0
  132. package/docs/word-index.md +150 -0
  133. package/package.json +10 -1
  134. package/rules/ast-grep-rules/rule-tests/find-import-file-without-extension-test.yml +4 -0
  135. package/rules/ast-grep-rules/rule-tests/no-javascript-url-js-test.yml +5 -0
  136. package/rules/ast-grep-rules/rule-tests/no-javascript-url-test.yml +9 -0
  137. package/rules/ast-grep-rules/rules/array-callback-return-js.yml +1 -1
  138. package/rules/ast-grep-rules/rules/find-import-file-without-extension.yml +3 -1
  139. package/rules/ast-grep-rules/rules/jwt-no-verify-js.yml +1 -1
  140. package/rules/ast-grep-rules/rules/no-extra-boolean-cast-js.yml +1 -1
  141. package/rules/ast-grep-rules/rules/no-implied-eval-js.yml +1 -1
  142. package/rules/ast-grep-rules/rules/no-javascript-url-js.yml +13 -1
  143. package/rules/ast-grep-rules/rules/no-javascript-url.yml +12 -0
  144. package/rules/ast-grep-rules/rules/no-sql-in-code-js.yml +1 -1
  145. package/rules/ast-grep-rules/rules/weak-rsa-key-js.yml +1 -1
  146. package/skills/pi-lens-ast-grep/SKILL.md +3 -1
  147. package/dist/clients/production-readiness.js +0 -493
@@ -0,0 +1,157 @@
1
+ # LSP capability matrix — affirmative-clean-signal strategy
2
+
3
+ How pi-lens knows a just-edited file is **clean** (no diagnostics) vs the server
4
+ simply **hasn't answered yet** (cold/crashed/silent). pi-lens waits *synchronously*
5
+ for a verdict, so — unlike an editor, which renders asynchronously and never needs
6
+ to decide — it must have a positive signal. There is no signal for "silence", so we
7
+ classify each server and pick a per-server strategy. (Background: #240; mechanism
8
+ confirmed against Neovim's LSP client, which sidesteps this entirely by being async.)
9
+
10
+ Generate/refresh this matrix with `node scripts/characterize-lsp.mjs [--install]`
11
+ (the `mode` column) and `node scripts/probe-clean-signal.mjs [--install]` (the
12
+ `clean-behavior` column — 4-way: 2 / 2* / 3 / unknown). Both **merge** in place: a server the
13
+ running host couldn't spawn keeps its prior row, so an ubuntu-poor run can't
14
+ regress a richer one (#390). The nightly **tool-smoke** workflow runs both (plus
15
+ `server-capabilities.mjs`) and opens/updates a single auto-PR
16
+ (`bot/lsp-docs-refresh`, "docs(nightly): refresh LSP capability docs") with the
17
+ regenerated docs — so this file self-populates from CI without manual copy-paste.
18
+
19
+ `probe-clean-signal.mjs` also runs a **drift check** (#529): it compares each
20
+ probed server's observed `clean-behavior` against the hand-set `silentOnClean`
21
+ marker in `clients/lsp/server-strategies.ts` and writes any mismatch to the
22
+ `## silentOnClean drift` section below. This is telemetry only — never a CI
23
+ gate — because the probe is a timing-based negative observation; a mismatch
24
+ just tells a human the marker may need updating. `unknown` observations are
25
+ never compared in either direction (a slow/absent server isn't evidence of
26
+ anything). The native TS7 launch variant (`typescript7`/`typescript7-clean`,
27
+ #524/#526) is deliberately excluded from comparison against classic's marker —
28
+ they share a server id but not a verified clean-signal behavior.
29
+
30
+ ## The strategies
31
+
32
+ | Tier | Signal | Affirmative clean? | Example |
33
+ |---|---|---|---|
34
+ | **1 — pull** | `textDocument/diagnostic` returns an authoritative report (empty = clean) | YES, deterministic | rust-analyzer |
35
+ | **2 — push, publishes-versioned** | `publishDiagnostics([])` **with version** on every scan, incl. clean→clean | YES, currency-proven via version | ast-grep |
36
+ | **2\* — push, publishes-unversioned** | re-publishes on a clean scan but **version-less** — the wait still early-returns (the client accepts a version-less publish as fresh: it can't be proven stale), but currency is only *temporally correlated*, not proven | YES at runtime, with a staleness-risk caveat (not a latency cost) | opengrep |
37
+ | **3 — push, silent on clean** | server publishes nothing when nothing changed | **NO** — budget-wait floor (safe; a timeout is *not* a false clean). **This tier is #458's learned-deadline target set.** | typescript-language-server |
38
+
39
+ Detection is **cached** at `initialize` (`detectWorkspaceDiagnosticsSupport` →
40
+ `state.workspaceDiagnosticsSupport.mode`, upgraded on `client/registerCapability`),
41
+ so the tier is free at collection time — no per-edit probe.
42
+
43
+ ## Matrix (dev box + CI nightly; mode last refreshed 2026-06-17 from run 27713958681, clean-behavior probed on the dev box 2026-07-08 — #460)
44
+
45
+ `mode` from cached capabilities; `clean-behavior` from the phase-aware publish-trace
46
+ probe. The probe attributes publishes to two phases — the **dirty touch** (proves the
47
+ server is live) and the **clean transitions** (the discriminator) — and classifies
48
+ 4-way: `publishes-versioned` (tier 2: publish WITH version on a clean transition —
49
+ affirmative + currency-proven), `publishes-unversioned` (tier 2\*: version-less
50
+ publish on a clean transition — the wait still early-returns at runtime, currency
51
+ only temporally correlated), `silent` (tier 3: alive on dirty, silent on clean —
52
+ budget-wait, the #458 target), `unknown` (no publish at all — slow/absent,
53
+ conservatively not classified). `src` = where a row was measured: **ci** = the
54
+ nightly steps, **dev** = the dev box (a row measured on both reads `dev+ci`).
55
+ Merges never blank a prior good value, so a CI non-result leaves the dev
56
+ classification standing.
57
+
58
+ | lang | server | mode | clean-behavior | tier | src |
59
+ |---|---|---|---|---|---|
60
+ | json | vscode-json-language-server | pull | — | 1 | dev+ci |
61
+ | css | vscode-css-language-server | pull | — | 1 | dev+ci |
62
+ | html | vscode-html-language-server | pull | — | 1 | dev+ci |
63
+ | rust | rust-analyzer | pull | — | 1 | dev |
64
+ | svelte | svelte-language-server | pull | — | 1 | dev+ci |
65
+ | deno | deno (alt of typescript) | pull | — | 1 | dev+ci |
66
+ | ruby | ruby-lsp | pull | — | 1 | ci |
67
+ | csharp | csharp-ls | pull | — | 1 | ci |
68
+ | typescript | typescript-language-server | push-only | silent | 3 | dev+ci |
69
+ | python | pyright | push-only | publishes-versioned | 2 | dev+ci |
70
+ | jedi | jedi-language-server (alt of python) | push-only | publishes-versioned | 2 | ci |
71
+ | yaml | yaml-language-server | push-only | publishes-unversioned | 2* | dev+ci |
72
+ | shell | bash-language-server | push-only | publishes-versioned | 2 | dev+ci |
73
+ | dockerfile | docker-langserver | push-only | publishes-unversioned | 2* | dev+ci |
74
+ | toml | taplo | push-only | publishes-unversioned | 2* | dev+ci |
75
+ | terraform | terraform-ls | push-only | TBD | 2/3? | dev+ci |
76
+ | prisma | @prisma/language-server | push-only | publishes-unversioned | 2* | dev+ci |
77
+ | php | intelephense | push-only | TBD | 2/3? | dev |
78
+ | zig | zls | push-only | publishes-unversioned | 2* | dev+ci |
79
+ | vue | @vue/language-server | push-only | TBD | 2/3? | dev+ci |
80
+ | dart | dart language-server | push-only | publishes-unversioned | 2* | ci |
81
+ | gleam | gleam lsp | push-only | publishes-unversioned | 2* | ci |
82
+ | clojure | clojure-lsp | push-only | publishes-unversioned | 2* | ci |
83
+ | opengrep | opengrep (aux) | push-only | publishes-unversioned | 2* | dev+ci |
84
+ | ast-grep | ast-grep (aux) | push-only | publishes-versioned | 2 | dev+ci |
85
+
86
+ **Unknown — fixture exists, mode not yet captured.** The toolchain-gated family
87
+ (no auto-install today; tracked in #241) — `go` (gopls), `java` (jdtls),
88
+ `kotlin`, `swift` (sourcekit-lsp), `lua`, `cpp` (clangd), `haskell`, `elixir`,
89
+ `ocaml`, `nix` (nixd), `fsharp`. Their servers don't install in the nightly, so
90
+ characterize reports `unknown` (a non-failure ⚠). Once #241 lands they'll fill in
91
+ the same way clojure-lsp/gleam now do (both auto-install via the github strategy
92
+ and were characterized `push-only` in the run above).
93
+
94
+ ## Key findings
95
+ - **Mode ≠ tier, and the split needs BOTH axes.** Push-only further splits along
96
+ latency (does anything publish on a clean transition? — silence is the only
97
+ budget-wait case, because pi-lens's publish handler emits and early-returns the
98
+ wait on EVERY publish, versioned or not) and currency-proof (is the publish
99
+ versioned, i.e. provably about the live edit?). The 4-way
100
+ `probe-clean-signal.mjs` measurement drives this: ast-grep → 2, yaml/opengrep →
101
+ 2\*, typescript (clean file) → 3.
102
+ - **opengrep is 2\*, not 3 and not plain 2.** It *does* re-publish on a clean scan
103
+ (the wait early-returns at runtime — fast), but every push carries
104
+ `pubVersion=undefined`, so currency is only temporally correlated, not proven —
105
+ a staleness-risk note, not a latency cost. An earlier hand-note called it Tier 2
106
+ on the "re-publishes" observation alone; the phase-aware probe refines it to 2\*.
107
+ - **typescript's clean behavior is diagnostic-set-dependent (major probe finding).**
108
+ On a DIRTY file it re-publishes (version-lessly) after every change — the dirty
109
+ fixture measures 2\*. On a genuinely CLEAN file (the `typescript-clean` fixture)
110
+ it publishes nothing on a clean→clean edit — silent, Tier 3. The clean-file
111
+ behavior is the production case (the observed budget-wait timeouts), so the
112
+ matrix row records the clean fixture's verdict; the probe prefers `clean: true`
113
+ fixtures for exactly this reason. Corollary: a 2\* measured only on a dirty
114
+ fixture may overstate a server whose publishes stop when its set goes empty —
115
+ langs without a clean fixture carry that caveat.
116
+ - **#458's learned-deadline target set = the tier-3 rows only.** 2\* rows resolve
117
+ the wait at runtime and must NOT be given learned deadlines.
118
+ - **Tier 3 is budget-bound by necessity**, not laziness: a silent server's silence is
119
+ ambiguous (clean-unchanged vs still-analyzing), so shortening the wait or reusing
120
+ `lastKnownDiagnostics` would risk a false clean. The wait *is* the safety mechanism.
121
+ - **ast-grep (Phase 2 / #239) is Tier 2** — it self-signals clean on every scan, so it
122
+ is not the bottleneck. The cost on a clean with-auxiliary touch is the *silent
123
+ primary* (typescript), a pre-existing Tier-3 cost independent of ast-grep.
124
+
125
+ ## Completing the matrix
126
+ The fixtures (`tests/fixtures/tool-smoke/<lang>/`) are durable and cover every
127
+ registered server. `mode` is read from the server's advertised capabilities at
128
+ `initialize`, so it is **content-independent** — for languages that already had a
129
+ tool-layer fixture we point `characterize-lsp.mjs` at the existing (deliberately
130
+ dirty) `bad.*` source rather than a colliding clean duplicate; new languages get a
131
+ minimal clean source. Either way the mode reported is the same.
132
+
133
+ The nightly **tool-smoke** workflow runs `characterize-lsp.mjs --install` **and**
134
+ `probe-clean-signal.mjs --install` (after the LSP handshake layer) on `ubuntu-latest`,
135
+ then opens/updates an auto-PR with the regenerated docs (#390) — so both the `mode`
136
+ and `clean-behavior` columns self-populate in CI without manual copy-paste. They fill
137
+ for servers that either auto-install (npm/pip/github — including clojure-lsp and gleam,
138
+ both github-strategy as of f263cf3) or whose toolchain the workflow provisions
139
+ (Ruby/Dart/Zig + .NET→csharp). The remaining `unknown` rows are the toolchain-gated
140
+ family (#241): until `runtimeInstall` + canonical-bin discovery land, their servers
141
+ don't install in CI and stay ⚠. The **merge guard** means a CI run that can't reach a
142
+ server never blanks its dev-measured row.
143
+
144
+ The `clean-behavior` split (4-way: 2 publishes-versioned / 2\* publishes-unversioned /
145
+ 3 silent / unknown) is now measured per-server by `probe-clean-signal.mjs`, no longer a
146
+ manual one-off. Locally confirmed: ast-grep + ast-grep-baseline → 2, yaml + opengrep →
147
+ 2\*, typescript → 3 on its clean fixture (2\* on the dirty fixture — see Key findings);
148
+ the rest fill in as the nightly reaches them.
149
+
150
+ ## silentOnClean drift (nightly-generated)
151
+
152
+ Telemetry only — never a CI gate. Compares each probed server's observed
153
+ `clean-behavior` against `clients/lsp/server-strategies.ts`'s `silentOnClean`
154
+ marker; a mismatch means the marker may need a human update (#529). `unknown`
155
+ observations are never compared (a slow/absent server is not evidence either way).
156
+
157
+ _None observed as of the last probe run._
@@ -0,0 +1,169 @@
1
+ # LSP latency benchmark
2
+
3
+ Cold/warm per-edit latency for pi-lens's registered language servers, measured to
4
+ gate the ast-grep LSP consolidation (#239 Gate A: an auxiliary scanner is
5
+ acceptable as long as its warm latency is in the range of the primary language
6
+ servers pi-lens already tolerates).
7
+
8
+ - **Reproduce:** `npm run build:dist && node scripts/bench-lsp.mjs [lang …] [--install]`
9
+ - **Machine:** Windows 11, dev box, warm npm cache. Numbers are **indicative, not
10
+ absolute** — single machine, small fixtures, one run.
11
+ - **cold** = first touch: spawn + `initialize` handshake + first scan.
12
+ - **warm** = re-touch after a content edit (the per-edit latency the agent waits on),
13
+ averaged over 3 edits.
14
+ - Source fixtures: `tests/fixtures/tool-smoke/*` (driven through the real
15
+ `LSPService.touchFile`, same entry the dispatch path uses).
16
+ - **Isolation (since #242):** each fixture measures **one server alone**. The
17
+ service + workspace config are reset between fixtures (no lingering server from a
18
+ previous measurement), and every *other* server matching the file is disabled —
19
+ so an auxiliary number is the auxiliary by itself (primaries off) and a primary
20
+ number is the default by itself (alternates + auxiliaries off). This removes the
21
+ primary↔auxiliary cross-contamination that produced the old "20557ms" artifact
22
+ (see the resolved-history note below).
23
+
24
+ > **Coverage:** this run measured **31 servers** (27 primary/alternate + 4
25
+ > auxiliary) — those whose toolchain is installed on this box. **Newly measuring
26
+ > since the previous run:** **clangd** (cpp) and **zizmor** (auxiliary) now
27
+ > auto-install via the archive/github strategies (#241/#272), **PowerShell Editor
28
+ > Services** via its pwsh-bootstrapped bundle (#278), and **marksman** (markdown,
29
+ > #274). **11** fixtures remain toolchain-gated and reported `unavailable` here
30
+ > (csharp-ls, jdtls [java + java-lombok], kotlin-language-server, sourcekit-lsp,
31
+ > dart, lua-language-server, haskell-language-server, elixir-ls, ocamllsp, nixd) —
32
+ > run with `--install` on a box with their runtimes to measure them (#241).
33
+ >
34
+ > **Most fixtures are intentionally broken** (they carry a known defect so the smoke
35
+ > harness can assert a diagnostic). That means such a server always has a diagnostic
36
+ > to early-return on — see the clean-file note below, which a broken fixture masks.
37
+ > The `typescript-clean` fixture exists precisely to expose that cost.
38
+
39
+ ## Results (2026-06-21 run, isolated)
40
+
41
+ Sorted by warm/edit. `role`: primary = the file's language server; alternate =
42
+ second server for a language (reached when the default is disabled); auxiliary =
43
+ cross-cutting, attaches alongside the primary in production but measured alone here.
44
+
45
+ | lang | role | cold | warm/edit | server |
46
+ |---|---|---:|---:|---|
47
+ | ruby | primary | 5608ms | 159ms | ruby-lsp |
48
+ | deno | alternate | 1521ms | 165ms | deno (alternate of typescript) |
49
+ | **ast-grep-baseline** | auxiliary | 1906ms | **394ms** | ast-grep (no-sgconfig baseline) |
50
+ | **ast-grep** | auxiliary | 1614ms | **399ms** | ast-grep |
51
+ | dockerfile | primary | 1715ms | 470ms | docker-langserver |
52
+ | toml | primary | 1622ms | 471ms | taplo |
53
+ | go | primary | 1777ms | 473ms | gopls |
54
+ | prisma | primary | 1935ms | 473ms | @prisma/language-server |
55
+ | zig | primary | 1237ms | 496ms | zls |
56
+ | yaml | primary | 1908ms | 506ms | yaml-language-server |
57
+ | python | primary | 2025ms | 520ms | pyright |
58
+ | rust | primary | 1015ms | 576ms | rust-analyzer |
59
+ | gleam | primary | 1219ms | 585ms | gleam lsp |
60
+ | typescript | primary | 1811ms | 605ms | typescript-language-server |
61
+ | **cpp** | primary | 983ms | 607ms | clangd (#241) |
62
+ | clojure | primary | 5496ms | 624ms | clojure-lsp |
63
+ | **opengrep** | auxiliary | 2156ms | **651ms** | opengrep |
64
+ | css | primary | 2204ms | 967ms | vscode-css-language-server |
65
+ | json | primary | 2014ms | 972ms | vscode-json-language-server |
66
+ | html | primary | 2234ms | 981ms | vscode-html-language-server |
67
+ | shell | primary | 2327ms | 1012ms | bash-language-server |
68
+ | svelte | primary | 3311ms | 1028ms | svelte-language-server |
69
+ | typescript-clean | primary | 1827ms | 1043ms | typescript-language-server (clean file) |
70
+ | php | primary | 1703ms | 1167ms | intelephense |
71
+ | jedi | alternate | 2477ms | 1278ms | jedi (alternate of pyright) |
72
+ | **powershell** | primary | 5053ms | 1401ms | PowerShell Editor Services (#278) |
73
+ | markdown | primary | 2979ms | 1725ms | marksman (#274) |
74
+ | terraform | primary | 2331ms | 1729ms | terraform-ls |
75
+ | fsharp | primary | 2958ms | 1730ms | fsautocomplete (`dotnet tool`, #241) |
76
+ | vue | primary | 5545ms | 1731ms | @vue/language-server |
77
+ | **zizmor** | auxiliary | 962ms | **2158ms†** | zizmor (#272) |
78
+
79
+ **Primary/alternate warm:** min 159ms · avg 870ms · max 1731ms (n=27).
80
+ **Auxiliary warm:** ast-grep 399ms · ast-grep-baseline 394ms · opengrep 651ms —
81
+ all at the **fast end** of the primary band. **zizmor 2158ms†** is a bench
82
+ artifact, not its real per-edit cost — see the caveat below; its true warm/edit is
83
+ **~680ms**, also fast-end.
84
+
85
+ > **† zizmor caveat (the bench number is a no-signal artifact).** The harness's
86
+ > warm edit appends `\n// bench edit N\n`, but `//` is **not** a YAML comment
87
+ > (`#` is) — so each warm touch hands zizmor an *unparseable workflow*. zizmor
88
+ > collects no auditable input and publishes nothing, so the touch has no diagnostic
89
+ > to early-return on and runs to its `aggregateWaitMs` budget (the same class of
90
+ > "no-signal, budget-bound" cost as a clean file — see below). Re-measured with a
91
+ > *valid* edit (appending a `#` comment so the workflow stays parseable + still
92
+ > flagged), zizmor early-returns on the version-matched republish:
93
+ >
94
+ > | edit kind | cold | warm/edit | diags |
95
+ > |---|---:|---:|---:|
96
+ > | bench `//` (unparseable) | 962ms | 2158ms | 1 cold, 0 warm |
97
+ > | valid `#` (parseable, flagged) | 2376ms | **682ms** | 1 every touch |
98
+ >
99
+ > So zizmor's real per-edit latency sits with opengrep (~650ms), not at 2158ms.
100
+
101
+ > **fsautocomplete caveat:** it returned **0 diagnostics** (the fixture isn't a
102
+ > restored .NET project, so the server surfaces nothing), so its 1730ms is the
103
+ > *clean-file* near-budget cost — see the clean-file note below — not a real
104
+ > per-edit-with-diagnostic latency. It is a heavy server regardless; this number is
105
+ > an upper bound, not a typical edit.
106
+
107
+ ### Auxiliary verdict (#239 Gate A, re-confirmed #272)
108
+ **Pass, decisively.** Measured in isolation, every auxiliary's real warm latency is
109
+ at or below the median primary (ast-grep 399ms, opengrep 651ms, zizmor ~682ms vs
110
+ primary avg 870ms). None regresses the hot path.
111
+
112
+ ### Shared with-auxiliary floor on YAML (#272 re-measure)
113
+ Editing a workflow YAML now attaches **three** auxiliaries alongside the yaml
114
+ primary — opengrep, ast-grep, **and zizmor** (all include YAML in their kind set).
115
+ The concern (#272) was that each new auxiliary joins the same `with-auxiliary`
116
+ `Promise.all` and could extend the per-edit floor. It does **not**, because of the
117
+ #242 fix: each attached server gets its **own** deadline bounded by the caller cap
118
+ as a ceiling (`min(callerCap, aggregateWaitMs)`), and they run concurrently — so
119
+ the touch waits for the *slowest single* aux, not their sum. opengrep
120
+ (`min(2500, 3500) = 2500`) already sets that ceiling; zizmor
121
+ (`min(2500, 2000) = 2000`) sits under it and never extends the floor. On a normal
122
+ (parseable) edit all three early-return well inside the cap (~400–680ms). zizmor's
123
+ `aggregateWaitMs` is intentionally left at 2000ms for online-mode (GitHub-API)
124
+ headroom; a late online finding is cached and surfaces on the next edit.
125
+
126
+ ## Resolved: the with-auxiliary measurement artifact (#240 + #242)
127
+
128
+ An earlier full-suite bench reported **ast-grep warm = 20557ms** (and ~2000–3700ms
129
+ at lower caps). That was **never ast-grep being slow** — it was two compounding bugs,
130
+ both now fixed:
131
+
132
+ 1. **The benchmark co-spawned the primary.** An auxiliary fixture was touched with
133
+ `clientScope: "with-auxiliary"`, which spawns the file's primary language server
134
+ *and* the auxiliary, then waits for **all** of them (`Promise.all`). The fixtures
135
+ are clean JS with no TypeScript errors, so the primary (typescript, push-silent on
136
+ a clean file) never published and never early-returned — the touch ran to the
137
+ deadline even though ast-grep published in ~0.5s. **Fix:** the bench now disables
138
+ the primary and measures each server alone (this doc, isolation note above).
139
+ 2. **The with-auxiliary deadline was a floor, not a ceiling.** The collection used
140
+ `timeoutMs = max(callerCap, maxStrategyWait)` for *every* attached server, so a
141
+ large cap *became* the deadline and a slow auxiliary could override the per-edit
142
+ cap. **Fix (#242):** each server now gets its own deadline bounded by the caller
143
+ cap as a ceiling — `min(callerCap, strategyWait)` — so a clean/push-silent primary
144
+ can no longer hold the touch and an auxiliary can't blow the per-edit budget.
145
+ ast-grep's `aggregateWaitMs` was also raised 1000 → 1800 (its scan is ~1.3s; 1000
146
+ was under-budgeted, masked before only by the global floor).
147
+
148
+ Correctness of the early-return signal was hardened separately in **#240**: pull
149
+ diagnostics now return a discriminated `found | clean | unavailable` outcome and a
150
+ failed pull (dead/null/threw) is **never** read as clean — only an affirmative,
151
+ version-matched publish (or authoritative empty pull) ends the wait early. A
152
+ crashed/cold/stale/errored server is never painted "clean".
153
+
154
+ ### Clean-file cost is real and irreducible for push-silent servers
155
+ A server that processes an edit and finds **no new diagnostics** gives the wait
156
+ nothing to trip on, so a clean file costs more than a broken one on the *same* server:
157
+
158
+ | fixture | warm/edit |
159
+ |---|---:|
160
+ | typescript (broken — persistent error, re-published each edit) | 605ms |
161
+ | typescript-clean (no diagnostics) | 1043ms |
162
+
163
+ The clean file is ~1.7× slower: with a persistent diagnostic the server re-publishes
164
+ (version bump → early-return); with nothing to report, a Tier-3 push-silent server
165
+ (typescript publishes nothing on clean) gives no signal, so the touch waits its
166
+ budget. This is **budget-bound by necessity** — silence is irreducibly ambiguous, so
167
+ the cap is the only safe backstop. Tier-2 servers that re-publish empty-with-version
168
+ (ast-grep, opengrep) *do* early-return on clean files, which is why both measure fast
169
+ above even though their fixtures could be clean on a given edit.