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
@@ -0,0 +1,239 @@
1
+ /**
2
+ * Automatic test-runner delivery for the post-agent idle window (#2366).
3
+ *
4
+ * Test results remain in `test-runner-findings` for pull diagnostics and the
5
+ * commit guard. This module only stages an owner-qualified pointer and
6
+ * appends a non-context custom entry after the host proves that the agent is
7
+ * idle. Provenance validation stays in `peekTestFindings`, the one shared
8
+ * delivery gate for this cache.
9
+ */
10
+ import { emitBounded } from "./bounded-telemetry.js";
11
+ import { peekTestFindings } from "./runtime-context.js";
12
+ import { fitLines } from "./tui-fit.js";
13
+ export const TEST_RUNNER_ENTRY_TYPE = "pilens:test-runner-findings";
14
+ const MAX_PENDING_DELIVERIES = 32;
15
+ const MAX_ENTRY_CONTENT = 12_000;
16
+ const pending = new Map();
17
+ function key(cwd, sessionId, ownerId) {
18
+ // `cwd` comes from the same host session field at turn_end and settle. Keep
19
+ // that canonical value unchanged so cache reads use the identical workspace
20
+ // identity; dispatch contexts already normalize their own path fields.
21
+ return `${ownerId ?? "direct"}\u0000${cwd}\u0000${sessionId}`;
22
+ }
23
+ function boundedContent(content) {
24
+ if (content.length <= MAX_ENTRY_CONTENT)
25
+ return { content, droppedDetailCount: 0 };
26
+ return {
27
+ content: `${content.slice(0, MAX_ENTRY_CONTENT)}\n[…details truncated]`,
28
+ droppedDetailCount: content.length - MAX_ENTRY_CONTENT,
29
+ };
30
+ }
31
+ function record(identity, outcome, delivery, metadata = {}) {
32
+ emitBounded("test_runner_delivery", identity, {
33
+ filePath: delivery.cwd,
34
+ durationMs: 0,
35
+ metadata: {
36
+ outcome,
37
+ sessionId: delivery.sessionId,
38
+ generation: delivery.generation,
39
+ targetCount: delivery.targetCount,
40
+ deliveryBoundary: "agent_settled",
41
+ ownership: "pi",
42
+ droppedDetailCount: 0,
43
+ ...metadata,
44
+ },
45
+ }, {
46
+ capPerTurn: { limit: 8, turnIndex: delivery.generation },
47
+ ledgerKind: outcome === "delivery-failed" || outcome === "capability-unavailable"
48
+ ? "test-runner-delivery"
49
+ : undefined,
50
+ reason: `test runner delivery ${outcome}`,
51
+ });
52
+ }
53
+ function prunePending() {
54
+ while (pending.size > MAX_PENDING_DELIVERIES) {
55
+ const oldest = pending.keys().next();
56
+ if (oldest.done)
57
+ return;
58
+ pending.delete(oldest.value);
59
+ }
60
+ }
61
+ /** Stage the newest result for this session. Clean results supersede failures. */
62
+ export function stageTestRunnerDelivery(args) {
63
+ const deliveryKey = key(args.cwd, args.sessionId, args.owner?.ownerId);
64
+ const prior = pending.get(deliveryKey);
65
+ if (prior && prior.generation > args.generation) {
66
+ const superseded = {
67
+ cwd: args.cwd,
68
+ sessionId: args.sessionId,
69
+ ownerId: args.owner?.ownerId,
70
+ generation: args.generation,
71
+ targetCount: args.targetCount,
72
+ createdAt: Date.now(),
73
+ owner: args.owner,
74
+ };
75
+ record(deliveryKey, "superseded", superseded, {
76
+ currentGeneration: prior.generation,
77
+ });
78
+ return;
79
+ }
80
+ if (!args.hasFindings) {
81
+ if (prior) {
82
+ pending.delete(deliveryKey);
83
+ record(deliveryKey, "superseded", {
84
+ ...prior,
85
+ targetCount: args.targetCount,
86
+ generation: args.generation,
87
+ });
88
+ }
89
+ return;
90
+ }
91
+ const delivery = {
92
+ cwd: args.cwd,
93
+ sessionId: args.sessionId,
94
+ ownerId: args.owner?.ownerId,
95
+ generation: args.generation,
96
+ targetCount: args.targetCount,
97
+ createdAt: Date.now(),
98
+ owner: args.owner,
99
+ };
100
+ pending.set(deliveryKey, delivery);
101
+ prunePending();
102
+ record(deliveryKey, "staged", delivery);
103
+ }
104
+ /** Deliver the latest staged result during a host-confirmed idle window. */
105
+ export function deliverTestRunnerFindings(args) {
106
+ const cwd = args.ctx.cwd ?? process.cwd();
107
+ const deliveryKey = key(cwd, args.sessionId, args.ownerId);
108
+ const delivery = pending.get(deliveryKey);
109
+ if (!delivery)
110
+ return;
111
+ const currentGeneration = args.cacheManager.readCache("test-runner-findings", cwd)?.data?.testRunGeneration;
112
+ if (currentGeneration !== undefined &&
113
+ currentGeneration > delivery.generation) {
114
+ pending.delete(deliveryKey);
115
+ record(deliveryKey, "superseded", delivery, {
116
+ currentGeneration,
117
+ });
118
+ return;
119
+ }
120
+ if (typeof args.ctx.isIdle !== "function") {
121
+ pending.delete(deliveryKey);
122
+ record(deliveryKey, "capability-unavailable", delivery, {
123
+ reason: "host does not expose ctx.isIdle",
124
+ });
125
+ return;
126
+ }
127
+ try {
128
+ if (!args.ctx.isIdle()) {
129
+ record(deliveryKey, "carried", delivery);
130
+ return;
131
+ }
132
+ }
133
+ catch (error) {
134
+ record(deliveryKey, "delivery-failed", delivery, {
135
+ error: String(error).slice(0, 500),
136
+ });
137
+ return;
138
+ }
139
+ const findings = peekTestFindings(args.cacheManager, cwd, args.runtime, true);
140
+ if (!findings) {
141
+ pending.delete(deliveryKey);
142
+ record(deliveryKey, "superseded", delivery);
143
+ return;
144
+ }
145
+ // Recheck immediately before append. The host may accept a prompt between
146
+ // the first check and this synchronous call.
147
+ try {
148
+ if (!args.ctx.isIdle()) {
149
+ record(deliveryKey, "carried", delivery);
150
+ return;
151
+ }
152
+ }
153
+ catch (error) {
154
+ record(deliveryKey, "delivery-failed", delivery, {
155
+ error: String(error).slice(0, 500),
156
+ });
157
+ return;
158
+ }
159
+ // SAFETY: appendEntry is an optional host capability absent from older Pi SDKs.
160
+ const appendEntry = args.pi.appendEntry;
161
+ if (typeof appendEntry !== "function") {
162
+ pending.delete(deliveryKey);
163
+ record(deliveryKey, "capability-unavailable", delivery);
164
+ return;
165
+ }
166
+ const bounded = boundedContent(findings.messages[0]?.content ?? "");
167
+ try {
168
+ appendEntry.call(args.pi, TEST_RUNNER_ENTRY_TYPE, {
169
+ ...bounded,
170
+ sessionId: delivery.sessionId,
171
+ generation: delivery.generation,
172
+ targetCount: delivery.targetCount,
173
+ });
174
+ pending.delete(deliveryKey);
175
+ record(deliveryKey, "delivered", delivery, {
176
+ droppedDetailCount: bounded.droppedDetailCount,
177
+ ageMs: Math.max(0, Date.now() - delivery.createdAt),
178
+ });
179
+ }
180
+ catch (error) {
181
+ record(deliveryKey, "delivery-failed", delivery, {
182
+ error: String(error).slice(0, 500),
183
+ });
184
+ }
185
+ }
186
+ /** Deliver only the result staged by this activation and settled session. */
187
+ export function deliverStagedTestRunnerFindings(args) {
188
+ if (!args?.cwd || !args.ownerId)
189
+ return;
190
+ const delivery = args.sessionId
191
+ ? pending.get(key(args.cwd, args.sessionId, args.ownerId))
192
+ : [...pending.values()]
193
+ .reverse()
194
+ .find((candidate) => candidate.cwd === args.cwd && candidate.ownerId === args.ownerId);
195
+ if (!delivery?.owner)
196
+ return;
197
+ deliverTestRunnerFindings({
198
+ pi: delivery.owner.pi,
199
+ ctx: delivery.owner.getCtx(),
200
+ cacheManager: delivery.owner.cacheManager,
201
+ runtime: delivery.owner.runtime,
202
+ sessionId: delivery.sessionId,
203
+ ownerId: delivery.ownerId,
204
+ });
205
+ }
206
+ export function registerTestRunnerEntryRenderer(pi) {
207
+ // SAFETY: registerEntryRenderer is an optional host capability absent from older Pi SDKs.
208
+ const register = pi.registerEntryRenderer;
209
+ if (typeof register !== "function")
210
+ return false;
211
+ try {
212
+ register.call(pi, TEST_RUNNER_ENTRY_TYPE, renderTestRunnerEntry);
213
+ return true;
214
+ }
215
+ catch {
216
+ return false;
217
+ }
218
+ }
219
+ function renderTestRunnerEntry(entry, _options, theme) {
220
+ const content = entry.data?.content;
221
+ if (typeof content !== "string")
222
+ return undefined;
223
+ const lines = [
224
+ theme.fg("error", "pi-lens test failures"),
225
+ ...content.split("\n"),
226
+ ];
227
+ return {
228
+ render: (width) => fitLines(lines, width),
229
+ invalidate: () => { },
230
+ };
231
+ }
232
+ /** Test-only reset; session ownership prevents cross-session consumption. */
233
+ export function _resetTestRunnerDeliveryForTests() {
234
+ pending.clear();
235
+ }
236
+ /** Clear staged results when a primary session replaces the owning runtime. */
237
+ export function resetTestRunnerDelivery() {
238
+ pending.clear();
239
+ }
@@ -1,7 +1,47 @@
1
+ /**
2
+ * THE final registration boundary for a host-facing tool: everything that
3
+ * must be true of a tool definition the instant before `pi.registerTool`
4
+ * sees it. `index.ts` runs every tool it registers — always-active,
5
+ * situational, and the compact-line-wrapped variants — through this one
6
+ * function, so a property added here reaches every tool without a second
7
+ * call site to keep in sync.
8
+ *
9
+ * Two jobs today:
10
+ * 1. metadata the host requires is present (a non-empty description);
11
+ * 2. an in-flight tool call HOLDS the event loop (#2507). pi-lens unrefs the
12
+ * LSP child, its stdio pipes and several waiting timers so an idle
13
+ * one-shot process can exit; in a headless child with no other referenced
14
+ * handle that let Node exit 0 in the middle of `lsp_diagnostics`. Every
15
+ * tool call therefore takes a counted hold for its own duration — see
16
+ * `clients/event-loop-hold.ts` for why the hold lives there and not in
17
+ * the unref itself.
18
+ */
19
+ import { acquireEventLoopHold } from "./event-loop-hold.js";
1
20
  export function normalizeToolDefinition(tool) {
2
21
  const name = typeof tool.name === "string" ? tool.name.trim() : "tool";
3
22
  const description = typeof tool.description === "string" && tool.description.trim().length > 0
4
23
  ? tool.description
5
24
  : `Use the ${name} tool.`;
6
- return { ...tool, description };
25
+ const execute = tool.execute;
26
+ if (typeof execute !== "function") {
27
+ return { ...tool, description };
28
+ }
29
+ const inner = execute;
30
+ return {
31
+ ...tool,
32
+ description,
33
+ // A plain function, not an arrow: `this` stays whatever the host called
34
+ // the tool with, so a definition whose `execute` reads `this` keeps
35
+ // working. The hold is released in `finally`, so a rejected or aborted
36
+ // call releases exactly like a resolved one.
37
+ execute: async function (...args) {
38
+ const release = acquireEventLoopHold(name);
39
+ try {
40
+ return await inner.apply(this, args);
41
+ }
42
+ finally {
43
+ release();
44
+ }
45
+ },
46
+ };
7
47
  }
@@ -176,7 +176,10 @@ const FORMATTER_POLICY_BY_EXTENSION = new Map([
176
176
  {
177
177
  formatterNames: ["prettier"],
178
178
  defaultFormatter: "prettier",
179
- defaultWhenUnconfigured: true,
179
+ // HTML commonly carries template markers ({{JS}} embeds, Handlebars,
180
+ // Helm-adjacent Go templates). Prettier reinterprets them as code and
181
+ // corrupts them. Opt in via project prettier config; do not run by default.
182
+ defaultWhenUnconfigured: false,
180
183
  gate: "smart-default",
181
184
  },
182
185
  ],
@@ -185,7 +188,7 @@ const FORMATTER_POLICY_BY_EXTENSION = new Map([
185
188
  {
186
189
  formatterNames: ["prettier"],
187
190
  defaultFormatter: "prettier",
188
- defaultWhenUnconfigured: true,
191
+ defaultWhenUnconfigured: false,
189
192
  gate: "smart-default",
190
193
  },
191
194
  ],
@@ -194,7 +197,10 @@ const FORMATTER_POLICY_BY_EXTENSION = new Map([
194
197
  {
195
198
  formatterNames: ["prettier"],
196
199
  defaultFormatter: "prettier",
197
- defaultWhenUnconfigured: true,
200
+ // YAML commonly carries template markers (Helm `{{ .Values.x }}`,
201
+ // Ansible `{{ var }}`). Prettier splits `{{` into `{ {` and reindents
202
+ // embedded templates. Opt in via project prettier config.
203
+ defaultWhenUnconfigured: false,
198
204
  gate: "smart-default",
199
205
  },
200
206
  ],
@@ -203,7 +209,7 @@ const FORMATTER_POLICY_BY_EXTENSION = new Map([
203
209
  {
204
210
  formatterNames: ["prettier"],
205
211
  defaultFormatter: "prettier",
206
- defaultWhenUnconfigured: true,
212
+ defaultWhenUnconfigured: false,
207
213
  gate: "smart-default",
208
214
  },
209
215
  ],
@@ -984,7 +990,7 @@ const TOOL_COMMAND_SPECS = new Map([
984
990
  {
985
991
  command: "markdownlint-cli2",
986
992
  windowsExt: ".cmd",
987
- versionArgs: ["--version"],
993
+ versionArgs: ["--no-globs", "-"],
988
994
  managedToolId: "markdownlint",
989
995
  },
990
996
  ],
@@ -2236,8 +2242,14 @@ export function _getSpotlessGradleReadCountForTests() {
2236
2242
  * executable Gradle source. This is deliberately a small lexical pre-pass,
2237
2243
  * not a Groovy/Kotlin parser. In particular, statically disabled constructs
2238
2244
  * such as `if (false) { ktlint() }` remain a documented false-positive.
2245
+ *
2246
+ * Exported (with `gradleBlockRanges` below) so `clients/gradle-ktfmt-
2247
+ * style.ts` (#2468) reuses this exact pre-pass for the `ktfmt { }` extension
2248
+ * block instead of a second hand-rolled Gradle lexer — the same
2249
+ * single-source-of-truth reuse `clients/cargo-manifest.ts` did for TOML
2250
+ * parsing (#2466).
2239
2251
  */
2240
- function stripGradleCommentsAndStrings(source) {
2252
+ export function stripGradleCommentsAndStrings(source) {
2241
2253
  let result = "";
2242
2254
  let state = "code";
2243
2255
  let quote = "";
@@ -2303,24 +2315,82 @@ function stripGradleCommentsAndStrings(source) {
2303
2315
  }
2304
2316
  return result;
2305
2317
  }
2306
- function namedGradleBlockBodies(source, name) {
2307
- const bodies = [];
2308
- const startPattern = new RegExp(`\\b${name}\\s*\\{`, "g");
2309
- for (const match of source.matchAll(startPattern)) {
2310
- const open = source.indexOf("{", match.index);
2311
- let depth = 1;
2312
- for (let index = open + 1; index < source.length; index += 1) {
2313
- if (source[index] === "{")
2314
- depth += 1;
2315
- if (source[index] === "}")
2316
- depth -= 1;
2317
- if (depth === 0) {
2318
- bodies.push(source.slice(open + 1, index));
2319
- break;
2320
- }
2318
+ const GRADLE_IDENTIFIER_CHAR = /[A-Za-z0-9_$]/;
2319
+ /**
2320
+ * The identifier immediately preceding a `{`, or `""`. Mirrors the
2321
+ * `\b<name>\s*\{` shape the named lookup used before it became a filter over
2322
+ * this scanner: whitespace (including newlines) between the name and the
2323
+ * brace is skipped, and `.` ends the identifier so `com.ncorti.ktfmt { }`
2324
+ * still reads as `ktfmt` while `something_ktfmt { }` does not.
2325
+ */
2326
+ function gradleBlockNameBefore(source, open) {
2327
+ let index = open - 1;
2328
+ while (index >= 0 && /\s/.test(source[index]))
2329
+ index -= 1;
2330
+ const end = index + 1;
2331
+ while (index >= 0 && GRADLE_IDENTIFIER_CHAR.test(source[index]))
2332
+ index -= 1;
2333
+ return source.slice(index + 1, end);
2334
+ }
2335
+ /**
2336
+ * Body ranges of EVERY brace pair in `source`, in source order, each labelled
2337
+ * with the identifier that introduced it.
2338
+ *
2339
+ * This is the repo's only Gradle brace scanner: `namedGradleBlockRanges` and
2340
+ * `namedGradleBlockBodies` are filters over it. Scanning every pair — rather
2341
+ * than only the pairs whose name a caller thought to ask about — is what lets
2342
+ * `clients/gradle-ktfmt-style.ts` (#2468 review round 3) answer "what block
2343
+ * ENCLOSES this one?" instead of only "is it inside the one name I checked?".
2344
+ * The distinction is load-bearing there: `configure(subprojects.filter { … })
2345
+ * { ktfmt { } }`, `project(":app") { ktfmt { } }` and a convention-plugin
2346
+ * wrapper all have to be recognized as *some* enclosing scope the resolver
2347
+ * cannot evaluate, so it can fail closed rather than treat the block as
2348
+ * top-level.
2349
+ *
2350
+ * Unbalanced braces are best-effort: an unmatched `}` is ignored and an
2351
+ * unclosed `{` yields no range, exactly as the previous per-name depth scan
2352
+ * behaved.
2353
+ */
2354
+ export function gradleBlockRanges(source) {
2355
+ const ranges = [];
2356
+ const opens = [];
2357
+ for (let index = 0; index < source.length; index += 1) {
2358
+ const char = source[index];
2359
+ if (char === "{") {
2360
+ opens.push(index);
2361
+ continue;
2321
2362
  }
2363
+ if (char !== "}")
2364
+ continue;
2365
+ const open = opens.pop();
2366
+ if (open === undefined)
2367
+ continue;
2368
+ ranges.push({
2369
+ name: gradleBlockNameBefore(source, open),
2370
+ start: open + 1,
2371
+ end: index,
2372
+ });
2322
2373
  }
2323
- return bodies;
2374
+ // Pairs are completed innermost-first; callers want source order.
2375
+ return ranges.sort((a, b) => a.start - b.start);
2376
+ }
2377
+ /**
2378
+ * Body ranges of every `<name> { ... }` block, in source order.
2379
+ *
2380
+ * The offsets exist so a caller can ask where a block sits RELATIVE to
2381
+ * another one — `clients/gradle-ktfmt-style.ts` (#2468) needs to know which
2382
+ * block encloses a `ktfmt { }` one, because Gradle's `subprojects { }`
2383
+ * applies it to the children and NOT to the project declaring it, while
2384
+ * `allprojects { }` applies to both. Containment of two ranges is the only
2385
+ * reliable way to answer that; matching body TEXT would collide whenever two
2386
+ * blocks happen to have identical bodies. `namedGradleBlockBodies` stays the
2387
+ * ergonomic form for callers that only need the text.
2388
+ */
2389
+ function namedGradleBlockRanges(source, name) {
2390
+ return gradleBlockRanges(source).filter((range) => range.name === name);
2391
+ }
2392
+ function namedGradleBlockBodies(source, name) {
2393
+ return namedGradleBlockRanges(source, name).map((range) => source.slice(range.start, range.end));
2324
2394
  }
2325
2395
  /**
2326
2396
  * Resolve the formatter elected by a Spotless `kotlin { ... }` block.
@@ -14,6 +14,7 @@
14
14
  import { logTreeSitterDiagnostic } from "./tree-sitter-logger.js";
15
15
  import * as crypto from "node:crypto";
16
16
  import * as fs from "node:fs";
17
+ import { BoundedFifoMap } from "./bounded-cache.js";
17
18
  import { normalizeFilePath } from "./path-utils.js";
18
19
  const TREE_RETIREMENT_GRACE_MICROTASKS = 4;
19
20
  export const TREE_CACHE_COUNTER_KEYS = [
@@ -82,17 +83,23 @@ export function deriveScanTreeCacheCapacity(fileCount, currentMaxSize) {
82
83
  return Math.max(currentMaxSize, target);
83
84
  }
84
85
  export class TreeCache {
85
- cache = new Map();
86
- recentlyEvicted = new Map();
87
- maxSize;
86
+ // BoundedFifoMap, not BoundedLruCache, even though eviction here IS true
87
+ // LRU: recency is refreshed by this class's own explicit `delete`+`set`
88
+ // touch on a VALIDATED hit (see `get`), and only there. A `get` that
89
+ // promoted on every read would also promote entries `get` goes on to
90
+ // reject (content-hash mismatch returns null without removing), silently
91
+ // changing which entry eviction targets (#2442 review F7).
92
+ cache = new BoundedFifoMap(TREE_CACHE_DEFAULT_MAX_SIZE);
93
+ recentlyEvicted;
88
94
  evictionHistoryMax;
89
95
  debug;
90
96
  counters = createTreeCacheCounters();
91
97
  counterObserver;
92
98
  treeErrorObserver;
93
99
  constructor(maxSize = TREE_CACHE_DEFAULT_MAX_SIZE, debug = false, evictionHistoryMax = 4096, counterObserver, treeErrorObserver) {
94
- this.maxSize = maxSize;
100
+ this.cache.setMaxEntries(Math.max(1, Math.floor(maxSize)));
95
101
  this.evictionHistoryMax = Math.max(1, Math.floor(evictionHistoryMax));
102
+ this.recentlyEvicted = new BoundedFifoMap(this.evictionHistoryMax);
96
103
  this.counterObserver = counterObserver;
97
104
  this.treeErrorObserver = treeErrorObserver;
98
105
  this.debug = debug
@@ -109,7 +116,7 @@ export class TreeCache {
109
116
  }
110
117
  /** Current capacity ceiling (entry count, not bytes — see class docstring). */
111
118
  getMaxSize() {
112
- return this.maxSize;
119
+ return this.cache.getMaxEntries();
113
120
  }
114
121
  /**
115
122
  * Resize the capacity ceiling (#1715). Growing only changes the bound —
@@ -120,17 +127,24 @@ export class TreeCache {
120
127
  * entries can never record a hit.
121
128
  */
122
129
  setMaxSize(n) {
123
- const next = Math.max(1, Math.floor(n));
124
- this.maxSize = next;
125
- while (this.cache.size > this.maxSize) {
126
- const firstKey = this.cache.keys().next().value;
127
- if (firstKey === undefined)
128
- break;
129
- const evicted = this.cache.get(firstKey);
130
- if (evicted)
131
- this.rememberEviction(firstKey, evicted);
130
+ this.retireEvicted(this.cache.setMaxEntries(Math.max(1, Math.floor(n))));
131
+ }
132
+ /**
133
+ * Book-keep entries the bounded map dropped: ghost history, the eviction
134
+ * counter, and the WASM-heap tree retirement (#417/#890) that made this
135
+ * cache's eviction side-effect-coupled in the first place. One
136
+ * implementation, shared by the two paths that can overflow
137
+ * ({@link setMaxSize} and {@link set}) — the shape `BoundedFifoMap.set`'s
138
+ * `[key, value]` return exists to enable (#2442 review F7).
139
+ */
140
+ retireEvicted(evicted) {
141
+ for (const [key, cached] of evicted) {
142
+ this.rememberEviction(key, cached);
132
143
  this.recordCounter("evictions");
133
- this.removeEntry(firstKey);
144
+ // The bounded map already removed the entry, so retire the tree
145
+ // directly rather than through removeEntry's re-lookup.
146
+ this.retireTree(cached.tree);
147
+ this.debug(`Evicted: ${key}`);
134
148
  }
135
149
  }
136
150
  /**
@@ -175,14 +189,9 @@ export class TreeCache {
175
189
  }
176
190
  rememberEviction(key, cached) {
177
191
  this.recentlyEvicted.delete(key);
178
- if (this.recentlyEvicted.size >= this.evictionHistoryMax) {
179
- const oldestKey = this.recentlyEvicted.keys().next().value;
180
- if (oldestKey !== undefined) {
181
- this.recentlyEvicted.delete(oldestKey);
182
- this.recordCounter("ghostHistoryDrops");
183
- }
184
- }
185
- this.recentlyEvicted.set(key, cached.contentHash);
192
+ const dropped = this.recentlyEvicted.set(key, cached.contentHash);
193
+ if (dropped.length > 0)
194
+ this.recordCounter("ghostHistoryDrops");
186
195
  }
187
196
  /**
188
197
  * Generate hash for file content
@@ -298,20 +307,11 @@ export class TreeCache {
298
307
  this.recordCounter("replacements");
299
308
  this.removeEntry(key);
300
309
  }
301
- else if (this.cache.size >= this.maxSize) {
302
- // Evict + free the least-recently-used entry when the cache is full:
303
- // get() re-inserts hits, so Map insertion order IS recency order and
304
- // the first key is the LRU entry (#890).
305
- const firstKey = this.cache.keys().next().value;
306
- if (firstKey) {
307
- const evicted = this.cache.get(firstKey);
308
- if (evicted)
309
- this.rememberEviction(firstKey, evicted);
310
- this.recordCounter("evictions");
311
- this.removeEntry(firstKey);
312
- this.debug(`Evicted: ${firstKey}`);
313
- }
314
- }
310
+ // Overflow eviction itself now happens on the `this.cache.set` below —
311
+ // the bounded map evicts the least-recently-used entry (this class's
312
+ // explicit delete+set touch on a validated hit makes insertion order
313
+ // recency order, #890) and hands back the dropped [key, tree] pair for
314
+ // `retireEvicted` to free.
315
315
  let mtime = 0;
316
316
  try {
317
317
  mtime = fs.statSync(filePath).mtimeMs;
@@ -320,14 +320,14 @@ export class TreeCache {
320
320
  // File deleted between parse and cache — cache with mtime=0;
321
321
  // next get() will miss on mtime check and re-parse
322
322
  }
323
- this.cache.set(key, {
323
+ this.retireEvicted(this.cache.set(key, {
324
324
  tree,
325
325
  contentHash,
326
326
  languageId,
327
327
  fileSize: Buffer.byteLength(content, "utf8"),
328
328
  lineCount: content.split("\n").length,
329
329
  lastModified: mtime,
330
- });
330
+ }));
331
331
  this.debug(`Cached: ${filePath} (${content.split("\n").length} lines)`);
332
332
  }
333
333
  /**
@@ -355,7 +355,7 @@ export class TreeCache {
355
355
  return {
356
356
  ...this.counters,
357
357
  size: this.cache.size,
358
- maxSize: this.maxSize,
358
+ maxSize: this.cache.getMaxEntries(),
359
359
  totalLines,
360
360
  totalBytes,
361
361
  misses: this.counters.lookups - this.counters.hits,