pi-lens 4.1.2 → 4.1.4

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 (359) hide show
  1. package/CHANGELOG.md +1308 -0
  2. package/README.md +3 -0
  3. package/config/biome/core.jsonc +11 -2
  4. package/config/dependency-cruiser-eager-allowlist.json +91 -0
  5. package/dist/clients/actionable-warnings-logger.js +18 -3
  6. package/dist/clients/actionable-warnings.js +927 -80
  7. package/dist/clients/agent-behavior-client.js +13 -4
  8. package/dist/clients/ast-grep-client.js +136 -9
  9. package/dist/clients/ast-grep-rule-manager.js +60 -5
  10. package/dist/clients/ast-grep-tool-logger.js +2 -2
  11. package/dist/clients/availability-probe-flight.js +12 -0
  12. package/dist/clients/bash-file-access.js +1 -3
  13. package/dist/clients/biome-client.js +16 -2
  14. package/dist/clients/blocker-freshness.js +34 -0
  15. package/dist/clients/bootstrap.js +509 -73
  16. package/dist/clients/bounded-cache.js +152 -12
  17. package/dist/clients/bounded-telemetry.js +115 -23
  18. package/dist/clients/build-identity.js +95 -0
  19. package/dist/clients/bundled-resource-health.js +113 -0
  20. package/dist/clients/bus-events-logger.js +2 -2
  21. package/dist/clients/bus-publish.js +3 -0
  22. package/dist/clients/cache/rule-cache.js +33 -5
  23. package/dist/clients/cache-manager.js +105 -6
  24. package/dist/clients/cache-observability.js +46 -19
  25. package/dist/clients/cargo-manifest.js +422 -0
  26. package/dist/clients/cascade-logger.js +17 -3
  27. package/dist/clients/code-quality-warnings.js +13 -3
  28. package/dist/clients/complexity-client.js +16 -5
  29. package/dist/clients/config-core/deny.js +221 -0
  30. package/dist/clients/config-core/index.js +47 -0
  31. package/dist/clients/config-core/merge.js +357 -0
  32. package/dist/clients/config-core/normalize.js +340 -0
  33. package/dist/clients/config-core/process-spec.js +248 -0
  34. package/dist/clients/config-core/provenance.js +171 -0
  35. package/dist/clients/config-core/records.js +218 -0
  36. package/dist/clients/config-core/resolve.js +125 -0
  37. package/dist/clients/config-core/safe-object.js +78 -0
  38. package/dist/clients/config-core/schema.js +167 -0
  39. package/dist/clients/config-diagnostic-codes.js +281 -0
  40. package/dist/clients/config-locations.js +160 -0
  41. package/dist/clients/config-resolve.js +789 -0
  42. package/dist/clients/config-schema.js +197 -0
  43. package/dist/clients/config-warn.js +407 -0
  44. package/dist/clients/dead-code-client.js +33 -25
  45. package/dist/clients/dead-code-logger.js +2 -2
  46. package/dist/clients/deadline-utils.js +178 -0
  47. package/dist/clients/debug-handles.js +2 -2
  48. package/dist/clients/debug-heap.js +3 -3
  49. package/dist/clients/deferred-lsp-work.js +106 -0
  50. package/dist/clients/degradation-ledger.js +166 -8
  51. package/dist/clients/demoted-finding-render.js +14 -0
  52. package/dist/clients/dependency-checker.js +28 -22
  53. package/dist/clients/deps/minimatch.js +1 -1
  54. package/dist/clients/diagnostic-line-freshness.js +12 -9
  55. package/dist/clients/diagnostic-logger.js +14 -4
  56. package/dist/clients/dispatch/collect-later-tier.js +39 -0
  57. package/dist/clients/dispatch/dispatcher.js +304 -19
  58. package/dist/clients/dispatch/fact-store.js +328 -9
  59. package/dist/clients/dispatch/integration.js +214 -107
  60. package/dist/clients/dispatch/pending-runner-findings.js +87 -0
  61. package/dist/clients/dispatch/plan.js +21 -5
  62. package/dist/clients/dispatch/runners/actionlint.js +0 -1
  63. package/dist/clients/dispatch/runners/ast-grep-napi.js +550 -77
  64. package/dist/clients/dispatch/runners/biome-check.js +0 -1
  65. package/dist/clients/dispatch/runners/cpp-check.js +0 -1
  66. package/dist/clients/dispatch/runners/credo.js +0 -1
  67. package/dist/clients/dispatch/runners/cue-vet.js +0 -1
  68. package/dist/clients/dispatch/runners/dart-analyze.js +0 -1
  69. package/dist/clients/dispatch/runners/detekt.js +0 -1
  70. package/dist/clients/dispatch/runners/dotnet-build.js +0 -1
  71. package/dist/clients/dispatch/runners/elixir-check.js +0 -1
  72. package/dist/clients/dispatch/runners/eslint.js +5 -2
  73. package/dist/clients/dispatch/runners/fact-rules.js +0 -1
  74. package/dist/clients/dispatch/runners/fish-indent.js +0 -1
  75. package/dist/clients/dispatch/runners/gleam-check.js +0 -1
  76. package/dist/clients/dispatch/runners/go-vet.js +1 -3
  77. package/dist/clients/dispatch/runners/golangci-lint.js +0 -1
  78. package/dist/clients/dispatch/runners/hadolint.js +0 -1
  79. package/dist/clients/dispatch/runners/helm-lint.js +16 -4
  80. package/dist/clients/dispatch/runners/helm-render.js +54 -10
  81. package/dist/clients/dispatch/runners/htmlhint.js +0 -1
  82. package/dist/clients/dispatch/runners/index.js +1 -3
  83. package/dist/clients/dispatch/runners/javac.js +0 -1
  84. package/dist/clients/dispatch/runners/ktlint.js +0 -1
  85. package/dist/clients/dispatch/runners/lsp.js +37 -2
  86. package/dist/clients/dispatch/runners/markdownlint.js +4 -2
  87. package/dist/clients/dispatch/runners/mypy.js +0 -1
  88. package/dist/clients/dispatch/runners/oxlint.js +24 -22
  89. package/dist/clients/dispatch/runners/php-lint.js +0 -1
  90. package/dist/clients/dispatch/runners/phpstan.js +0 -1
  91. package/dist/clients/dispatch/runners/prisma-validate.js +0 -1
  92. package/dist/clients/dispatch/runners/psscriptanalyzer.js +4 -1
  93. package/dist/clients/dispatch/runners/pyright.js +7 -4
  94. package/dist/clients/dispatch/runners/rubocop.js +0 -1
  95. package/dist/clients/dispatch/runners/ruff.js +0 -1
  96. package/dist/clients/dispatch/runners/rust-clippy.js +9 -6
  97. package/dist/clients/dispatch/runners/shellcheck.js +0 -1
  98. package/dist/clients/dispatch/runners/shfmt.js +0 -1
  99. package/dist/clients/dispatch/runners/spellcheck.js +0 -1
  100. package/dist/clients/dispatch/runners/spotbugs.js +0 -1
  101. package/dist/clients/dispatch/runners/sqlfluff.js +0 -1
  102. package/dist/clients/dispatch/runners/stylelint.js +0 -1
  103. package/dist/clients/dispatch/runners/swiftlint.js +0 -1
  104. package/dist/clients/dispatch/runners/taplo.js +0 -1
  105. package/dist/clients/dispatch/runners/terragrunt.js +0 -1
  106. package/dist/clients/dispatch/runners/tflint.js +0 -1
  107. package/dist/clients/dispatch/runners/tree-sitter.js +0 -1
  108. package/dist/clients/dispatch/runners/trivy-config.js +0 -1
  109. package/dist/clients/dispatch/runners/utils/availability-policy.js +3 -0
  110. package/dist/clients/dispatch/runners/utils/diagnostic-parsers.js +3 -4
  111. package/dist/clients/dispatch/runners/utils/runner-helpers.js +270 -105
  112. package/dist/clients/dispatch/runners/utils/spawn-outcome.js +4 -0
  113. package/dist/clients/dispatch/runners/utils/tool-failure.js +19 -7
  114. package/dist/clients/dispatch/runners/utils/toolchain-availability.js +89 -26
  115. package/dist/clients/dispatch/runners/vale.js +0 -1
  116. package/dist/clients/dispatch/runners/yaml-rule-parser.js +51 -17
  117. package/dist/clients/dispatch/runners/yamllint.js +0 -1
  118. package/dist/clients/dispatch/runners/zig-check.js +0 -1
  119. package/dist/clients/disposition-logger.js +3 -3
  120. package/dist/clients/effective-config.js +403 -0
  121. package/dist/clients/error-class.js +23 -0
  122. package/dist/clients/event-loop-hold.js +274 -0
  123. package/dist/clients/event-loop-monitor.js +66 -1
  124. package/dist/clients/extension-log.js +2 -2
  125. package/dist/clients/feature-hints.js +2 -1
  126. package/dist/clients/file-time.js +6 -1
  127. package/dist/clients/file-utils.js +266 -13
  128. package/dist/clients/finding-delivery-gate.js +55 -13
  129. package/dist/clients/format-service.js +6 -1
  130. package/dist/clients/formatters.js +303 -104
  131. package/dist/clients/freshness-cadence.js +17 -0
  132. package/dist/clients/generated-artifacts.js +114 -19
  133. package/dist/clients/generation-guard.js +4 -9
  134. package/dist/clients/git-guard.js +198 -48
  135. package/dist/clients/git-tracked-ignore.js +32 -1
  136. package/dist/clients/go-client.js +39 -0
  137. package/dist/clients/govulncheck-client.js +5 -1
  138. package/dist/clients/gradle-ktfmt-style.js +252 -0
  139. package/dist/clients/gzip-stage-write.js +7 -0
  140. package/dist/clients/hashline-anchor.js +424 -0
  141. package/dist/clients/hook-budgets.js +76 -0
  142. package/dist/clients/host-edit-normalize.js +5 -2
  143. package/dist/clients/host-ports.js +1 -1
  144. package/dist/clients/installer/index.js +200 -84
  145. package/dist/clients/installer/managed-tool-refresh.js +3 -1
  146. package/dist/clients/instance-reaper.js +108 -243
  147. package/dist/clients/instance-registry-lock.js +145 -0
  148. package/dist/clients/instance-registry.js +435 -122
  149. package/dist/clients/jscpd-client.js +6 -1
  150. package/dist/clients/language-profile.js +4 -2
  151. package/dist/clients/language-registry.js +595 -0
  152. package/dist/clients/latency-logger.js +179 -3
  153. package/dist/clients/ledger-bounds.js +13 -0
  154. package/dist/clients/lens-config.js +141 -30
  155. package/dist/clients/lens-engine.js +30 -37
  156. package/dist/clients/lens-flag-registry.js +67 -6
  157. package/dist/clients/lens-map.js +3 -1
  158. package/dist/clients/log-cleanup.js +2 -2
  159. package/dist/clients/lsp/aggregation.js +3 -1
  160. package/dist/clients/lsp/cascade-tier.js +4 -0
  161. package/dist/clients/lsp/client.js +672 -49
  162. package/dist/clients/lsp/config.js +504 -156
  163. package/dist/clients/lsp/diagnostic-binding.js +10 -5
  164. package/dist/clients/lsp/edits.js +15 -2
  165. package/dist/clients/lsp/index.js +1179 -247
  166. package/dist/clients/lsp/jvm-runtime.js +5 -0
  167. package/dist/clients/lsp/language.js +18 -169
  168. package/dist/clients/lsp/launch.js +38 -9
  169. package/dist/clients/lsp/pending-aux-coverage.js +113 -12
  170. package/dist/clients/lsp/server.js +426 -160
  171. package/dist/clients/lsp/session-roots.js +192 -0
  172. package/dist/clients/lsp/spawn-history.js +2 -7
  173. package/dist/clients/lsp/tsserver-sync.js +9 -2
  174. package/dist/clients/lsp/workspace-diagnostics-cache.js +17 -7
  175. package/dist/clients/lsp/workspace-sweep-hold.js +32 -23
  176. package/dist/clients/lsp-mutation.js +179 -23
  177. package/dist/clients/map-with-concurrency.js +36 -0
  178. package/dist/clients/mcp/analyze.js +45 -10
  179. package/dist/clients/mcp/session.js +19 -17
  180. package/dist/clients/memory-sampler.js +28 -1
  181. package/dist/clients/message-end-attribution.js +30 -0
  182. package/dist/clients/metrics-history.js +21 -4
  183. package/dist/clients/middle-man-analysis.js +2 -4
  184. package/dist/clients/module-report.js +20 -45
  185. package/dist/clients/mutating-tool.js +651 -0
  186. package/dist/clients/mutation-attribution.js +368 -0
  187. package/dist/clients/mutation-bridge.js +240 -0
  188. package/dist/clients/ndjson-logger.js +247 -20
  189. package/dist/clients/observed-mutation-sources.js +101 -0
  190. package/dist/clients/observed-mutation.js +1215 -0
  191. package/dist/clients/opaque-mutation-scan.js +300 -40
  192. package/dist/clients/opengrep-config.js +4 -0
  193. package/dist/clients/package-manager.js +201 -44
  194. package/dist/clients/partial-edit-apply.js +359 -80
  195. package/dist/clients/path-attribution-telemetry.js +23 -7
  196. package/dist/clients/path-keyed-map.js +35 -3
  197. package/dist/clients/path-utils.js +460 -13
  198. package/dist/clients/persist-debounce.js +8 -1
  199. package/dist/clients/php-cs-fixer-config.js +114 -0
  200. package/dist/clients/pipeline.js +77 -24
  201. package/dist/clients/probe-home-state.js +228 -0
  202. package/dist/clients/process-bridge.js +66 -0
  203. package/dist/clients/process-singletons.js +210 -0
  204. package/dist/clients/process-snapshot.js +68 -0
  205. package/dist/clients/project-diagnostics/fresh-fetch.js +4 -3
  206. package/dist/clients/project-diagnostics/runner-adapters/madge.js +2 -1
  207. package/dist/clients/project-diagnostics/runner-adapters/runner-findings.js +27 -3
  208. package/dist/clients/project-diagnostics/scanner.js +61 -24
  209. package/dist/clients/project-lens-config.js +393 -122
  210. package/dist/clients/project-snapshot.js +85 -35
  211. package/dist/clients/python-environment.js +217 -0
  212. package/dist/clients/python-provenance.js +639 -0
  213. package/dist/clients/quiet-window.js +6 -1
  214. package/dist/clients/read-bridge.js +13 -21
  215. package/dist/clients/read-expansion.js +9 -46
  216. package/dist/clients/read-guard-logger.js +39 -6
  217. package/dist/clients/read-guard-tool-lines.js +364 -212
  218. package/dist/clients/read-guard.js +95 -7
  219. package/dist/clients/recent-touches.js +17 -5
  220. package/dist/clients/resource-sampler.js +195 -96
  221. package/dist/clients/review-graph/builder.js +752 -132
  222. package/dist/clients/review-graph/service.js +55 -3
  223. package/dist/clients/review-graph/shared-extraction-ir.js +7 -15
  224. package/dist/clients/review-graph/tsconfig-paths.js +5 -1
  225. package/dist/clients/review-graph/workspace-modules.js +10 -74
  226. package/dist/clients/review-graph-logger.js +30 -4
  227. package/dist/clients/runtime-agent-end.js +18 -2
  228. package/dist/clients/runtime-context.js +49 -16
  229. package/dist/clients/runtime-coordinator.js +93 -6
  230. package/dist/clients/runtime-session.js +169 -34
  231. package/dist/clients/runtime-tool-call.js +183 -35
  232. package/dist/clients/runtime-tool-result.js +894 -241
  233. package/dist/clients/runtime-turn.js +1264 -74
  234. package/dist/clients/rust-client.js +31 -0
  235. package/dist/clients/safe-spawn.js +191 -28
  236. package/dist/clients/sanitize.js +7 -1
  237. package/dist/clients/scratch-tree-policy.js +1 -3
  238. package/dist/clients/security-scan-client.js +51 -17
  239. package/dist/clients/session-lifecycle.js +238 -42
  240. package/dist/clients/session-start-observability.js +79 -0
  241. package/dist/clients/sessionstart-logger.js +12 -3
  242. package/dist/clients/sg-runner.js +27 -15
  243. package/dist/clients/sgconfig.js +6 -1
  244. package/dist/clients/shared-checkout-guard.js +338 -0
  245. package/dist/clients/skills-resolver.js +105 -0
  246. package/dist/clients/smells-rollup.js +2 -2
  247. package/dist/clients/spawn-output-cap.js +37 -0
  248. package/dist/clients/spawn-timeout-cooldown.js +15 -1
  249. package/dist/clients/startup-scan.js +6 -7
  250. package/dist/clients/startup-timing.js +69 -4
  251. package/dist/clients/string-utils.js +33 -0
  252. package/dist/clients/subagent-mode.js +17 -4
  253. package/dist/clients/test-runner-client.js +586 -118
  254. package/dist/clients/test-runner-delivery.js +239 -0
  255. package/dist/clients/tool-definition.js +41 -1
  256. package/dist/clients/tool-policy.js +92 -22
  257. package/dist/clients/tree-sitter-cache.js +40 -40
  258. package/dist/clients/tree-sitter-client.js +97 -53
  259. package/dist/clients/tree-sitter-logger.js +19 -3
  260. package/dist/clients/tree-sitter-query-loader.js +81 -1
  261. package/dist/clients/tree-sitter-shared.js +51 -46
  262. package/dist/clients/tree-sitter-symbol-extractor.js +28 -0
  263. package/dist/clients/typos-config.js +5 -0
  264. package/dist/clients/user-notify.js +6 -2
  265. package/dist/clients/warm-attach.js +8 -10
  266. package/dist/clients/widget-state.js +191 -26
  267. package/dist/clients/word-index-logger.js +15 -3
  268. package/dist/clients/word-index-store.js +463 -0
  269. package/dist/clients/word-index.js +815 -125
  270. package/dist/clients/workspace-topology.js +25 -1
  271. package/dist/clients/zizmor-config.js +29 -6
  272. package/dist/index.js +68577 -53950
  273. package/dist/mcp/analyze-cli.js +3 -1
  274. package/dist/mcp/build-staleness.js +8 -7
  275. package/dist/mcp/cli.js +3 -1
  276. package/dist/mcp/server.js +133 -6
  277. package/dist/scripts/lib/process-scan.mjs +583 -0
  278. package/dist/scripts/lib/skills-predicate.mjs +129 -0
  279. package/dist/tools/effective-config.js +89 -0
  280. package/dist/tools/lens-diagnostics.js +99 -24
  281. package/dist/tools/lsp-diagnostics.js +81 -85
  282. package/dist/tools/lsp-navigation.js +40 -17
  283. package/dist/tools/shared.js +0 -1
  284. package/docs/agent-guide.md +68 -3
  285. package/docs/ast-grep_rules_catalog.md +41 -10
  286. package/docs/astplayground.md +26 -17
  287. package/docs/configuration.md +222 -0
  288. package/docs/dependencies.md +3 -3
  289. package/docs/features.md +44 -5
  290. package/docs/language-coverage.md +2 -2
  291. package/docs/pi-lens-fixer.md +25 -0
  292. package/docs/pi-lens-investigator.md +25 -0
  293. package/docs/pi-lens-reviewer.md +27 -0
  294. package/docs/pi-lens-subagent.md +38 -0
  295. package/docs/pi-lens-warden.md +55 -0
  296. package/docs/public-api-stability.md +359 -0
  297. package/docs/release-qa-baseline.md +182 -0
  298. package/docs/subagent-compat.md +110 -30
  299. package/docs/tree-sitter_rules_catalog.md +2 -2
  300. package/package.json +17 -21
  301. package/rules/ast-grep-rules/rule-tests/c-no-malloc-free-test.yml +20 -0
  302. package/rules/ast-grep-rules/rule-tests/cpp-no-malloc-free-test.yml +25 -0
  303. package/rules/ast-grep-rules/rule-tests/cpp-no-printf-test.yml +19 -0
  304. package/rules/ast-grep-rules/rule-tests/go-no-fmt-println-test.yml +19 -0
  305. package/rules/ast-grep-rules/rule-tests/go-no-panic-in-lib-test.yml +17 -0
  306. package/rules/ast-grep-rules/rule-tests/go-no-underscore-func-name-test.yml +18 -0
  307. package/rules/ast-grep-rules/rule-tests/go-prefer-errors-is-test.yml +15 -0
  308. package/rules/ast-grep-rules/rule-tests/go-prefer-string-builder-test.yml +21 -0
  309. package/rules/ast-grep-rules/rule-tests/hyphenated-svg-attribute-test.yml +11 -0
  310. package/rules/ast-grep-rules/rule-tests/kotlin-no-lateinit-test.yml +17 -0
  311. package/rules/ast-grep-rules/rule-tests/kotlin-no-nullable-boolean-test.yml +15 -0
  312. package/rules/ast-grep-rules/rule-tests/kotlin-no-println-test.yml +19 -0
  313. package/rules/ast-grep-rules/rule-tests/kotlin-no-unnecessary-let-test.yml +7 -0
  314. package/rules/ast-grep-rules/rule-tests/kotlin-no-unsafe-cast-test.yml +7 -0
  315. package/rules/ast-grep-rules/rule-tests/kotlin-no-var-test.yml +7 -0
  316. package/rules/ast-grep-rules/rule-tests/kotlin-prefer-data-class-test.yml +25 -0
  317. package/rules/ast-grep-rules/rule-tests/kotlin-prefer-is-empty-test.yml +11 -0
  318. package/rules/ast-grep-rules/rule-tests/no-important-test.yml +21 -0
  319. package/rules/ast-grep-rules/rule-tests/no-raw-types-test.yml +30 -0
  320. package/rules/ast-grep-rules/rule-tests/no-string-concat-in-loop-test.yml +100 -0
  321. package/rules/ast-grep-rules/rule-tests/no-system-out-println-test.yml +7 -0
  322. package/rules/ast-grep-rules/rule-tests/prefer-string-is-empty-test.yml +7 -0
  323. package/rules/ast-grep-rules/rule-tests/python-optional-type-test.yml +9 -0
  324. package/rules/ast-grep-rules/rule-tests/reducible-list-comprehension-test.yml +11 -0
  325. package/rules/ast-grep-rules/rule-tests/ruby-symbol-to-proc-candidate-test.yml +10 -0
  326. package/rules/ast-grep-rules/rule-tests/rust-no-chars-enumerate-test.yml +7 -0
  327. package/rules/ast-grep-rules/rules/c-no-malloc-free.yml +39 -0
  328. package/rules/ast-grep-rules/rules/cpp-no-malloc-free.yml +45 -0
  329. package/rules/ast-grep-rules/rules/cpp-no-printf.yml +41 -0
  330. package/rules/ast-grep-rules/rules/go-no-fmt-println.yml +43 -0
  331. package/rules/ast-grep-rules/rules/go-no-panic-in-lib.yml +41 -0
  332. package/rules/ast-grep-rules/rules/go-no-underscore-func-name.yml +35 -0
  333. package/rules/ast-grep-rules/rules/go-prefer-errors-is.yml +40 -0
  334. package/rules/ast-grep-rules/rules/go-prefer-string-builder.yml +43 -0
  335. package/rules/ast-grep-rules/rules/hyphenated-svg-attribute.yml +32 -0
  336. package/rules/ast-grep-rules/rules/kotlin-no-lateinit.yml +37 -0
  337. package/rules/ast-grep-rules/rules/kotlin-no-nullable-boolean.yml +33 -0
  338. package/rules/ast-grep-rules/rules/kotlin-no-println.yml +42 -0
  339. package/rules/ast-grep-rules/rules/kotlin-no-unnecessary-let.yml +28 -0
  340. package/rules/ast-grep-rules/rules/kotlin-no-unsafe-cast.yml +28 -0
  341. package/rules/ast-grep-rules/rules/kotlin-no-var.yml +35 -0
  342. package/rules/ast-grep-rules/rules/kotlin-prefer-data-class.yml +66 -0
  343. package/rules/ast-grep-rules/rules/kotlin-prefer-is-empty.yml +35 -0
  344. package/rules/ast-grep-rules/rules/no-important.yml +21 -0
  345. package/rules/ast-grep-rules/rules/no-raw-types.yml +33 -0
  346. package/rules/ast-grep-rules/rules/no-string-concat-in-loop.yml +90 -0
  347. package/rules/ast-grep-rules/rules/no-system-out-println.yml +16 -0
  348. package/rules/ast-grep-rules/rules/prefer-string-is-empty.yml +11 -0
  349. package/rules/ast-grep-rules/rules/python-optional-type.yml +22 -0
  350. package/rules/ast-grep-rules/rules/reducible-list-comprehension.yml +29 -0
  351. package/rules/ast-grep-rules/rules/ruby-symbol-to-proc-candidate.yml +25 -0
  352. package/rules/ast-grep-rules/rules/rust-no-chars-enumerate.yml +19 -0
  353. package/rules/tree-sitter-queries/python/python-cross-language-method.yml +3 -3
  354. package/rules/tree-sitter-queries/python/python-hallucinated-import.yml +2 -2
  355. package/rules/tree-sitter-queries/python/python-sql-injection.yml +12 -2
  356. package/scripts/analyze-pi-lens-logs.mjs +192 -1
  357. package/scripts/install-selftest.mjs +58 -2
  358. package/scripts/lib/skills-predicate.mjs +129 -0
  359. package/scripts/rpc-load-check.mjs +3 -1
@@ -2,22 +2,24 @@
2
2
  * ast-grep NAPI runner for dispatch system
3
3
  *
4
4
  * Uses @ast-grep/napi for programmatic parsing instead of CLI.
5
- * Handles TypeScript/JavaScript/CSS/HTML files with YAML rule support.
5
+ * The languages it can serve are exactly the grammars that addon bundles —
6
+ * see `NAPI_LANGUAGE_BINDINGS` for the matrix and for what the other twelve
7
+ * catalog languages deliver through instead.
6
8
  *
7
9
  * Replaces CLI-based runners for faster performance (100x speedup).
8
10
  */
9
11
  import * as fs from "node:fs";
10
12
  import * as path from "node:path";
13
+ import { incrementDegradationCount, recordDegradationOnce, } from "../../degradation-ledger.js";
11
14
  import { loadAstGrepNapi, } from "../../deps/ast-grep-napi.js";
12
15
  import { minimatch } from "../../deps/minimatch.js";
13
- import { getAstGrepRuleSources, } from "../../sgconfig.js";
14
16
  import { logLatency } from "../../latency-logger.js";
17
+ import { hasAuxiliaryLspPublishedForRoot } from "../../lsp/index.js";
18
+ import { clearPendingAuxiliaryCoverage, hasPendingAuxiliaryCoverage, recordNapiFallbackCoverage, } from "../../lsp/pending-aux-coverage.js";
19
+ import { getAstGrepRuleSources, } from "../../sgconfig.js";
15
20
  import { hasEslintConfig } from "../../tool-policy.js";
16
21
  import { enabledAuxiliaryLspServerIds } from "../auxiliary-lsp.js";
17
22
  import { classifyDefect } from "../diagnostic-taxonomy.js";
18
- import { recordDegradationOnce } from "../../degradation-ledger.js";
19
- import { isAuxiliaryLspAlive } from "../../lsp/index.js";
20
- import { resolveAstGrepNativeExe } from "../../lsp/wait-policy/index.js";
21
23
  import { PRIORITY } from "../priorities.js";
22
24
  import { calculateRuleComplexity, isOverlyBroadPattern, isStructuredRule, loadYamlRules, loadYamlRulesFresh, MAX_BLOCKING_RULE_COMPLEXITY, } from "./yaml-rule-parser.js";
23
25
  const defaultUnsupportedLanguageLog = new Set();
@@ -200,8 +202,85 @@ export function resetAstGrepNapiLoadState() {
200
202
  sgSessionHold = false;
201
203
  sgHoldReason = undefined;
202
204
  }
203
- // Supported extensions for NAPI
204
- const SUPPORTED_EXTS = [".ts", ".tsx", ".js", ".jsx", ".css", ".html", ".htm"];
205
+ const NAPI_LANGUAGE_BINDINGS = [
206
+ // `.mts`/`.cts` and `.mjs`/`.cjs` are the same two grammars under a
207
+ // module-system-flavored extension; leaving them off the old hand list meant
208
+ // the whole catalog went dark on every ES/CommonJS module file (#2215).
209
+ {
210
+ ruleLanguage: "typescript",
211
+ napiExport: "ts",
212
+ extensions: [".ts", ".mts", ".cts"],
213
+ },
214
+ { ruleLanguage: "tsx", napiExport: "tsx", extensions: [".tsx"] },
215
+ {
216
+ ruleLanguage: "javascript",
217
+ napiExport: "js",
218
+ // `.jsx` stays on the `js` grammar it has always used here. The addon
219
+ // also exports a separate `jsx`, but switching to it is a matching
220
+ // change, not a coverage one, and belongs with its own fixtures.
221
+ extensions: [".js", ".jsx", ".mjs", ".cjs"],
222
+ },
223
+ { ruleLanguage: "css", napiExport: "css", extensions: [".css"] },
224
+ { ruleLanguage: "html", napiExport: "html", extensions: [".html", ".htm"] },
225
+ ];
226
+ const BINDING_BY_EXTENSION = new Map(NAPI_LANGUAGE_BINDINGS.flatMap((binding) => binding.extensions.map((ext) => [ext, binding])));
227
+ const SUPPORTED_RULE_LANGUAGES = NAPI_LANGUAGE_BINDINGS.map((binding) => binding.ruleLanguage);
228
+ /**
229
+ * Rule `language:` tokens the shipped catalog carries that NO bundled napi
230
+ * grammar can parse. Their rules deliver through the ast-grep LSP/CLI, which
231
+ * ships its own grammar set — never in-process. One shared reason, so this is
232
+ * a list of decisions rather than twelve copies of a sentence: the addon
233
+ * bundles six grammars (see `NAPI_LANGUAGE_BINDINGS`) and nothing registers
234
+ * more at runtime.
235
+ *
236
+ * The LSP/CLI half of that claim is measured, not assumed: every entry below
237
+ * was run through the real `ast-grep run -l <lang>` on 2026-08-26 and parsed,
238
+ * with `cobol` as the negative control (`cobol is not supported!`, exit 2). So
239
+ * no catalog language is unreachable by BOTH engines — the issue's
240
+ * "genuinely missing" bucket is empty, and this list is a routing fact rather
241
+ * than a coverage hole.
242
+ *
243
+ * This is the deliberate-exclusion half of the #2215 contract; the served half
244
+ * is derived from the addon itself. `ast-grep-napi-language-coverage.test.ts`
245
+ * reds when a catalog language with enabled rules is in neither half.
246
+ */
247
+ export const AST_GREP_LSP_ONLY_RULE_LANGUAGES = [
248
+ "c",
249
+ "cpp",
250
+ "csharp",
251
+ "go",
252
+ "java",
253
+ "kotlin",
254
+ "php",
255
+ "python",
256
+ "ruby",
257
+ "rust",
258
+ "scala",
259
+ "swift",
260
+ ];
261
+ const LSP_ONLY_RULE_LANGUAGES = new Set(AST_GREP_LSP_ONLY_RULE_LANGUAGES);
262
+ /**
263
+ * How a rule's declared language reaches the user. `unclassified` is the
264
+ * runtime signal that the #2215 class regrew: rules ship for a language nobody
265
+ * decided a delivery route for, so they may run nowhere at all.
266
+ */
267
+ export function deliveryRouteForRuleLanguage(ruleLanguage) {
268
+ if (SUPPORTED_RULE_LANGUAGES.includes(ruleLanguage))
269
+ return "napi";
270
+ return LSP_ONLY_RULE_LANGUAGES.has(ruleLanguage)
271
+ ? "ast-grep-lsp-cli"
272
+ : "unclassified";
273
+ }
274
+ /**
275
+ * Delivery route for one aggregated skip key. A `mismatch:<rule>-><file>` key
276
+ * is a rule whose language this engine DOES serve and that simply isn't this
277
+ * file's grammar, so it still routes through napi on its own files.
278
+ */
279
+ function skipRouteFor(key) {
280
+ return key.startsWith("mismatch:")
281
+ ? "napi"
282
+ : deliveryRouteForRuleLanguage(key);
283
+ }
205
284
  /** Maximum matches per rule to prevent excessive false positives */
206
285
  const MAX_MATCHES_PER_RULE = 10;
207
286
  /** Maximum total diagnostics per file to prevent output spam */
@@ -302,7 +381,7 @@ function matchesRuleIgnores(filePath, root, patterns) {
302
381
  return patterns.some((pattern) => minimatch(rel, pattern, { dot: true }));
303
382
  }
304
383
  export function canHandle(filePath) {
305
- return SUPPORTED_EXTS.includes(path.extname(filePath).toLowerCase());
384
+ return BINDING_BY_EXTENSION.has(path.extname(filePath).toLowerCase());
306
385
  }
307
386
  /**
308
387
  * The TypeScript grammar is a syntactic superset of JavaScript, so a
@@ -327,40 +406,246 @@ export function canHandle(filePath) {
327
406
  * parsed as tsx (ast-grep-tsx-coverage.test.ts) rather than assumed.
328
407
  * Without this exception the entire TS ruleset silently never runs on
329
408
  * `.tsx` files. `TSX`-tagged rules stay `.tsx`-exclusive; the exception
330
- * is TS→TSX only. Returns undefined for extensions this scoping doesn't
331
- * apply to (css/html), where no filtering is added.
409
+ * is TS→TSX only. Returns undefined for an extension outside the matrix.
332
410
  */
333
411
  export function ruleLanguageForFile(filePath) {
334
- const ext = path.extname(filePath).toLowerCase();
335
- switch (ext) {
336
- case ".ts":
337
- return "typescript";
338
- case ".tsx":
339
- return "tsx";
340
- case ".js":
341
- case ".jsx":
342
- return "javascript";
343
- default:
344
- return undefined;
345
- }
412
+ return BINDING_BY_EXTENSION.get(path.extname(filePath).toLowerCase())
413
+ ?.ruleLanguage;
346
414
  }
415
+ /**
416
+ * The grammar for `filePath` on the addon that actually loaded, or undefined
417
+ * when there is none.
418
+ *
419
+ * Both callers (this file's runner and clients/project-diagnostics/scanner.ts)
420
+ * gate on `canHandle` first, so an undefined return HERE means the matrix
421
+ * admitted the file and the addon then dropped it — the silent skip #2215 is
422
+ * about, which is why it records instead of just returning. An extension
423
+ * outside the matrix was never admitted, so it records nothing.
424
+ */
347
425
  export function getLang(filePath, sgModule) {
348
- const ext = path.extname(filePath).toLowerCase();
349
- switch (ext) {
350
- case ".ts":
351
- return sgModule.ts;
352
- case ".tsx":
353
- return sgModule.tsx;
354
- case ".js":
355
- case ".jsx":
356
- return sgModule.js;
357
- case ".css":
358
- return sgModule.css;
359
- case ".html":
360
- case ".htm":
361
- return sgModule.html;
362
- default:
363
- return undefined;
426
+ const binding = BINDING_BY_EXTENSION.get(path.extname(filePath).toLowerCase());
427
+ if (!binding)
428
+ return undefined;
429
+ const lang = sgModule[binding.napiExport];
430
+ if (!lang) {
431
+ recordDegradationOnce({
432
+ kind: "ast-grep-napi-language-unavailable",
433
+ subject: binding.ruleLanguage,
434
+ reason: `@ast-grep/napi exposes no "${binding.napiExport}" grammar; every ${binding.ruleLanguage} rule is skipped in-process this session`,
435
+ });
436
+ }
437
+ return lang;
438
+ }
439
+ /**
440
+ * Budget for embedded-`<script>` evaluation (#2347 review F2/F4). Every nonempty
441
+ * script body is reparsed as JS and EVERY `language: JavaScript` rule runs
442
+ * against it, so the work is `rules * bodies * body-size`, all multiplied by
443
+ * the native addon's per-call overhead. Measured on this seam (full catalog,
444
+ * real addon, 2026-08-30): ~8.5 ms per small body, ~9 s for 500 blocks, and
445
+ * ~19.5 s for one ~1 MiB dense body (the shape that made the byte axis the
446
+ * real bound). The caps apply to EVERY body — first one included — so an
447
+ * oversized single inline script is omitted and recorded like any other
448
+ * over-budget body. Hand-authored HTML carries a handful of small inline
449
+ * scripts, so the caps only ever trip on generated/pathological pages, where a
450
+ * bounded, recorded partial evaluation beats a seconds-long synchronous stall
451
+ * on the per-edit hook. Budget truncation is a coverage loss and is recorded
452
+ * (see `emitHtmlScriptDegradations`).
453
+ */
454
+ export const MAX_SCRIPT_BODIES_EVALUATED = 64;
455
+ /**
456
+ * Cumulative parsed script-body size cap; bounds the `rules * bytes` work.
457
+ * Every body — including the first — must fit within the running total.
458
+ */
459
+ export const MAX_SCRIPT_BODY_BYTES_EVALUATED = 128 * 1024;
460
+ /**
461
+ * UTF-16 code-unit indexes, in the original file, of every line start, newest
462
+ * line last. `@ast-grep/napi` reports line/column/index in UTF-16 code units
463
+ * (verified against 0.45.1 with `é` and emoji), so the line table must be
464
+ * built in the SAME unit; a UTF-8 byte walk would shift every coordinate that
465
+ * follows a multibyte character.
466
+ */
467
+ function computeLineStartsUtf16(content) {
468
+ const starts = [0];
469
+ for (let i = 0; i < content.length; i++) {
470
+ if (content.charCodeAt(i) === 0x0a)
471
+ starts.push(i + 1);
472
+ }
473
+ return starts;
474
+ }
475
+ /** 0-based line and code-unit column for a UTF-16 index, via binary search. */
476
+ function filePositionForIndex(lineStarts, index) {
477
+ let lo = 0;
478
+ let hi = lineStarts.length - 1;
479
+ while (lo < hi) {
480
+ const mid = (lo + hi + 1) >> 1;
481
+ if (lineStarts[mid] <= index)
482
+ lo = mid;
483
+ else
484
+ hi = mid - 1;
485
+ }
486
+ return { line: lo, column: index - lineStarts[lo] };
487
+ }
488
+ /**
489
+ * Extract every `<script>` body from an HTML root and parse it as JavaScript
490
+ * (#2347). Mirrors the ast-grep CLI/LSP: the injection is unconditional —
491
+ * `type` and `src` attributes do not suppress it (verified against the 0.45.1
492
+ * CLI with `text/template`, `application/ld+json`, and `src`-bearing script
493
+ * tags, all injected). Whitespace-only bodies are skipped (nothing to match).
494
+ * A body the JS grammar cannot parse is dropped the way an unparseable `.js`
495
+ * file would be; the failure is counted, never silent. The evaluation budget
496
+ * (count + cumulative bytes) is applied BEFORE any grammar parse. The caller
497
+ * emits the bounded degradation records from the result — collection stays
498
+ * pure so the count evidence is directly testable.
499
+ */
500
+ export function collectHtmlScriptInjections(htmlRoot, sgModule) {
501
+ // SAFETY: `AstGrepNapi` is the loaded addon module's own (typed) shape, so
502
+ // its `js` accessor is guaranteed present while the addon lives; this
503
+ // narrowing only names the subshape this helper consumes (a `js` grammar
504
+ // whose parses expose `findAll`), and the absence check right below
505
+ // degrades to "no embedded coverage" rather than throwing when the addon
506
+ // ever drops the grammar.
507
+ const addon = sgModule;
508
+ if (!addon.js) {
509
+ return {
510
+ injections: [],
511
+ scriptElementCount: 0,
512
+ bodiesEvaluated: 0,
513
+ truncatedBodies: 0,
514
+ parseFailures: 0,
515
+ missingJsGrammar: true,
516
+ htmlScanFailure: false,
517
+ };
518
+ }
519
+ let scripts;
520
+ try {
521
+ scripts = htmlRoot.findAll({ rule: { kind: "script_element" } });
522
+ }
523
+ catch {
524
+ return {
525
+ injections: [],
526
+ scriptElementCount: 0,
527
+ bodiesEvaluated: 0,
528
+ truncatedBodies: 0,
529
+ parseFailures: 0,
530
+ missingJsGrammar: false,
531
+ htmlScanFailure: true,
532
+ };
533
+ }
534
+ // Extract every nonempty body first (text extraction is cheap) so the raw
535
+ // script-element count is exact, then apply the budget BEFORE parsing any
536
+ // of them.
537
+ let scriptElementCount = 0;
538
+ const candidates = [];
539
+ for (const element of scripts) {
540
+ scriptElementCount += 1;
541
+ let raw;
542
+ for (const child of element.children()) {
543
+ if (child.kind() === "raw_text") {
544
+ raw = child;
545
+ break;
546
+ }
547
+ }
548
+ if (!raw)
549
+ continue;
550
+ const body = raw.text();
551
+ if (!body.trim())
552
+ continue;
553
+ candidates.push({
554
+ body,
555
+ startIndex: raw.range().start.index,
556
+ });
557
+ }
558
+ let budgetedBytes = 0;
559
+ const selected = [];
560
+ let truncatedBodies = 0;
561
+ for (const candidate of candidates) {
562
+ const bytes = Buffer.byteLength(candidate.body, "utf8");
563
+ // EVERY body must fit inside both caps — including the first. There is
564
+ // no "one big script" allowance: a single over-cap body (a ~1 MiB dense
565
+ // inline script measured 19.5 s of synchronous rule traversal on this
566
+ // seam) is omitted like any other over-budget body, and the omission is
567
+ // recorded via `truncatedBodies`, so the bounded
568
+ // `ast-grep-napi-html-script-budget` degradation record always explains
569
+ // why that file lost embedded coverage.
570
+ if (selected.length >= MAX_SCRIPT_BODIES_EVALUATED ||
571
+ budgetedBytes + bytes > MAX_SCRIPT_BODY_BYTES_EVALUATED) {
572
+ truncatedBodies += 1;
573
+ continue;
574
+ }
575
+ selected.push(candidate);
576
+ budgetedBytes += bytes;
577
+ }
578
+ const injections = [];
579
+ let parseFailures = 0;
580
+ for (const candidate of selected) {
581
+ try {
582
+ const root = addon.js.parse(candidate.body).root();
583
+ injections.push({
584
+ body: candidate.body,
585
+ startIndex: candidate.startIndex,
586
+ root,
587
+ });
588
+ }
589
+ catch {
590
+ parseFailures += 1;
591
+ }
592
+ }
593
+ return {
594
+ injections,
595
+ scriptElementCount,
596
+ bodiesEvaluated: selected.length,
597
+ truncatedBodies,
598
+ parseFailures,
599
+ missingJsGrammar: false,
600
+ htmlScanFailure: false,
601
+ };
602
+ }
603
+ /**
604
+ * Emit bounded, discriminating degradation records for every embedded-script
605
+ * coverage loss (#2347 review F3). Each failure mode keeps its own kind so the
606
+ * ledger answers WHICH failure and WHICH file; subjects are the normalized file
607
+ * path, bounded by the ledger's own per-kind window. Collection failures
608
+ * (`missingJsGrammar`, `htmlScanFailure`) are once-per-file-per-session
609
+ * categorical events; parse failures and budget truncation are counted per file
610
+ * so the EXACT totals survive past the retained-entry window.
611
+ */
612
+ function emitHtmlScriptDegradations(filePath, result) {
613
+ if (result.missingJsGrammar) {
614
+ recordDegradationOnce({
615
+ kind: "ast-grep-napi-html-js-grammar-missing",
616
+ subject: filePath,
617
+ reason: "addon exposes no js grammar; embedded <script> coverage dropped",
618
+ });
619
+ }
620
+ if (result.htmlScanFailure) {
621
+ recordDegradationOnce({
622
+ kind: "ast-grep-napi-html-script-scan-failed",
623
+ subject: filePath,
624
+ reason: "script_element enumeration failed; embedded <script> coverage dropped",
625
+ });
626
+ }
627
+ if (result.parseFailures > 0) {
628
+ incrementDegradationCount({
629
+ kind: "ast-grep-napi-html-script-parse-failed",
630
+ subject: filePath,
631
+ reason: `${result.parseFailures} of ${result.bodiesEvaluated} script bodies refused to parse as JS`,
632
+ metadata: {
633
+ scriptElementCount: result.scriptElementCount,
634
+ parseFailures: result.parseFailures,
635
+ },
636
+ });
637
+ }
638
+ if (result.truncatedBodies > 0) {
639
+ incrementDegradationCount({
640
+ kind: "ast-grep-napi-html-script-budget",
641
+ subject: filePath,
642
+ reason: `script budget truncated coverage: ${result.bodiesEvaluated}/${result.scriptElementCount} bodies evaluated, ${result.truncatedBodies} dropped`,
643
+ metadata: {
644
+ scriptElementCount: result.scriptElementCount,
645
+ bodiesEvaluated: result.bodiesEvaluated,
646
+ truncatedBodies: result.truncatedBodies,
647
+ },
648
+ });
364
649
  }
365
650
  }
366
651
  function duplicateRuleIds(rules) {
@@ -439,6 +724,38 @@ export function evaluateAstGrepRules(filePath, rootNode, cwd, kind, options = {}
439
724
  const seenRuleIds = new Set();
440
725
  const suppressLinterOverlap = kind === "jsts" && hasEslintConfig(cwd);
441
726
  const fileLang = ruleLanguageForFile(filePath);
727
+ // Embedded `<script>` coverage (#2347): on an HTML file, every script body
728
+ // is parsed as JavaScript and `language: JavaScript` rules run inside it —
729
+ // the exact behavior the ast-grep CLI/LSP has (verified against 0.45.1).
730
+ // `content` + `sgModule` are only needed for HTML; other callers of this
731
+ // shared seam (the per-edit runner and the project scanner both now pass
732
+ // them) cost a no-op on non-HTML files. The collection is budget-bounded
733
+ // and every silent-failure mode plus the budget cut is recorded, so "no
734
+ // embedded findings" stays distinguishable from "coverage was dropped"
735
+ // (#2347 review F2/F3).
736
+ const htmlCollection = fileLang === "html" &&
737
+ options.content !== undefined &&
738
+ options.sgModule !== undefined
739
+ ? collectHtmlScriptInjections(rootNode, options.sgModule)
740
+ : {
741
+ injections: [],
742
+ scriptElementCount: 0,
743
+ bodiesEvaluated: 0,
744
+ truncatedBodies: 0,
745
+ parseFailures: 0,
746
+ missingJsGrammar: false,
747
+ htmlScanFailure: false,
748
+ };
749
+ const htmlInjections = htmlCollection.injections;
750
+ emitHtmlScriptDegradations(filePath, htmlCollection);
751
+ // UTF-16 code-unit index of each line start in the ORIGINAL file, used to
752
+ // translate an injected match's body-relative UTF-16 index back to file
753
+ // line/column. The unit matters: napi positions are UTF-16, so a UTF-8 byte
754
+ // table would shift every coordinate after a multibyte character (#2347
755
+ // review F1).
756
+ const fileLineStarts = htmlInjections.length > 0 && options.content !== undefined
757
+ ? computeLineStartsUtf16(options.content)
758
+ : [];
442
759
  // Unsupported-language skips are expected in bulk (every non-jsts rule in the
443
760
  // catalog, e.g. ~30 Python rules) — aggregate them into ONE latency-log entry
444
761
  // per evaluation instead of per-rule terminal lines (#282 follow-up).
@@ -467,6 +784,25 @@ export function evaluateAstGrepRules(filePath, rootNode, cwd, kind, options = {}
467
784
  {
468
785
  count: ruleIds.length,
469
786
  ruleIds: ruleIds.slice(0, UNSUPPORTED_RULE_ID_SAMPLE_SIZE),
787
+ // #2215: a skip count alone says a rule did not run and
788
+ // nothing about whether it runs anywhere else. `route` names
789
+ // the delivery path (`ast-grep-lsp-cli` for the twelve
790
+ // grammar-less catalog languages); `unclassified` means rules
791
+ // ship for a language nobody decided a route for, which is the
792
+ // class this issue closed regrowing.
793
+ route: skipRouteFor(language),
794
+ // #2347 observability: on an HTML file the per-language skip
795
+ // count is only part of the picture — `mismatch:*->html`
796
+ // entries name a rule family that never enters scripts, and
797
+ // `htmlInlineScriptCount` says whether the file offered any
798
+ // script bodies to enter. A `javascript->html` mismatch here
799
+ // is necessarily `0` (JS rules RUN when scripts exist), so the
800
+ // field makes "no injection target" distinguishable from the
801
+ // same key on a plain non-HTML file, keeping the residual
802
+ // coverage gap countable in production.
803
+ ...(fileLang === "html"
804
+ ? { htmlInlineScriptCount: htmlInjections.length }
805
+ : {}),
470
806
  },
471
807
  ])),
472
808
  },
@@ -520,11 +856,14 @@ export function evaluateAstGrepRules(filePath, rootNode, cwd, kind, options = {}
520
856
  !isStructuredRule(rule)) {
521
857
  continue;
522
858
  }
859
+ // CSS and HTML rules are scoped to their parsed roots.
860
+ // Without this scope, language-tagged rules scan unrelated roots.
861
+ // The file-language mismatch check below enforces this scope.
862
+ // CSS rules therefore run only on CSS roots.
863
+ // This preserves CSS rule findings while avoiding unrelated scans.
864
+ // The fallback and LSP paths share the same parsed-language scope.
523
865
  const lang = rule.language?.toLowerCase();
524
- if (lang &&
525
- lang !== "typescript" &&
526
- lang !== "tsx" &&
527
- lang !== "javascript") {
866
+ if (lang && !SUPPORTED_RULE_LANGUAGES.includes(lang)) {
528
867
  if (!unsupportedLanguageLog.has(lang)) {
529
868
  const ids = newlyUnsupported.get(lang) ?? [];
530
869
  ids.push(rule.id);
@@ -544,9 +883,20 @@ export function evaluateAstGrepRules(filePath, rootNode, cwd, kind, options = {}
544
883
  // rules) goes dark on every .tsx file. `language: TSX` rules
545
884
  // stay tsx-exclusive — they're already scoped to fileLang
546
885
  // "tsx" by the exact-match check.
886
+ // #2347: `language: JavaScript` rules run INSIDE an HTML file's
887
+ // embedded `<script>` bodies instead of being filtered out as a
888
+ // language mismatch — that filter is what closed napi's embedded
889
+ // coverage entirely while the ast-grep CLI/LSP resolves the
890
+ // injections itself. Only when the file has at least one inline
891
+ // script body; with none, a `javascript->html` rule stays a
892
+ // genuine mismatch with nothing to run against (countable via
893
+ // `htmlInlineScriptCount` in the skip record).
894
+ const runsInsideHtmlScript = fileLang === "html" &&
895
+ lang === "javascript" &&
896
+ htmlInjections.length > 0;
547
897
  if (lang && fileLang && lang !== fileLang) {
548
898
  const runsAsTsOnTsx = fileLang === "tsx" && lang === "typescript";
549
- if (!runsAsTsOnTsx) {
899
+ if (!runsAsTsOnTsx && !runsInsideHtmlScript) {
550
900
  const key = `mismatch:${lang}->${fileLang}`;
551
901
  if (!unsupportedLanguageLog.has(key)) {
552
902
  const ids = newlyUnsupported.get(key) ?? [];
@@ -565,7 +915,31 @@ export function evaluateAstGrepRules(filePath, rootNode, cwd, kind, options = {}
565
915
  if (!rule.rule)
566
916
  continue;
567
917
  try {
568
- let matches = [];
918
+ // A `language: JavaScript` rule on an HTML file with inline
919
+ // scripts runs against EACH parsed script body; every other rule
920
+ // runs against the file root. `scope.position` maps a match onto
921
+ // 0-based FILE line/column — injection matches are body-relative
922
+ // and must be translated via the injection's `startIndex` and the
923
+ // file's line-start table, all in UTF-16 code units (#2347); the
924
+ // file-root scope is the node's own range, so its derived
925
+ // id/diagnostic stay byte-identical to the pre-#2347 path.
926
+ const scopes = runsInsideHtmlScript
927
+ ? htmlInjections.map((injection) => ({
928
+ root: injection.root,
929
+ position(match) {
930
+ const start = match.range().start;
931
+ return filePositionForIndex(fileLineStarts, injection.startIndex + start.index);
932
+ },
933
+ }))
934
+ : [
935
+ {
936
+ root: rootNode,
937
+ position(match) {
938
+ const start = match.range().start;
939
+ return { line: start.line, column: start.column };
940
+ },
941
+ },
942
+ ];
569
943
  // Delegate matching to napi's native engine, which handles the
570
944
  // full ast-grep rule grammar (pattern, kind, has/inside/follows/
571
945
  // precedes/stopBy/field/nthChild, any/all/not) plus metavariable
@@ -574,28 +948,41 @@ export function evaluateAstGrepRules(filePath, rootNode, cwd, kind, options = {}
574
948
  // (#663; `NapiConfig.utils: Record<string, Rule>` per
575
949
  // @ast-grep/napi's types, same shape napi already expects for
576
950
  // `rule`/`constraints`). A faithful js-yaml parse feeds the rule
577
- // object straight through. If napi rejects the rule (a malformed
578
- // or invalid-kind rule, or an unresolved `matches:` reference),
579
- // skip it — never silently match nothing through a partial
580
- // interpreter.
951
+ // object straight through. If napi rejects the rule for a scope
952
+ // (a malformed or invalid-kind rule, or an unresolved `matches:`
953
+ // reference), skip that scope — never silently match nothing
954
+ // through a partial interpreter. The reject log fires once per
955
+ // rule; a grammar-level rejection is uniform across scopes.
581
956
  const nativeConfig = { rule: rule.rule };
582
957
  if (rule.constraints)
583
958
  nativeConfig.constraints = rule.constraints;
584
959
  if (rule.utils)
585
960
  nativeConfig.utils = rule.utils;
586
- try {
587
- matches = rootNode.findAll(nativeConfig);
588
- }
589
- catch (err) {
590
- matches = [];
591
- log?.(`ast-grep-napi: rule "${rule.id}" rejected by native engine (${err instanceof Error ? err.message : String(err)})`);
961
+ const collected = [];
962
+ let rejectLogged = false;
963
+ for (const scope of scopes) {
964
+ let found;
965
+ try {
966
+ found = scope.root.findAll(nativeConfig);
967
+ }
968
+ catch (err) {
969
+ if (!rejectLogged) {
970
+ rejectLogged = true;
971
+ log?.(`ast-grep-napi: rule "${rule.id}" rejected by native engine (${err instanceof Error ? err.message : String(err)})`);
972
+ }
973
+ continue;
974
+ }
975
+ for (const match of found) {
976
+ if (collected.length >= maxMatchesPerRule)
977
+ break;
978
+ collected.push({ match, position: scope.position(match) });
979
+ }
980
+ if (collected.length >= maxMatchesPerRule)
981
+ break;
592
982
  }
593
- const limitedMatches = matches.slice(0, maxMatchesPerRule);
594
- for (const match of limitedMatches) {
983
+ for (const { position } of collected) {
595
984
  if (diagnostics.length >= maxTotalDiagnostics)
596
985
  break;
597
- const node = match;
598
- const range = node.range();
599
986
  // #1777: carry the rule's own tier through. The old collapse
600
987
  // (`=== "error" ? "error" : "warning"`) erased hint and info,
601
988
  // so the quiet tier the #1727 anti-slop rules ship at did not
@@ -608,11 +995,11 @@ export function evaluateAstGrepRules(filePath, rootNode, cwd, kind, options = {}
608
995
  const defectClass = classifyDefect(rule.id, "ast-grep-napi", rule.message || rule.id);
609
996
  const ruleFix = explicitRuleFixSuggestion(rule);
610
997
  diagnostics.push({
611
- id: `ast-grep-napi-${range.start.line}-${rule.id}`,
998
+ id: `ast-grep-napi-${position.line}-${rule.id}`,
612
999
  message: `[${rule.metadata?.category || "slop"}] ${rule.message || rule.id}`,
613
1000
  filePath,
614
- line: range.start.line + 1,
615
- column: range.start.column + 1,
1001
+ line: position.line + 1,
1002
+ column: position.column + 1,
616
1003
  severity,
617
1004
  semantic,
618
1005
  tool: "ast-grep-napi",
@@ -640,9 +1027,8 @@ export function evaluateAstGrepRules(filePath, rootNode, cwd, kind, options = {}
640
1027
  // --- Runner Definition ---
641
1028
  const astGrepNapiRunner = {
642
1029
  id: "ast-grep-napi",
643
- appliesTo: ["jsts"],
1030
+ appliesTo: ["jsts", "css", "html"],
644
1031
  priority: PRIORITY.SPECIALIZED_ANALYSIS,
645
- enabledByDefault: true,
646
1032
  skipTestFiles: true,
647
1033
  async run(ctx) {
648
1034
  if (!canHandle(ctx.filePath)) {
@@ -654,20 +1040,31 @@ const astGrepNapiRunner = {
654
1040
  // report against the LSP's `tool: ast-grep` diagnostics. Resume ONLY as the
655
1041
  // fallback when the binary is absent / can't spawn (Gate B).
656
1042
  const astGrepLspEnabled = enabledAuxiliaryLspServerIds((f) => ctx.pi?.getFlag?.(f)).includes("ast-grep");
657
- // Gate B asks whether the LSP will handle this file, not whether a bare
658
- // `ast-grep` command happens to be on PATH. The launcher first tries the
659
- // platform-native package binary, then PATH; mirror that resolution here.
660
- // A live client covers the already-warm case, while a resolvable binary
661
- // covers the cold case before the LSP has spawned for this root.
662
- const astGrepLspAlive = astGrepLspEnabled
663
- ? await isAuxiliaryLspAlive("ast-grep", ctx.filePath)
1043
+ // Gate B asks whether the LSP has proved it can handle this root, not
1044
+ // whether an ast-grep process or binary merely exists.
1045
+ // A first publication covers the warm case. Process liveness and binary
1046
+ // resolution do not: both precede proof that this root can publish findings.
1047
+ const astGrepLspPublished = astGrepLspEnabled
1048
+ ? await hasAuxiliaryLspPublishedForRoot("ast-grep", ctx.filePath)
664
1049
  : false;
665
- let astGrepBinaryResolvable = false;
666
- if (astGrepLspEnabled && !astGrepLspAlive) {
667
- astGrepBinaryResolvable =
668
- Boolean(resolveAstGrepNativeExe()) || (await ctx.hasTool("ast-grep"));
669
- }
670
- if (astGrepLspEnabled && (astGrepLspAlive || astGrepBinaryResolvable)) {
1050
+ if (astGrepLspEnabled && astGrepLspPublished) {
1051
+ // #2324 F2/R2-C: "has this server EVER published for this file" can go
1052
+ // stale — a LATER touch's aux-grace wait can find the server silent
1053
+ // for the CURRENT content while an OLDER revision's publication still
1054
+ // satisfies this per-file gate. Any pending late-aux entry visible
1055
+ // HERE is necessarily a LEFTOVER from an earlier touch, never this
1056
+ // one: the wait that marks a pair for THIS touch runs to completion
1057
+ // (up to its own grace budget, ~1800ms) strictly AFTER this
1058
+ // synchronous Gate-B check returns, so it cannot have marked
1059
+ // anything yet. Re-running napi here would risk the F3 duplicate
1060
+ // this fix closes, so the loss is made observable instead.
1061
+ if (hasPendingAuxiliaryCoverage(ctx.filePath, "ast-grep")) {
1062
+ incrementDegradationCount({
1063
+ kind: "aux-runner-findings-lost",
1064
+ subject: "ast-grep",
1065
+ reason: `Gate B skipped napi for ${ctx.filePath}: a pending late-auxiliary pair from an EARLIER touch is still undelivered while a prior publication satisfies the per-file gate`,
1066
+ });
1067
+ }
671
1068
  return { status: "skipped", diagnostics: [], semantic: "none" };
672
1069
  }
673
1070
  const sgModule = await loadSg();
@@ -718,11 +1115,87 @@ const astGrepNapiRunner = {
718
1115
  catch {
719
1116
  return { status: "skipped", diagnostics: [], semantic: "none" };
720
1117
  }
1118
+ // #2336: napi is standing in for the ast-grep auxiliary LSP exactly when
1119
+ // that server is expected but has not published for this root. Gate B
1120
+ // above already returned for the published case, so reaching here with
1121
+ // the auxiliary enabled IS the substitute role.
1122
+ const runningAsLspSubstitute = astGrepLspEnabled && !astGrepLspPublished;
1123
+ if (runningAsLspSubstitute) {
1124
+ // #2324 F3/R2-A/R2-B: napi is about to ACTUALLY EVALUATE RULES —
1125
+ // every early-return skip above (load failure, missing file,
1126
+ // unresolved language, stat/size/read/parse failure) is now behind
1127
+ // us, so this run genuinely covers the file rather than reporting a
1128
+ // zero-finding no-op. Placing this here (not at Gate-B's decision
1129
+ // point) matters twice over:
1130
+ // - R2-B: a napi run that never reached rule evaluation must NOT
1131
+ // consume anything — an unparseable .html file, say, would
1132
+ // otherwise silence the LSP's legitimate late delivery for a
1133
+ // file napi never actually covered.
1134
+ // - R2-A: recording coverage HERE, keyed by timestamp, lets the
1135
+ // aux-grace wait (clients/lsp/index.ts) — which decides whether
1136
+ // to mark a pending pair for THIS touch strictly AFTER this
1137
+ // synchronous call returns — see that napi already delivered
1138
+ // and skip marking, instead of racing a clear against a mark
1139
+ // that has not been written yet.
1140
+ // The clear below only ever removes a LEFTOVER pair from an
1141
+ // EARLIER touch (this touch's own pair, if the wait decides to
1142
+ // mark one, is marked strictly later) — that pair describes a
1143
+ // PREVIOUS revision this fresh evaluation supersedes, so dropping
1144
+ // it is safe. Unknown pairs are a no-op.
1145
+ recordNapiFallbackCoverage(ctx.filePath);
1146
+ clearPendingAuxiliaryCoverage(ctx.filePath, "ast-grep");
1147
+ }
1148
+ // #2336: the substitute runs at the severity floor the server it replaces
1149
+ // would have used. The ast-grep auxiliary profile states that floor in
1150
+ // words — "the rule severity is deliberate, so preserve ast-grep's
1151
+ // severity semantics: ERROR can block, WARNING/INFO stay advisory"
1152
+ // (clients/dispatch/auxiliary-lsp.ts:179-183). `blockingOnly` drops every
1153
+ // rule whose declared severity is not `error`, which is 380 of the 481
1154
+ // bundled rules, so the substitute delivered a fifth of the coverage the
1155
+ // LSP delivers and the runner reported zero diagnostics in all 255
1156
+ // retained dispatch records. Nothing downstream needs the filter for
1157
+ // noise control: `dispatcher.ts` already shows only `semantic:
1158
+ // "blocking"` diagnostics inline and routes warning-tier ones to
1159
+ // lens_diagnostics, and `tree-sitter.ts:525` already runs its full query
1160
+ // set under `blockingOnly` for the same reason.
1161
+ //
1162
+ // Scoped to the substitute role deliberately. Evaluating the full catalog
1163
+ // costs ~6x more per file (measured on this repo: 2.4 ms -> 23 ms at 68
1164
+ // lines, 19 ms -> 188 ms at 1275 lines), and outside the substitute role
1165
+ // either the LSP is publishing (Gate B skipped napi already) or the user
1166
+ // retired ast-grep with `no-ast-grep`. Paying the cost only when napi is
1167
+ // the sole ast-grep surface buys coverage that nothing else provides.
721
1168
  const diagnostics = evaluateAstGrepRules(ctx.filePath, rootNode, ctx.cwd, ctx.kind, {
722
- blockingOnly: ctx.blockingOnly,
1169
+ blockingOnly: runningAsLspSubstitute ? false : ctx.blockingOnly,
723
1170
  projectRoot: ctx.projectRoot,
724
1171
  log: (message) => ctx.log(message),
1172
+ content,
1173
+ sgModule,
725
1174
  });
1175
+ if (runningAsLspSubstitute) {
1176
+ // #2336 observability: the dispatcher's own runner record carries
1177
+ // `diagnosticCount`, which is what showed 0 of 255. It cannot say WHICH
1178
+ // severity floor produced that count. One line per substitute run —
1179
+ // the same cadence as the runner record it annotates — names the floor
1180
+ // and the tier mix, so a future "napi found nothing" reading can tell
1181
+ // an empty catalog pass from a filtered one.
1182
+ const bySeverity = {};
1183
+ for (const d of diagnostics) {
1184
+ bySeverity[d.severity] = (bySeverity[d.severity] ?? 0) + 1;
1185
+ }
1186
+ logLatency({
1187
+ type: "phase",
1188
+ phase: "astgrep_napi_substitute_floor",
1189
+ filePath: ctx.filePath,
1190
+ durationMs: 0,
1191
+ metadata: {
1192
+ floor: "lsp-equivalent",
1193
+ ctxBlockingOnly: ctx.blockingOnly === true,
1194
+ diagnosticCount: diagnostics.length,
1195
+ bySeverity,
1196
+ },
1197
+ });
1198
+ }
726
1199
  const hasBlocking = diagnostics.some((d) => d.semantic === "blocking");
727
1200
  let semantic = "none";
728
1201
  if (hasBlocking) {