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
@@ -20,17 +20,20 @@ import * as fs from "node:fs";
20
20
  import { createRequire } from "node:module";
21
21
  import * as path from "node:path";
22
22
  import { getDegradationLedgerGeneration, incrementDegradationCount, recordDegradation, recordDegradationOnce, } from "./degradation-ledger.js";
23
+ import { BoundedFifoMap } from "./bounded-cache.js";
23
24
  import { loadWebTreeSitter } from "./deps/web-tree-sitter.js";
24
25
  import { transientRetryDelayMs } from "./dispatch/runners/utils/availability-policy.js";
25
26
  import { getProjectIgnoreMatcher, isExcludedDirName } from "./file-utils.js";
26
27
  import { downloadGrammarDetailed, fileHasWasmMagic, grammarBlockReason, grammarFileSha256, isVendoredGrammar, LANGUAGE_TO_GRAMMAR, pinnedGrammarHash, vendoredGrammarRefusal, vendoredGrammarsDir, } from "./grammar-source.js";
27
28
  import { resolvePackagePath } from "./package-root.js";
28
29
  import { assertInstallAllowed, getProjectTrustGeneration, } from "./project-trust.js";
30
+ import { escapeRegExp } from "./string-utils.js";
29
31
  import { logTreeSitterDiagnostic } from "./tree-sitter-logger.js";
30
32
  import { notifyUserDegradation } from "./user-notify.js";
31
33
  const _require = createRequire(import.meta.url);
32
34
  import { createTreeCacheCounters, deriveScanTreeCacheCapacity, TreeCache, } from "./tree-sitter-cache.js";
33
35
  import { TreeSitterNavigator } from "./tree-sitter-navigator.js";
36
+ import { isProvenSqlAlchemySessionReceiver, isSafePsycopgIdentifierComposition, isSqlAlchemyEntityQueryArgument, isSqlAlchemyStatementArgument, PYTHON_SQLALCHEMY_RECEIVER_NAMES, PYTHON_SQLALCHEMY_STATEMENT_BUILDERS, } from "./python-provenance.js";
34
37
  import { TreeSitterQueryLoader, } from "./tree-sitter-query-loader.js";
35
38
  // Hard cap on a single structural-search file walk. Bounds a misrooted scan so
36
39
  // it can't enumerate an unbounded tree synchronously before result collection
@@ -46,6 +49,14 @@ const QUERY_BATCH_MAX_LOAD_FAILURES = 3;
46
49
  // must not stall the batched query walk; the filter fails open when this cap
47
50
  // is reached so unrelated diagnostics and the current match are preserved.
48
51
  const NO_NESTED_ANCHOR_VISIT_CAP = 10_000;
52
+ // The execute-style Python DB-API/ORM methods this rule treats as SQL sinks.
53
+ // Hoisted so the post-filter does not rebuild the set per match.
54
+ const PYTHON_SQL_SINK_METHODS = new Set([
55
+ "execute",
56
+ "executemany",
57
+ "query",
58
+ "raw",
59
+ ]);
49
60
  const NOT_PARSED = { parsed: false };
50
61
  function createParserCounters() {
51
62
  return {
@@ -269,12 +280,19 @@ export class TreeSitterClient {
269
280
  ParserClass = null;
270
281
  // biome-ignore lint/suspicious/noExplicitAny: Language loader from module
271
282
  LanguageLoader = null;
272
- // biome-ignore lint/suspicious/noExplicitAny: Compiled query cache by language+pattern hash
273
- queryCache = new Map();
274
- /** Combined multi-rule queries by language + rule-set identity (null = don't retry). */
275
- queryBatchCache = new Map();
283
+ // Declared BEFORE the two caches below: a static read by an instance field
284
+ // initializer must already be initialized (TS2729).
276
285
  static QUERY_CACHE_MAX_ENTRIES = 256;
277
286
  static QUERY_BATCH_CACHE_MAX_ENTRIES = 256;
287
+ // biome-ignore lint/suspicious/noExplicitAny: Compiled query cache by language+pattern hash
288
+ // BoundedFifoMap, not BoundedLruCache: recency here is refreshed by this
289
+ // class's own explicit delete+set on both the read and the write path (see
290
+ // cacheQuery and the two lookup sites) — exactly the raw-`Map` discipline
291
+ // the FIFO map documents. A get() that promoted on its own would change
292
+ // which entry eviction targets (#2442 review F5/F7).
293
+ queryCache = new BoundedFifoMap(TreeSitterClient.QUERY_CACHE_MAX_ENTRIES);
294
+ /** Combined multi-rule queries by language + rule-set identity (null = don't retry). */
295
+ queryBatchCache = new BoundedFifoMap(TreeSitterClient.QUERY_BATCH_CACHE_MAX_ENTRIES);
278
296
  queryCacheCap() {
279
297
  const value = Number.parseInt(process.env.PI_LENS_TREE_SITTER_QUERY_CACHE_CAP ?? "", 10);
280
298
  return Number.isSafeInteger(value) && value > 0
@@ -287,27 +305,25 @@ export class TreeSitterClient {
287
305
  ? value
288
306
  : TreeSitterClient.QUERY_BATCH_CACHE_MAX_ENTRIES;
289
307
  }
308
+ // biome-ignore lint/suspicious/noExplicitAny: compiled query objects
290
309
  cacheQuery(key, value) {
291
- this.queryCache.delete(key);
292
- this.queryCache.set(key, value);
293
- while (this.queryCache.size > this.queryCacheCap()) {
294
- const oldest = this.queryCache.entries().next().value;
295
- if (!oldest)
296
- break;
297
- this.queryCache.delete(oldest[0]);
298
- oldest[1]?.query?.delete?.();
299
- }
310
+ this.queryCache.delete(key); // write-refresh: this write is the newest
311
+ // The cap is env-readable, so re-apply it before every write — a raised
312
+ // or lowered ceiling has to take effect on the next insert, not on the
313
+ // next restart. Both calls hand back the dropped [key, value] pairs so
314
+ // the evicted compiled query's native handle is still freed (#2442
315
+ // review F5/F7 — this is why set() returns the VALUE, not just the key).
316
+ const evicted = this.queryCache.setMaxEntries(this.queryCacheCap());
317
+ evicted.push(...this.queryCache.set(key, value));
318
+ for (const [, dropped] of evicted)
319
+ dropped?.query?.delete?.();
300
320
  }
301
321
  cacheQueryBatch(key, value) {
302
322
  this.queryBatchCache.delete(key);
303
- this.queryBatchCache.set(key, value);
304
- while (this.queryBatchCache.size > this.queryBatchCacheCap()) {
305
- const oldest = this.queryBatchCache.entries().next().value;
306
- if (!oldest)
307
- break;
308
- this.queryBatchCache.delete(oldest[0]);
309
- oldest[1]?.query?.delete?.();
310
- }
323
+ const evicted = this.queryBatchCache.setMaxEntries(this.queryBatchCacheCap());
324
+ evicted.push(...this.queryBatchCache.set(key, value));
325
+ for (const [, dropped] of evicted)
326
+ dropped?.query?.delete?.();
311
327
  }
312
328
  /** Consecutive grammar-load failures per batch key — bounds load retries (#889). */
313
329
  queryBatchLoadFailures = new Map();
@@ -318,7 +334,13 @@ export class TreeSitterClient {
318
334
  activeMeasurements = 0;
319
335
  onWasmAbort;
320
336
  wasmAborted = false;
321
- constructor(verbose = false, onWasmAbort) {
337
+ grammarDirResolutionDeps;
338
+ constructor(verbose = false, onWasmAbort, grammarDirResolutionDeps) {
339
+ this.grammarDirResolutionDeps = grammarDirResolutionDeps ?? {
340
+ resolveAsset: (asset) => this.resolveWebTreeSitterAsset(asset),
341
+ resolvePackage: (specifier) => _require.resolve(specifier),
342
+ cwd: () => process.cwd(),
343
+ };
322
344
  this.grammarsDir = this.findGrammarsDir();
323
345
  this.verbose = verbose;
324
346
  this.onWasmAbort = onWasmAbort;
@@ -708,8 +730,8 @@ export class TreeSitterClient {
708
730
  });
709
731
  }
710
732
  /** Find tree-sitter grammar directory */
711
- findGrammarsDir() {
712
- const grammarsDir = this.resolveWebTreeSitterAsset("grammars");
733
+ findGrammarsDir(deps = this.grammarDirResolutionDeps) {
734
+ const grammarsDir = deps.resolveAsset("grammars");
713
735
  if (grammarsDir &&
714
736
  fs.existsSync(path.join(grammarsDir, "tree-sitter-typescript.wasm"))) {
715
737
  return grammarsDir;
@@ -717,14 +739,14 @@ export class TreeSitterClient {
717
739
  // Fallback: a real `tree-sitter-wasms` package, if the user installed one
718
740
  // (it is not a pi-lens dependency — grammars ship bundled / lazy-fetched).
719
741
  try {
720
- const wasmsOut = path.join(path.dirname(_require.resolve("tree-sitter-wasms/package.json")), "out");
742
+ const wasmsOut = path.join(path.dirname(deps.resolvePackage("tree-sitter-wasms/package.json")), "out");
721
743
  if (fs.existsSync(wasmsOut))
722
744
  return wasmsOut;
723
745
  }
724
746
  catch {
725
747
  /* fall through */
726
748
  }
727
- const cwdWasms = path.join(process.cwd(), "node_modules", "tree-sitter-wasms", "out");
749
+ const cwdWasms = path.join(deps.cwd(), "node_modules", "tree-sitter-wasms", "out");
728
750
  if (fs.existsSync(cwdWasms))
729
751
  return cwdWasms;
730
752
  return "";
@@ -1442,7 +1464,8 @@ export class TreeSitterClient {
1442
1464
  const perQuery = new Map();
1443
1465
  await this.parseFileAndUse(filePath, languageId, contentOverride, (tree) => {
1444
1466
  try {
1445
- for (const match of batch.query.matches(tree.rootNode)) {
1467
+ const rootNode = tree.rootNode;
1468
+ for (const match of batch.query.matches(rootNode)) {
1446
1469
  const owner = batch.ownerOfPattern[match.patternIndex];
1447
1470
  if (owner === undefined)
1448
1471
  continue;
@@ -1459,7 +1482,7 @@ export class TreeSitterClient {
1459
1482
  if (!this.evaluatePredicates(batch.query, match))
1460
1483
  continue;
1461
1484
  if (entry.postFilter &&
1462
- !this.applyPostFilter(entry.postFilter, entry.postFilterParams, captures, tree.rootNode)) {
1485
+ !this.applyPostFilter(entry.postFilter, entry.postFilterParams, captures, rootNode)) {
1463
1486
  continue;
1464
1487
  }
1465
1488
  if (match.captures.length === 0)
@@ -1830,14 +1853,16 @@ export class TreeSitterClient {
1830
1853
  }
1831
1854
  return false;
1832
1855
  }
1856
+ /**
1857
+ * SQLAlchemy sessions are conventionally bound to one of a handful of
1858
+ * receiver names. Structural provenance (`python-provenance.ts`) proves the
1859
+ * annotated case; this name check keeps the far more common unannotated
1860
+ * `session.execute(stmt)` quiet, as it has been since the exemption was
1861
+ * added. Removing it regresses every codebase that never annotates.
1862
+ */
1833
1863
  isLikelySqlAlchemyReceiver(text) {
1834
1864
  const tail = text.split(".").pop() ?? text;
1835
- return new Set([
1836
- "session",
1837
- "db_session",
1838
- "async_session",
1839
- "sync_session",
1840
- ]).has(tail.toLowerCase());
1865
+ return PYTHON_SQLALCHEMY_RECEIVER_NAMES.has(tail.toLowerCase());
1841
1866
  }
1842
1867
  /**
1843
1868
  * The body statements of a `switch_case`, in order. A switch_case's named
@@ -2391,12 +2416,20 @@ export class TreeSitterClient {
2391
2416
  return null;
2392
2417
  return object.text;
2393
2418
  }
2419
+ /**
2420
+ * `session.execute(select(...))` and friends pass a statement OBJECT, not a
2421
+ * SQL string: parameterized by construction, and far too noisy as blockers.
2422
+ */
2394
2423
  isSafeSqlAlchemyExpressionCall(node) {
2395
2424
  if (node.type !== "call")
2396
2425
  return false;
2397
2426
  const callee = node.children?.[0]?.text ?? "";
2398
2427
  const expression = node.text;
2399
- return ["select", "insert", "update", "delete"].some((name) => callee === name || expression.startsWith(`${name}(`));
2428
+ for (const name of PYTHON_SQLALCHEMY_STATEMENT_BUILDERS) {
2429
+ if (callee === name || expression.startsWith(`${name}(`))
2430
+ return true;
2431
+ }
2432
+ return false;
2400
2433
  }
2401
2434
  /**
2402
2435
  * Post-filter predicate: returns true if the match should be kept, false to skip.
@@ -2463,15 +2496,6 @@ export class TreeSitterClient {
2463
2496
  }
2464
2497
  return slots;
2465
2498
  }
2466
- /**
2467
- * Escape a string for safe interpolation into a `RegExp` source —
2468
- * needed anywhere an identifier's raw text is combined with regex
2469
- * metacharacters like `\b` word boundaries (see
2470
- * "not_closed_or_try_with_resources" below; #1089 P2).
2471
- */
2472
- function escapeRegExp(s) {
2473
- return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2474
- }
2475
2499
  switch (postFilter) {
2476
2500
  case "no_nested_anchor_chain": {
2477
2501
  // Tree-sitter queries can express the opening-tag shape, but they
@@ -3540,20 +3564,39 @@ export class TreeSitterClient {
3540
3564
  /^(Open|OpenFile|ReadFile|WriteFile|Create|Remove|RemoveAll)$/.test(captures.FN?.text ?? ""));
3541
3565
  case "py_sql_injection_sink": {
3542
3566
  const fn = captures.FN?.text ?? "";
3543
- if (!new Set(["execute", "executemany", "query", "raw"]).has(fn)) {
3567
+ if (!PYTHON_SQL_SINK_METHODS.has(fn))
3544
3568
  return false;
3545
- }
3546
3569
  const sqlNode = captures.SQL;
3547
- const receiver = captures.OBJ?.text ?? "";
3548
- // SQLAlchemy ORM sessions execute expression objects, not raw SQL
3549
- // strings. `session.execute(stmt)` and `session.execute(select(...))`
3550
- // are parameterized by construction and were too noisy as blockers.
3551
- if (fn === "execute" && this.isLikelySqlAlchemyReceiver(receiver)) {
3570
+ const receiver = captures.OBJ;
3571
+ // Pre-existing exemptions — kept verbatim so no current user
3572
+ // regresses (#2577 review): an unannotated session receiver, and a
3573
+ // statement-builder call in argument position.
3574
+ if (fn === "execute" &&
3575
+ this.isLikelySqlAlchemyReceiver(receiver?.text ?? "")) {
3552
3576
  return false;
3553
3577
  }
3554
3578
  if (sqlNode && this.isSafeSqlAlchemyExpressionCall(sqlNode)) {
3555
3579
  return false;
3556
3580
  }
3581
+ // #2576: a receiver PROVEN to be a sqlalchemy Session/AsyncSession.
3582
+ // `Session.query` takes entity classes and `Session.execute` a
3583
+ // statement object (a builder call, or a name bound to one —
3584
+ // `stmt = select(User)`, which the check above cannot see). Neither
3585
+ // suppression may swallow composed SQL text (#2577 review F1/F2).
3586
+ if (isProvenSqlAlchemySessionReceiver(receiver, rootNode)) {
3587
+ if (fn === "query" &&
3588
+ isSqlAlchemyEntityQueryArgument(sqlNode, rootNode)) {
3589
+ return false;
3590
+ }
3591
+ if (fn === "execute" &&
3592
+ isSqlAlchemyStatementArgument(sqlNode, rootNode)) {
3593
+ return false;
3594
+ }
3595
+ }
3596
+ // #2576: psycopg `sql.SQL("...").format(sql.Identifier(...))`.
3597
+ if (isSafePsycopgIdentifierComposition(sqlNode, rootNode)) {
3598
+ return false;
3599
+ }
3557
3600
  return true;
3558
3601
  }
3559
3602
  case "go_sql_injection_sink":
@@ -3862,7 +3905,8 @@ export class TreeSitterClient {
3862
3905
  const matches = [];
3863
3906
  await this.parseFileAndUse(filePath, languageId, contentOverride, (tree) => {
3864
3907
  try {
3865
- const queryMatches = query.matches(tree.rootNode);
3908
+ const rootNode = tree.rootNode;
3909
+ const queryMatches = query.matches(rootNode);
3866
3910
  for (const match of queryMatches) {
3867
3911
  const captures = {};
3868
3912
  for (const capture of match.captures) {
@@ -3877,7 +3921,7 @@ export class TreeSitterClient {
3877
3921
  continue;
3878
3922
  }
3879
3923
  if (postFilter &&
3880
- !this.applyPostFilter(postFilter, postFilterParams, captures, tree.rootNode)) {
3924
+ !this.applyPostFilter(postFilter, postFilterParams, captures, rootNode)) {
3881
3925
  continue;
3882
3926
  }
3883
3927
  if (match.captures.length > 0) {
@@ -1,9 +1,10 @@
1
1
  import * as path from "node:path";
2
2
  import { isTestMode } from "./env-utils.js";
3
- import { getGlobalPiLensDir } from "./file-utils.js";
3
+ import { getGlobalPiLensLogDir } from "./probe-home-state.js";
4
4
  import { createNdjsonLogger } from "./ndjson-logger.js";
5
5
  import { getMaxLogSizeMB } from "./log-cleanup.js";
6
- const TREE_SITTER_LOG_DIR = getGlobalPiLensDir();
6
+ import { normalizeLoggedPath } from "./path-utils.js";
7
+ const TREE_SITTER_LOG_DIR = getGlobalPiLensLogDir();
7
8
  const TREE_SITTER_LOG_FILE = path.join(TREE_SITTER_LOG_DIR, "tree-sitter.log");
8
9
  const writer = createNdjsonLogger({
9
10
  filePath: TREE_SITTER_LOG_FILE,
@@ -27,11 +28,26 @@ const CACHE_COUNTER_KEYS = [
27
28
  "parserDurationMs",
28
29
  "parserFailures",
29
30
  ];
31
+ /**
32
+ * #2219 (the #2141 class): `filePath` mixes raw `path.resolve()`/`cwd`
33
+ * values (`project-diagnostics/scanner.ts`, `review-graph/builder.ts`,
34
+ * `tree-sitter-shared.ts`) with already-normalized `ctx.filePath` (the
35
+ * `dispatch/runners/tree-sitter.ts` call sites) and the `"<tree-sitter>"`
36
+ * sentinel `logTreeSitterDiagnostic` falls back to below. This is the single
37
+ * seam every one of those paths funnels through (`logTreeSitterCacheStats`
38
+ * and `logTreeSitterDiagnostic` both call this function), so normalize once
39
+ * here — guarded via `normalizeLoggedPath` so the sentinel passes through
40
+ * unchanged instead of being resolved against the process cwd.
41
+ */
30
42
  export function logTreeSitter(entry) {
31
43
  if (isTestMode()) {
32
44
  return;
33
45
  }
34
- writer.log({ ts: new Date().toISOString(), ...entry });
46
+ writer.log({
47
+ ts: new Date().toISOString(),
48
+ ...entry,
49
+ filePath: normalizeLoggedPath(entry.filePath),
50
+ });
35
51
  }
36
52
  export function logTreeSitterCacheStats(options) {
37
53
  const delta = Object.fromEntries(CACHE_COUNTER_KEYS.map((key) => [key, options.stats[key]]));
@@ -7,7 +7,51 @@
7
7
  import { logTreeSitterDiagnostic } from "./tree-sitter-logger.js";
8
8
  import * as fs from "node:fs";
9
9
  import * as path from "node:path";
10
+ import { classifyBundledResourceDir, reportBundledResourceDirHealth, } from "./bundled-resource-health.js";
11
+ import { getDegradationLedgerGeneration } from "./degradation-ledger.js";
10
12
  import { resolvePackagePath } from "./package-root.js";
13
+ /**
14
+ * The bundled `rules/tree-sitter-queries` root — the ONE spelling of this
15
+ * path, imported by `clients/cache/rule-cache.ts` (re-exported there as
16
+ * `BUNDLED_RULES_ROOT`, #2636 review F4) rather than computed a second time,
17
+ * so the "one ledger row, not two" claim for `tree-sitter-queries-dir-missing`
18
+ * rests on reference equality, not merely two independently-resolved strings
19
+ * that happen to match.
20
+ */
21
+ export const BUNDLED_QUERIES_ROOT = resolvePackagePath(import.meta.url, "rules", "tree-sitter-queries");
22
+ let cachedBundledQueriesRootHealth;
23
+ let cachedBundledQueriesRootHealthGeneration;
24
+ /**
25
+ * #2636 review F6/round-2 F3: memoized, but keyed on the degradation
26
+ * ledger's OWN generation counter (bumped by `resetDegradationLedger`, wired
27
+ * into `handleSessionStart`) rather than "compute once, forever" — round 1's
28
+ * version overclaimed that the underlying fact "cannot change mid-process
29
+ * any more than the package's own install location can", but a managed
30
+ * extension cache RELOCATING a live install mid-process is exactly the
31
+ * failure #2587/#2626 investigated: a permanently-cached "absent" verdict
32
+ * from the FIRST probe would never notice the directory coming back (or
33
+ * disappearing later), the same silent-zero shape this issue exists to end.
34
+ * Re-probing once per SESSION (not once per call) is the right middle
35
+ * ground: a real, measured `readdirSync` cost (4.2 µs) is paid once per
36
+ * session rather than on every dispatched file — same generation-keyed
37
+ * pattern `clients/ast-grep-client.ts`'s `ensureRulesHealthReported` uses
38
+ * for its own per-instance re-check.
39
+ */
40
+ export function getBundledQueriesRootHealth() {
41
+ const generation = getDegradationLedgerGeneration();
42
+ if (cachedBundledQueriesRootHealth === undefined ||
43
+ cachedBundledQueriesRootHealthGeneration !== generation) {
44
+ cachedBundledQueriesRootHealthGeneration = generation;
45
+ cachedBundledQueriesRootHealth =
46
+ classifyBundledResourceDir(BUNDLED_QUERIES_ROOT);
47
+ }
48
+ return cachedBundledQueriesRootHealth;
49
+ }
50
+ /** Test-only: clear the memo so a scenario can simulate a different install layout. */
51
+ export function _resetBundledQueriesRootHealthForTests() {
52
+ cachedBundledQueriesRootHealth = undefined;
53
+ cachedBundledQueriesRootHealthGeneration = undefined;
54
+ }
11
55
  export function isDisabledQueryDirectoryName(name) {
12
56
  return name.endsWith("-disabled");
13
57
  }
@@ -60,7 +104,7 @@ export function ruleFilesForLanguage(languageId, rootDir = process.cwd()) {
60
104
  for (const lang of ruleSourceLanguages(languageId)) {
61
105
  for (const dir of [
62
106
  path.join(resolvedRoot, "rules", "tree-sitter-queries", lang),
63
- resolvePackagePath(import.meta.url, "rules", "tree-sitter-queries", lang),
107
+ path.join(BUNDLED_QUERIES_ROOT, lang),
64
108
  ]) {
65
109
  if (!fs.existsSync(dir))
66
110
  continue;
@@ -70,6 +114,42 @@ export function ruleFilesForLanguage(languageId, rootDir = process.cwd()) {
70
114
  }
71
115
  }
72
116
  }
117
+ // #2636 (review F2): a language resolving zero files here is NORMAL when
118
+ // nobody has authored bundled/project queries for it — seven REACHABLE
119
+ // grammars have none by design: bash, dart, elixir, lua, ocaml, swift, zig
120
+ // (`.sh`/`.bash`, `.dart`, `.ex`/`.exs`, `.lua`, `.ml`/`.mli`, `.swift`,
121
+ // `.zig` — see `language-registry.ts`'s `EXTENSION_TO_GRAMMAR`). cobol and
122
+ // plsql are NOT in that registry at all — only their `-disabled` query
123
+ // directories exist — so `ruleFilesForLanguage` never actually resolves
124
+ // those two languageIds in production; they are not examples of this
125
+ // case. A BUG looks identical from an empty `files` set alone: the
126
+ // bundled root itself relocated out from under the package (same
127
+ // managed-cache-relocation shape #2626 fixed for skills/). Only pay for
128
+ // the extra classification in this COLD branch (never on the common,
129
+ // non-empty path), key it on the shared ROOT rather than this call's
130
+ // `languageId` (every language hitting an actually-missing root collapses
131
+ // into the SAME ledger row instead of one per language), and only RECORD
132
+ // when the root is actually unhealthy — one of the seven by-design-empty
133
+ // languages is touched routinely (any `.sh`/`.lua` edit).
134
+ //
135
+ // #2636 review round 2, F2: NO separate phase/latency row here (unlike
136
+ // the ast-grep/skills sibling sites, which log one PER CONSTRUCTION or
137
+ // PER REQUEST — a bounded cardinality). This branch runs on EVERY
138
+ // dispatched file while the root stays broken (AGENTS.md's "no raw
139
+ // per-occurrence log for repeats"): 200 touches of a by-design-empty
140
+ // language against a broken root would otherwise write 200 raw
141
+ // `latency.log` rows. `reportBundledResourceDirHealth`'s
142
+ // `incrementDegradationCount` already answers "did this run" AND "how
143
+ // many times" in a BOUNDED way — one row per session at count 1, then
144
+ // only at power-of-two milestones (1, 2, 4, 8, … so 200 occurrences write
145
+ // exactly 8 durable rows) — so a second, unbounded record here would add
146
+ // nothing the ledger row lacks.
147
+ if (files.size === 0) {
148
+ const health = getBundledQueriesRootHealth();
149
+ if (health.status !== "healthy") {
150
+ reportBundledResourceDirHealth("tree-sitter-queries-dir-missing", BUNDLED_QUERIES_ROOT, health, "bundled tree-sitter query rules");
151
+ }
152
+ }
73
153
  return [...files];
74
154
  }
75
155
  /**
@@ -16,6 +16,7 @@
16
16
  import { notifyUserDegradation } from "./user-notify.js";
17
17
  import * as path from "node:path";
18
18
  import { TreeSitterClient, } from "./tree-sitter-client.js";
19
+ import { EXTENSION_TO_GRAMMAR } from "./language-registry.js";
19
20
  import { logTreeSitter } from "./tree-sitter-logger.js";
20
21
  let _shared = null;
21
22
  let _wasmAborted = false;
@@ -80,56 +81,60 @@ export function _resetSharedTreeSitterClientForTests() {
80
81
  _wasmAborted = false;
81
82
  _wasmAbortedAt = undefined;
82
83
  }
83
- // Grammar selection by extension — the single ext→grammar-id authority. `.tsx` →
84
- // the tsx grammar (parses JSX); `.jsx` → the javascript grammar. The project
85
- // scanner (project-diagnostics/scanner.ts) DERIVES its map from this one and
86
- // module-report (module-report.ts `tsLangForFile`, #887) resolves its
87
- // extension-split kinds (jsts/cxx) through `resolveTreeSitterLanguage` below,
88
- // so neither can drift: post-#877 both key `.tsx`→tsx (the old note here said the
89
- // scanner mapped `.tsx`→typescript to reuse typescript queries — that stopped
90
- // being true when #877 moved typescript-rule inheritance into
91
- // `queriesForLanguage`). The scanner layers only java/kotlin on top, whose
92
- // grammars + rule dirs exist but are not wired into a per-edit runner `appliesTo`.
93
- export const EXT_TO_LANG = {
94
- ".ts": "typescript",
95
- ".mts": "typescript",
96
- ".cts": "typescript",
97
- ".tsx": "tsx",
98
- ".js": "javascript",
99
- ".mjs": "javascript",
100
- ".cjs": "javascript",
101
- ".jsx": "javascript",
102
- ".py": "python",
103
- ".go": "go",
104
- ".rs": "rust",
105
- ".rb": "ruby",
106
- ".c": "c",
107
- ".h": "c",
108
- ".cc": "cpp",
109
- ".cpp": "cpp",
110
- ".cxx": "cpp",
111
- ".c++": "cpp",
112
- ".hh": "cpp",
113
- ".hpp": "cpp",
114
- ".hxx": "cpp",
115
- ".inl": "cpp",
116
- ".ipp": "cpp",
117
- ".tpp": "cpp",
118
- ".txx": "cpp",
119
- ".cu": "cpp",
120
- ".hip": "cpp",
121
- ".cs": "csharp",
122
- ".php": "php",
123
- ".phtml": "php",
124
- ".php3": "php",
125
- ".php4": "php",
126
- ".php5": "php",
127
- ".css": "css",
128
- };
84
+ // Grammar selection by extension — projected from the canonical language
85
+ // registry (language-registry.ts, #2424), which is now THE ext→grammar
86
+ // authority. `.tsx` → the tsx grammar (parses JSX); `.jsx` → the javascript
87
+ // grammar. The project scanner (project-diagnostics/scanner.ts), read expansion
88
+ // (read-expansion.ts) and module-report (`tsLangForFile`, #887) all project the
89
+ // same registry column, so none of them can drift from this one: before #2424
90
+ // the scanner spread this map and layered java/kotlin on top while read
91
+ // expansion kept a hand-copied near-copy of it.
92
+ // `Readonly` because the projection is `Object.freeze`d at the source, and the
93
+ // mutable `Record` type let a caller type-check a write that would throw at
94
+ // runtime (#2424 review, S4).
95
+ export const EXT_TO_LANG = EXTENSION_TO_GRAMMAR;
129
96
  /** Resolve a tree-sitter grammar/language id from a file path's extension. */
130
97
  export function resolveTreeSitterLanguage(filePath) {
131
98
  return EXT_TO_LANG[path.extname(filePath).toLowerCase()];
132
99
  }
100
+ /**
101
+ * The grammars `clients/complexity-client.ts` has a node mapping for.
102
+ *
103
+ * Lives HERE, beside the ext→grammar resolver, because the question "can this
104
+ * path produce a complexity baseline?" has to be answerable WITHOUT loading
105
+ * the analyzer graph (#2467): `clients/runtime-tool-call.ts` asks it on every
106
+ * tool call to decide whether the seventeen-module load is worth starting, and
107
+ * the client that owns the answer is behind that very load.
108
+ *
109
+ * It is not a second extension table — the extension half still comes from the
110
+ * one canonical registry through {@link resolveTreeSitterLanguage}. It is the
111
+ * language-id half, and `LANGUAGE_NODES` is typed
112
+ * `Record<ComplexityLanguageId, …>`, so adding a grammar there without adding
113
+ * it here (or the reverse) is a COMPILE error rather than a predicate that
114
+ * quietly drifts from the client it is supposed to mirror.
115
+ */
116
+ export const COMPLEXITY_LANGUAGE_IDS = [
117
+ "typescript",
118
+ "tsx",
119
+ "javascript",
120
+ "python",
121
+ "go",
122
+ "rust",
123
+ ];
124
+ /** Narrow a resolved grammar id to one complexity can analyze. */
125
+ export function isComplexityLanguageId(languageId) {
126
+ return COMPLEXITY_LANGUAGE_IDS.includes(languageId);
127
+ }
128
+ /**
129
+ * Can a complexity baseline be produced for this path? Pure, synchronous, and
130
+ * free of every analyzer module — the guard `handleToolCallImpl` needs above
131
+ * its demand on the bootstrap seam. `ComplexityClient.isSupportedFile`
132
+ * delegates to it, so there is exactly one answer.
133
+ */
134
+ export function isComplexitySupportedFile(filePath) {
135
+ const languageId = resolveTreeSitterLanguage(filePath);
136
+ return languageId !== undefined && isComplexityLanguageId(languageId);
137
+ }
133
138
  export async function withTreeSitterRoot(filePath, content, consume) {
134
139
  const languageId = resolveTreeSitterLanguage(filePath);
135
140
  const client = getSharedTreeSitterClient();
@@ -3,6 +3,7 @@
3
3
  * Extracts definitions and references from source files
4
4
  */
5
5
  import { logTreeSitterDiagnostic } from "./tree-sitter-logger.js";
6
+ import { EXTENSION_TO_GRAMMAR, KIND_TO_GRAMMAR } from "./language-registry.js";
6
7
  import * as path from "node:path";
7
8
  import { loadWebTreeSitter } from "./deps/web-tree-sitter.js";
8
9
  // Tree-sitter query patterns for symbol extraction
@@ -563,6 +564,33 @@ SYMBOL_QUERIES.javascript = {
563
564
  constructor: (identifier) @newIdent) @newRef
564
565
  `,
565
566
  };
567
+ /**
568
+ * The grammar a KIND-keyed consumer (review-graph builder, module-report
569
+ * outline) should extract `filePath` with, or undefined when this file is not
570
+ * something the symbol extractor can read.
571
+ *
572
+ * Three registry-derived steps, no hand-kept table (#2424 replaced one switch
573
+ * in review-graph/builder.ts and one map in module-report.ts with this):
574
+ * 1. the kind must have a single grammar-bearing language, or declare a
575
+ * `kindFallback` one — so extension-split `jsts` is excluded and its
576
+ * callers keep resolving it by path;
577
+ * 2. the file's own extension wins where it has grammar wiring, which is what
578
+ * splits `cxx` into `.c`/`.h` -> c and the rest -> cpp;
579
+ * 3. the grammar must actually have symbol queries here, which is what keeps
580
+ * css/json/yaml/html/toml (grammars that ship, queries that do not) out.
581
+ */
582
+ export function symbolExtractionGrammar(kind, filePath) {
583
+ if (!kind)
584
+ return undefined;
585
+ const kindGrammar = KIND_TO_GRAMMAR[kind];
586
+ if (!kindGrammar)
587
+ return undefined;
588
+ const byExtension = filePath
589
+ ? EXTENSION_TO_GRAMMAR[path.extname(filePath).toLowerCase()]
590
+ : undefined;
591
+ const grammar = byExtension ?? kindGrammar;
592
+ return grammar in SYMBOL_QUERIES ? grammar : undefined;
593
+ }
566
594
  /** Language keys with symbol queries; used by fixture drift guards. */
567
595
  export function getSymbolQueryLanguages() {
568
596
  return Object.keys(SYMBOL_QUERIES);
@@ -19,6 +19,11 @@ export const LOCAL_TYPOS_CONFIG_NAMES = [
19
19
  "_typos.toml",
20
20
  ".typos.toml",
21
21
  ];
22
+ // Deliberately UNCEILINGED at $HOME (refs #2472 review round 3, F1): typos
23
+ // discovers `~/typos.toml` as the user's legitimate global config, and
24
+ // `typos-lsp` merges an injected config with its own discovery (injected
25
+ // wins) — a ceilinged search here silently hid the user's config from that
26
+ // merge and let pi-lens's own shipped `_typos.toml` clobber it instead.
22
27
  export function findLocalTyposConfig(startDir) {
23
28
  return findLocalToolConfig(startDir, LOCAL_TYPOS_CONFIG_NAMES);
24
29
  }
@@ -19,6 +19,7 @@
19
19
  * is a no-op and the ndjson sink remains the sole destination — the message is
20
20
  * never lost, only un-rendered.
21
21
  */
22
+ import { withConfigDiagnosticCode, } from "./config-diagnostic-codes.js";
22
23
  let notifierGetter;
23
24
  /** Called once from the extension entry with a getter over the live `ctx.ui.notify`. */
24
25
  export function wireUserNotifier(portsOrGetter) {
@@ -40,10 +41,13 @@ export function hasUserNotifier() {
40
41
  * `ctx` after a session replacement must degrade to a no-op, not break the
41
42
  * diagnostic path that called it.
42
43
  */
43
- export function notifyUserDegradation(message, level = "warning") {
44
+ export function notifyUserDegradation(message, level = "warning", options) {
44
45
  try {
45
46
  const notify = notifierGetter?.();
46
- notify?.(message, level);
47
+ const coded = options?.code
48
+ ? withConfigDiagnosticCode(message, options.code)
49
+ : message;
50
+ notify?.(coded, level);
47
51
  }
48
52
  catch {
49
53
  // The ndjson sink already holds this message; a dead host is not an error.