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
@@ -1,6 +1,37 @@
1
1
  import * as path from "node:path";
2
+ import { BoundedSet } from "./bounded-cache.js";
2
3
  import { createReadGuardEditBatchSummary, getReadGuardCorrelationId, logReadGuardEvent, } from "./read-guard-logger.js";
3
4
  import { normalizeMapKey } from "./path-utils.js";
5
+ import { getMutationBridge } from "./mutation-bridge.js";
6
+ import { noteMutationHandled } from "./observed-mutation.js";
7
+ import { recordDegradationOnce } from "./degradation-ledger.js";
8
+ // #2450 fix round 3 (minor): gates the "bridge unavailable" dbg line to fire
9
+ // once per session, not once per FILE — a rename that touches many files in
10
+ // a bridge-less process (e.g. the MCP server) would otherwise spam the same
11
+ // line once per touched file. `recordDegradationOnce` itself has no
12
+ // first-occurrence return value (unlike `incrementDegradationCount`), so this
13
+ // tiny local latch is the minimal way to mirror that "once" intent onto the
14
+ // dbg line without changing the ledger's own once-per-(kind,subject) record.
15
+ //
16
+ // Catalog shape 17 (a process-lifetime latch must re-arm at session_start):
17
+ // wired into `handleSessionStart` (`clients/runtime-session.ts`) directly,
18
+ // NOT into `resetDegradationLedger()` — that lives in `degradation-ledger.ts`,
19
+ // which this module already imports, and reaching back from there to here
20
+ // would be a cycle.
21
+ let noBridgeDbgLogged = false;
22
+ export function resetLspMutationNoBridgeDbgLatch() {
23
+ noBridgeDbgLogged = false;
24
+ }
25
+ /**
26
+ * Test-only peek at the latch's current value (#2450 fix round 5, F2) — lets
27
+ * the session-state registry probe the actual internal state instead of
28
+ * inferring it from captured dbg messages, the same pattern every other
29
+ * process-private-scalar probe in `tests/support/session-state-registry.ts`
30
+ * uses (e.g. `_observedMutationStateForTests`).
31
+ */
32
+ export function _lspMutationNoBridgeDbgLoggedForTests() {
33
+ return noBridgeDbgLogged;
34
+ }
4
35
  const MAX_SAMPLES = 100;
5
36
  const MAX_SUMMARIES_PER_CONTEXT = 100;
6
37
  export function newLspMutationCorrelationId(toolCallId) {
@@ -52,8 +83,22 @@ function combineResults(results) {
52
83
  }
53
84
  function uniqueDetails(files, fileDetails) {
54
85
  const byPath = new Map();
86
+ // #2016: `files` and `fileDetails` name the same paths, so without this the
87
+ // map-build loop and the lookup below each pay `realpathSync.native` for the
88
+ // same path (~200 microseconds per call on Windows; POSIX short-circuits, so
89
+ // CI cannot see it). The memo lives for one call, so it has no staleness
90
+ // window at all and needs no freshness design.
91
+ const keyMemo = new Map();
92
+ const keyFor = (filePath) => {
93
+ let key = keyMemo.get(filePath);
94
+ if (key === undefined) {
95
+ key = normalizeMapKey(path.resolve(filePath));
96
+ keyMemo.set(filePath, key);
97
+ }
98
+ return key;
99
+ };
55
100
  for (const detail of fileDetails) {
56
- const key = normalizeMapKey(path.resolve(detail.filePath));
101
+ const key = keyFor(detail.filePath);
57
102
  const previous = byPath.get(key);
58
103
  if (!previous) {
59
104
  byPath.set(key, detail);
@@ -70,7 +115,7 @@ function uniqueDetails(files, fileDetails) {
70
115
  importsChanged: previous.importsChanged || detail.importsChanged,
71
116
  });
72
117
  }
73
- return files.map((filePath) => byPath.get(normalizeMapKey(path.resolve(filePath))) ?? {
118
+ return files.map((filePath) => byPath.get(keyFor(filePath)) ?? {
74
119
  filePath,
75
120
  // Resource operations have no already-computed text range. A small
76
121
  // range is still enough to invalidate the touched-file turn state;
@@ -81,8 +126,35 @@ function uniqueDetails(files, fileDetails) {
81
126
  }
82
127
  function bookkeepLspMutation(context, files, fileDetails) {
83
128
  const details = uniqueDetails(files, fileDetails);
129
+ // The direct path below needs `context.runtime` (the receipt) and
130
+ // `context.cacheManager` (the turn-state entry) threaded in by the caller.
131
+ // One caller structurally cannot: `workspace/applyEdit`'s server-initiated-edit
132
+ // handler (clients/lsp/client.ts) builds its own fallback `LspMutationContext`
133
+ // with neither, because there is no live reference to the runtime/cache-manager
134
+ // singletons at that call site. Rather than silently drop bookkeeping for that
135
+ // write, fall back to the mutation bridge (#2423) — mounted once at extension
136
+ // activation with live getters closing over the SAME singletons — which is
137
+ // exactly the seam built for "a producer that cannot reach the bookkeeping
138
+ // surfaces directly". This keeps `recordLspMutation` the ONE call site every
139
+ // LSP-applied edit bookkeeps through; the bridge is this function's OWN
140
+ // fallback, not a second seam other code reaches for on its behalf (#2450).
141
+ // Only the receipt (recordProjectMutation) and the turn-state entry
142
+ // (cacheManager.addModifiedRange) need the bridge fallback: the read-guard
143
+ // stamp is independent of them and already tolerates either being absent.
144
+ const useBridgeFallback = !context.runtime || !context.cacheManager;
84
145
  for (const detail of details) {
85
146
  const filePath = path.resolve(detail.filePath);
147
+ // #2450 review round 2 (F4): the SAME recordability gate the bridge
148
+ // fallback below already applies internally (`isRecordable`, mounted in
149
+ // index.ts). Applied here too so the direct (deps-threaded) branch
150
+ // cannot record a write the fallback branch would have silently
151
+ // dropped for the exact same reason (`no-read-guard` / ignored /
152
+ // vendor) — the two branches must agree on which files count as
153
+ // project source, not just on how they bookkeep the ones that do.
154
+ if (context.isRecordable && !context.isRecordable(filePath)) {
155
+ context.dbg?.(`lsp mutation not recordable, skipping ${filePath}`);
156
+ continue;
157
+ }
86
158
  if (context.readGuard) {
87
159
  try {
88
160
  context.readGuard.recordWritten(filePath);
@@ -91,34 +163,118 @@ function bookkeepLspMutation(context, files, fileDetails) {
91
163
  context.dbg?.(`lsp mutation read-guard stamp failed for ${filePath}: ${err}`);
92
164
  }
93
165
  }
94
- const runtime = context.runtime;
95
- // One mutation seam (#2000 phase 1): bump + receipt + change-log live in
96
- // RuntimeCoordinator.recordProjectMutation.
97
- runtime?.recordProjectMutation?.({
98
- filePath,
99
- source: context.source,
100
- cwd: context.cwd,
101
- changedRange: detail.range,
102
- onAppendError: (err) => context.dbg?.(`lsp mutation project change append failed for ${filePath}: ${err}`),
103
- });
104
- if (context.cacheManager) {
105
- try {
106
- context.cacheManager.addModifiedRange(filePath, detail.range ?? { start: 1, end: 1 }, detail.importsChanged ?? true, context.cwd, runtime?.telemetrySessionId);
166
+ if (useBridgeFallback) {
167
+ const bridge = getMutationBridge();
168
+ if (!bridge) {
169
+ // #2450 review round 2 (F4)/round 3 (minor): the MCP server process
170
+ // (`mcp/server.ts`) builds `lsp_navigation` with no
171
+ // `mutationDeps` AND never mounts the bridge (that only happens
172
+ // inside pi's own in-process extension activation, `index.ts`) —
173
+ // every LSP-applied edit in that process is unrecorded. Once per
174
+ // session (not once per file — a rename can touch many files in
175
+ // one call, and a session can issue many renames), not once per
176
+ // process lifetime silently. `subject` is a fixed, one-word
177
+ // degradation-ledger key (round 3: was `context.tool`, which
178
+ // varies per LSP operation — "lsp_navigation:rename" vs
179
+ // "...executeCommand" — so the SAME session could log this
180
+ // degradation once per operation kind instead of truly once).
181
+ recordDegradationOnce({
182
+ kind: "lsp-mutation-bridge-unmounted",
183
+ subject: "lsp-mutation:no-bridge",
184
+ reason: "workspace/applyEdit bookkeeping fell back to the mutation " +
185
+ "bridge, but no bridge is mounted in this process (e.g. the " +
186
+ "MCP server, which never runs pi's extension activation) — " +
187
+ "the write is unrecorded (no read-guard stamp, no turn-state " +
188
+ "entry, no change-log receipt).",
189
+ });
190
+ if (!noBridgeDbgLogged) {
191
+ noBridgeDbgLogged = true;
192
+ context.dbg?.(`lsp mutation bridge unavailable, dropping bookkeeping for ${filePath}`);
193
+ }
107
194
  }
108
- catch (err) {
109
- context.dbg?.(`lsp mutation modified-range tracking failed for ${filePath}: ${err}`);
195
+ else {
196
+ try {
197
+ const recorded = bridge.recordMutation({
198
+ filePath,
199
+ kind: "edit",
200
+ editRanges: detail.range
201
+ ? [[detail.range.start, detail.range.end]]
202
+ : undefined,
203
+ consumer: context.tool,
204
+ // Real value threaded through, not the bridge's own
205
+ // historical `false` default (#2450 review round 2, F1) —
206
+ // the tsserver organize-imports/add-import case is exactly
207
+ // the import-changing one.
208
+ importsChanged: detail.importsChanged ?? true,
209
+ // Equivalence with the direct branch below, which never
210
+ // enqueues a deferred autofix/format pass for an LSP-applied
211
+ // edit (#2450 review round 2, F3).
212
+ deferAutofix: false,
213
+ });
214
+ if (!recorded) {
215
+ context.dbg?.(`lsp mutation bridge dropped ${filePath} (recordMutation() ` +
216
+ "returned false — see the mutation_bridge debug output " +
217
+ "for the reason: malformed entry, out-of-scope path, or " +
218
+ "a bookkeeping error)");
219
+ }
220
+ }
221
+ catch (err) {
222
+ context.dbg?.(`lsp mutation bridge fallback failed for ${filePath}: ${err}`);
223
+ }
110
224
  }
111
225
  }
226
+ else {
227
+ const runtime = context.runtime;
228
+ // One mutation seam (#2000 phase 1): bump + receipt + change-log live in
229
+ // RuntimeCoordinator.recordProjectMutation.
230
+ runtime?.recordProjectMutation?.({
231
+ filePath,
232
+ source: context.source,
233
+ cwd: context.cwd,
234
+ changedRange: detail.range,
235
+ onAppendError: (err) => context.dbg?.(`lsp mutation project change append failed for ${filePath}: ${err}`),
236
+ });
237
+ if (context.cacheManager) {
238
+ try {
239
+ context.cacheManager.addModifiedRange(filePath, detail.range ?? { start: 1, end: 1 }, detail.importsChanged ?? true, context.cwd, runtime?.telemetrySessionId, undefined,
240
+ // #2504 review round 2 (F1). #2504 added a containment
241
+ // filter to `addModifiedRange` and gave it `cwd` as the
242
+ // project root. On THIS path `cwd` is the calling
243
+ // directory, which `tools/lsp-navigation.ts` scopes to a
244
+ // sub-package while the LSP client's root spans the whole
245
+ // monorepo — so a server-initiated edit on a sibling
246
+ // package (#2450) and every edit whose context carries a
247
+ // separate bookkeeping cwd (#2479) were silently dropped
248
+ // from the worklist. Same root the `isRecordable` gate a
249
+ // few frames up already uses: `runtime.projectRoot`,
250
+ // falling back to `cwd` when no runtime is threaded.
251
+ runtime?.projectRoot ?? context.cwd);
252
+ }
253
+ catch (err) {
254
+ context.dbg?.(`lsp mutation modified-range tracking failed for ${filePath}: ${err}`);
255
+ }
256
+ }
257
+ // #2465: the bridge-fallback branch above gets this for free —
258
+ // `recordMutationThroughSeam` calls `noteMutationHandled` for every
259
+ // entry it takes. The DIRECT branch bypasses the bridge entirely, so
260
+ // it has to say so itself, or the #2430 `agent_settled` sweep finds
261
+ // bytes on disk it has no baseline for and reports every
262
+ // `lsp_navigation` rename as drift no tool call explains. Placed
263
+ // after `addModifiedRange` and OUTSIDE its `try`: the mark is about
264
+ // "pi-lens wrote this file", which is true whether or not the
265
+ // turn-state insert threw.
266
+ noteMutationHandled(filePath);
267
+ }
268
+ // #2450 review round 2 (Partial-deps): its own surface, independent of
269
+ // whether the receipt/turn-state used the direct path or the bridge
270
+ // fallback above — a half-threaded caller (one that provides
271
+ // `recordAutofix` but not `runtime`/`cacheManager`) must not lose its
272
+ // turn-summary publisher just because SOME other surface fell back.
112
273
  if (context.recordAutofix && context.source === "autofix") {
113
274
  const key = normalizeMapKey(filePath);
114
- const seen = context.autofixRecordedPaths ?? new Set();
275
+ const seen = context.autofixRecordedPaths ?? new BoundedSet(MAX_SAMPLES);
115
276
  context.autofixRecordedPaths = seen;
116
277
  if (!seen.has(key)) {
117
- if (seen.size >= MAX_SAMPLES) {
118
- const oldest = seen.values().next().value;
119
- if (oldest)
120
- seen.delete(oldest);
121
- }
122
278
  seen.add(key);
123
279
  context.recordAutofix(filePath);
124
280
  }
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Bounded worker pool — the repo's one shared `mapWithConcurrency`.
3
+ *
4
+ * Lived in `dependency-checker.ts` (its first caller) and was exported from
5
+ * there for `dispatch/runners/biome-check.ts`. #2504 added a third caller,
6
+ * `runtime-turn.ts`'s bounded test-runner batch — and runtime-turn must not
7
+ * drag an ANALYZER client into its eager import graph for a 15-line helper
8
+ * (that is exactly the eager-bootstrap cost #2467 removed). So the value moved
9
+ * to this zero-dependency leaf and `dependency-checker.ts` re-exports it: every
10
+ * existing importer keeps its specifier, and a consumer that only needs the
11
+ * pool no longer loads madge.
12
+ *
13
+ * Same shape as `ledger-bounds.ts` (#2426) for the degradation ledger's bound.
14
+ *
15
+ * NOT consolidated here: `lsp/client.ts` keeps its own file-local copy on
16
+ * purpose (see its comment) — that is a documented decision, not drift.
17
+ */
18
+ /**
19
+ * Run `mapper` over `items` with at most `concurrency` in flight at once.
20
+ */
21
+ export async function mapWithConcurrency(items, concurrency, mapper) {
22
+ if (items.length === 0)
23
+ return;
24
+ let nextIndex = 0;
25
+ const workerCount = Math.max(1, Math.min(concurrency, items.length));
26
+ const worker = async () => {
27
+ while (true) {
28
+ const index = nextIndex++;
29
+ if (index >= items.length)
30
+ return;
31
+ await mapper(items[index]);
32
+ }
33
+ };
34
+ const workers = Array.from({ length: workerCount }, () => worker());
35
+ await Promise.all(workers);
36
+ }
@@ -23,8 +23,8 @@ import { PathKeyedMap } from "../path-keyed-map.js";
23
23
  import { normalizeMapKey } from "../path-utils.js";
24
24
  import { loadProjectSnapshot } from "../project-snapshot.js";
25
25
  import { buildOrUpdateGraph } from "../review-graph/service.js";
26
- import { recordDiagnostics } from "../widget-state.js";
27
- import { deserializeWordIndex, removeWordIndexDocument, scheduleWordIndexPersist, updateWordIndexDocument, WORD_INDEX_MAX_BYTES, } from "../word-index.js";
26
+ import { classifyDiagnosticTier, recordDiagnostics } from "../widget-state.js";
27
+ import { deserializeWordIndex, removeWordIndexDocumentAsync, scheduleWordIndexPersist, updateWordIndexDocumentForEdit, WORD_INDEX_MAX_BYTES, } from "../word-index.js";
28
28
  import { logWordIndex } from "../word-index-logger.js";
29
29
  import { createMcpHost } from "./host-shim.js";
30
30
  // #536: module-scoped FactStore for the warm-analyze graph seam, mirroring the
@@ -33,7 +33,9 @@ import { createMcpHost } from "./host-shim.js";
33
33
  // tiers key off a stable FactStore instance across calls, so a fresh FactStore
34
34
  // per call would defeat that reuse. Scoped separately from integration.ts's
35
35
  // singleton since this file has no dependency on that module's internal state.
36
- const warmGraphFacts = new FactStore();
36
+ // Subject labels this store's capacity-eviction telemetry distinctly from
37
+ // the other five production FactStore instances (#2243 review round 3, F1).
38
+ const warmGraphFacts = new FactStore("mcp-analyze");
37
39
  // #536 rider (issue body: "when #348 phase 2 lands, the word-index per-edit
38
40
  // update should ride the SAME seam so both indexes stay warm together"):
39
41
  // MCP has no RuntimeCoordinator/`runtime.wordIndex` to hold a live index the
@@ -168,6 +170,28 @@ export function _getWarmWordIndexCacheStateForTests() {
168
170
  // is the persistent warm server's job (see the `lsp` honesty signal + Tier 2).
169
171
  const WARMUP_CLIENT_WAIT_MS = 10_000;
170
172
  const WARMUP_DIAGNOSTICS_WAIT_MS = 6_000;
173
+ /** One diagnostic, flattened to the fields an MCP consumer needs. */
174
+ /**
175
+ * Split the dispatch `warnings` bucket into real warnings vs advisories using
176
+ * the ONE severity-projection policy ({@link classifyDiagnosticTier}, #2414/
177
+ * #2417) so the model-facing counts never present a `hint`/`info`-tier finding
178
+ * as a warning (#2420, remainder of #2414). The dispatch `semantic` axis (which
179
+ * also drives blocking) is intentionally NOT consulted or mutated here: this is
180
+ * a display-count split, not a blocking-decision change. `blockers`
181
+ * (`semantic:"blocking"`) are computed upstream and never enter this bucket, so
182
+ * nothing that blocks can stop blocking through this seam. A non-advisory entry
183
+ * in the bucket (an `error`/`warning`-tier finding routed here as
184
+ * `semantic:"warning"`/`"none"`) stays counted as a warning exactly as before,
185
+ * so a mixed file's real warning count is unchanged.
186
+ */
187
+ export function summarizeWarningTiers(warnings) {
188
+ let advisories = 0;
189
+ for (const w of warnings) {
190
+ if (classifyDiagnosticTier(w) === "advisory")
191
+ advisories++;
192
+ }
193
+ return { warnings: warnings.length - advisories, advisories };
194
+ }
171
195
  function toMcpDiagnostic(diagnostic) {
172
196
  return {
173
197
  line: diagnostic.line,
@@ -233,7 +257,7 @@ export async function analyzeFile(filePath, cwd, options = {}) {
233
257
  : path.resolve(cwd, filePath);
234
258
  // no-delta by default → a full snapshot every call (not delta-filtered);
235
259
  // caller flags win over the default.
236
- const host = createMcpHost({ "no-delta": true, ...(options.flags ?? {}) }, cwd);
260
+ const host = createMcpHost({ "no-delta": true, ...options.flags }, cwd);
237
261
  if (options.warmLsp !== false) {
238
262
  await warmLspForFile(absPath, host);
239
263
  }
@@ -295,8 +319,9 @@ export async function analyzeFile(filePath, cwd, options = {}) {
295
319
  // index with no `forward` map (or none loaded) ⇒ no-op (no incremental
296
320
  // primitive available — the eventual full rebuild covers it); an
297
321
  // oversized file is REMOVED, never partially indexed; the update is
298
- // synchronous (no interleaving hazard — MCP is single-process, same as
299
- // pi); a successful update schedules the SAME debounced persist
322
+ // cooperative and serialized through the index's own operation queue
323
+ // (#2067 AC4), so this seam no longer holds the event loop for a whole
324
+ // replacement either; a successful update schedules the SAME debounced persist
300
325
  // (`scheduleWordIndexPersist`, `PI_LENS_WORD_INDEX_PERSIST_DEBOUNCE_MS`)
301
326
  // pi's path uses — no second persist mechanism.
302
327
  //
@@ -313,10 +338,13 @@ export async function analyzeFile(filePath, cwd, options = {}) {
313
338
  const content = fs.readFileSync(absPath, "utf8");
314
339
  const byteLength = Buffer.byteLength(content, "utf-8");
315
340
  if (byteLength > WORD_INDEX_MAX_BYTES) {
316
- removeWordIndexDocument(warmIndex, absPath);
341
+ await removeWordIndexDocumentAsync(warmIndex, absPath);
317
342
  }
318
343
  else {
319
- updateWordIndexDocument(warmIndex, { path: absPath, content });
344
+ await updateWordIndexDocumentForEdit(warmIndex, {
345
+ path: absPath,
346
+ content,
347
+ });
320
348
  }
321
349
  scheduleWordIndexPersist(cwd, warmIndex);
322
350
  }
@@ -339,7 +367,9 @@ export async function analyzeFile(filePath, cwd, options = {}) {
339
367
  const lspRunner = latencyReport?.runners.find((runner) => runner.runnerId === "lsp");
340
368
  const lsp = lspRunner
341
369
  ? {
342
- ran: lspRunner.status !== "skipped" && lspRunner.status !== "when_skipped",
370
+ ran: lspRunner.status !== "skipped" &&
371
+ lspRunner.status !== "when_skipped" &&
372
+ lspRunner.status !== "test_file_skipped",
343
373
  status: lspRunner.status,
344
374
  diagnosticCount: lspRunner.diagnosticCount,
345
375
  durationMs: lspRunner.durationMs,
@@ -354,7 +384,12 @@ export async function analyzeFile(filePath, cwd, options = {}) {
354
384
  counts: {
355
385
  diagnostics: result.diagnostics.length,
356
386
  blockers: result.blockers.length,
357
- warnings: result.warnings.length,
387
+ // #2420: `result.warnings` is the dispatch warnings bucket
388
+ // (`semantic:"warning"|"none"`), which still carries `hint`/`info`-tier
389
+ // findings folded in by the runner's severity→semantic map. Project
390
+ // them out through the shared tier policy so the model sees real
391
+ // warnings and advisories separately.
392
+ ...summarizeWarningTiers(result.warnings),
358
393
  fixed: result.fixed.length,
359
394
  },
360
395
  lsp,
@@ -20,7 +20,7 @@ import * as crypto from "node:crypto";
20
20
  import * as fs from "node:fs";
21
21
  import * as path from "node:path";
22
22
  import { AstGrepClient } from "../ast-grep-client.js";
23
- import { loadBootstrapClients } from "../bootstrap.js";
23
+ import { loadBootstrapClients, residentBootstrapAccess, } from "../bootstrap.js";
24
24
  import { CacheManager, MCP_TURN_STATE_OWNER_ID, } from "../cache-manager.js";
25
25
  import { resetDispatchBaselines } from "../dispatch/integration.js";
26
26
  import { resetFormatService } from "../format-service.js";
@@ -96,23 +96,12 @@ export async function runSessionStart(cwd) {
96
96
  dbg: noop,
97
97
  log: noop,
98
98
  runtime: ctx.runtime,
99
- metricsClient: ctx.clients.metricsClient,
100
99
  cacheManager: ctx.cacheManager,
101
- todoScanner: ctx.clients.todoScanner,
102
100
  astGrepClient: ctx.astGrepClient,
103
- biomeClient: ctx.clients.biomeClient,
104
- ruffClient: ctx.clients.ruffClient,
105
- knipClient: ctx.clients.knipClient,
106
- jscpdClient: ctx.clients.jscpdClient,
107
- deadCodeClients: ctx.clients.deadCodeClients,
108
- govulncheckClient: ctx.clients.govulncheckClient,
109
- gitleaksClient: ctx.clients.gitleaksClient,
110
- trivyClient: ctx.clients.trivyClient,
111
- opengrepClient: ctx.clients.opengrepClient,
112
- depChecker: ctx.clients.depChecker,
113
- testRunnerClient: ctx.clients.testRunnerClient,
114
- goClient: ctx.clients.goClient,
115
- rustClient: ctx.clients.rustClient,
101
+ // The MCP server has already loaded the analyzer graph (an MCP call has
102
+ // nothing to defer to), so it hands the handler the same one-shape seam
103
+ // every other caller uses rather than a second, field-by-field shape.
104
+ bootstrap: residentBootstrapAccess(ctx.clients),
116
105
  ensureTool: async (name) => (await import("../installer/index.js")).ensureTool(name),
117
106
  cleanStaleTsBuildInfo: () => [],
118
107
  resetDispatchBaselines: (cwdArg) => resetDispatchBaselines(cwdArg ?? cwd),
@@ -309,8 +298,13 @@ export function runTurnEndForIpc(cwd) {
309
298
  const inFlight = inFlightIpcTurnEnds.get(cwd);
310
299
  if (inFlight)
311
300
  return inFlight;
301
+ // Identity-guarded release (#1968's pattern): delete only if THIS run is
302
+ // still the registered one. A bare delete-by-key lets a late-settling run
303
+ // evict a live successor a second writer registered under the same cwd
304
+ // mid-flight, after which the next caller starts a duplicate Stop-hook pass.
312
305
  const run = runTurnEndForIpcNow(cwd).finally(() => {
313
- inFlightIpcTurnEnds.delete(cwd);
306
+ if (inFlightIpcTurnEnds.get(cwd) === run)
307
+ inFlightIpcTurnEnds.delete(cwd);
314
308
  });
315
309
  inFlightIpcTurnEnds.set(cwd, run);
316
310
  return run;
@@ -374,3 +368,11 @@ export function _resetTurnEndChain() {
374
368
  pendingTurnEndDeliveries.clear();
375
369
  inFlightIpcTurnEnds.clear();
376
370
  }
371
+ /**
372
+ * Test hook — read the `runTurnEndForIpc` in-flight map directly (#1968's
373
+ * ABA regression: a second writer replacing an entry mid-flight, then a
374
+ * late-settling first run evicting it with a bare delete-by-key).
375
+ */
376
+ export function _peekInFlightIpcTurnEnds() {
377
+ return inFlightIpcTurnEnds;
378
+ }
@@ -29,9 +29,11 @@
29
29
  * `arrayBuffers` is used as the process-wide proxy instead — WASM linear
30
30
  * memory backs an ArrayBuffer, so it is already included there.
31
31
  */
32
+ import { countPostingEntries, estimateWordIndexStoreBytes, } from "./word-index-store.js";
32
33
  import { getSharedTreeSitterClient } from "./tree-sitter-shared.js";
33
34
  import { getReviewGraphWorkspaceCacheSnapshot } from "./review-graph/builder.js";
34
35
  import { getDispatchCascadeCacheStats } from "./dispatch/integration.js";
36
+ import { getLspDocumentTextRetentionSnapshot } from "./lsp/client.js";
35
37
  /** Every N turns, emit one `memory_sample` latency.log line (#1123 item 2). */
36
38
  export const MEMORY_SAMPLE_TURN_INTERVAL = 10;
37
39
  /** True on turn 10, 20, 30, ... — never on turn 0 (nothing meaningful is
@@ -104,6 +106,13 @@ export function toMemoryProcessUsage(mem, resourceUsage) {
104
106
  * process-global singletons) but every individual read is a `.size`/`.length`
105
107
  * access — see the module docstring's hard constraint.
106
108
  */
109
+ /** Byte attribution for `dispatchCaches`, from measured per-entry costs: three
110
+ * isolated-store runs put a recently-clean neighbor entry at 320 bytes, and
111
+ * #2282 measured a 500-file batch's 1,000 session-fact entries at 439,560
112
+ * bytes (~440 each). O(1) — the cache key and Map bookkeeping are included. */
113
+ export function estimateDispatchCacheBytes(stats) {
114
+ return (stats.recentlyCleanNeighborCacheSize * 320 + stats.sessionFactEntries * 440);
115
+ }
107
116
  export function collectMemorySampleSubsystems(wordIndex) {
108
117
  const reviewGraph = getReviewGraphWorkspaceCacheSnapshot();
109
118
  const treeSitterClient = getSharedTreeSitterClient();
@@ -121,16 +130,34 @@ export function collectMemorySampleSubsystems(wordIndex) {
121
130
  : null;
122
131
  const dispatchCaches = getDispatchCascadeCacheStats();
123
132
  return {
133
+ lsp: (() => {
134
+ const retention = getLspDocumentTextRetentionSnapshot();
135
+ return {
136
+ clients: retention.clients,
137
+ clientRoots: retention.roots,
138
+ incrementalTextEntries: retention.entries,
139
+ incrementalTextBytes: retention.bytes,
140
+ };
141
+ })(),
124
142
  reviewGraph,
125
143
  wordIndex: wordIndex
126
144
  ? {
127
145
  docs: wordIndex.docLengths.size,
146
+ fileTable: wordIndex.fileTable.size,
128
147
  postings: wordIndex.postings.size,
148
+ // O(distinct tokens), reading only `.length`/`.byteLength` per
149
+ // list — no posting ELEMENT is touched, so this stays inside the
150
+ // module docstring’s bounded-read constraint.
151
+ postingEntries: countPostingEntries(wordIndex),
152
+ residentBytes: estimateWordIndexStoreBytes(wordIndex),
129
153
  forwardEntries: wordIndex.forward?.size ?? 0,
130
154
  }
131
155
  : null,
132
156
  treeSitter,
133
- dispatchCaches,
157
+ dispatchCaches: {
158
+ ...dispatchCaches,
159
+ estimatedBytes: estimateDispatchCacheBytes(dispatchCaches),
160
+ },
134
161
  };
135
162
  }
136
163
  /** Assemble one full sample. `mem` is injectable for tests; defaults to a
@@ -0,0 +1,30 @@
1
+ /** Session-local attribution captured before a stale message_end can drain. */
2
+ let lastStableSessionId;
3
+ let previousSessionId;
4
+ /** Remember the stable id used by a live message_end's own cache row. */
5
+ export function noteLiveMessageEndSessionId(sessionId) {
6
+ lastStableSessionId = sessionId;
7
+ }
8
+ /** Return the newest usable id for a stale message_end attribution. */
9
+ export function getLastLiveMessageEndSessionId() {
10
+ return lastStableSessionId ?? previousSessionId;
11
+ }
12
+ /**
13
+ * Rotate the attribution anchor at the real session boundary. A queued stale
14
+ * message_end from the session being replaced may drain after this boundary,
15
+ * so retain exactly one prior anchor while making room for the replacement.
16
+ */
17
+ export function rotateMessageEndAttribution() {
18
+ // Keep the prior anchor when no live message_end arrived this session:
19
+ // two back-to-back boundaries (reload/resume re-announcing) must not
20
+ // erase the only id a still-queued stale drain can use (#1956 R10).
21
+ if (lastStableSessionId !== undefined) {
22
+ previousSessionId = lastStableSessionId;
23
+ }
24
+ lastStableSessionId = undefined;
25
+ }
26
+ /** Full reset seam used by the session-state registry and test isolation. */
27
+ export function resetMessageEndAttribution() {
28
+ lastStableSessionId = undefined;
29
+ previousSessionId = undefined;
30
+ }
@@ -34,14 +34,31 @@ function isInsideGitRepo(startDir) {
34
34
  /**
35
35
  * Get current git commit hash (short)
36
36
  */
37
- function getCurrentCommit() {
38
- if (!isInsideGitRepo(process.cwd())) {
37
+ function getCurrentCommit(startDir) {
38
+ const repoStartDir = path.resolve(startDir);
39
+ if (!isInsideGitRepo(repoStartDir)) {
39
40
  return "unknown";
40
41
  }
42
+ let spawnDir = repoStartDir;
43
+ while (!fs.existsSync(spawnDir)) {
44
+ const parent = path.dirname(spawnDir);
45
+ if (parent === spawnDir)
46
+ return "unknown";
47
+ spawnDir = parent;
48
+ }
41
49
  try {
50
+ // #2095: execSync inherits the child's stderr to THIS process by
51
+ // default (only stdout is captured for the return value), so a
52
+ // failing `git rev-parse` prints its raw "fatal: ..." line straight
53
+ // into the pi TUI — the catch below only ever sees the thrown
54
+ // non-zero-exit error, never the already-inherited stream. The
55
+ // explicit stdio array pipes stderr instead, so a failure is fully
56
+ // contained: silent here, same as the "unknown" fallback it feeds.
42
57
  return execSync("git rev-parse --short HEAD", {
43
58
  encoding: "utf-8",
44
59
  timeout: 5000,
60
+ stdio: ["ignore", "pipe", "ignore"],
61
+ cwd: spawnDir,
45
62
  }).trim();
46
63
  }
47
64
  catch {
@@ -99,7 +116,7 @@ export function captureSnapshot(filePath, metrics) {
99
116
  pendingHistory = loadHistory();
100
117
  }
101
118
  const relativePath = path.relative(process.cwd(), filePath);
102
- const commit = getCurrentCommit();
119
+ const commit = getCurrentCommit(path.dirname(filePath));
103
120
  const snapshot = {
104
121
  commit,
105
122
  timestamp: new Date().toISOString(),
@@ -152,7 +169,7 @@ export function captureSnapshots(files) {
152
169
  const history = loadHistory();
153
170
  for (const file of files) {
154
171
  const relativePath = path.relative(process.cwd(), file.filePath);
155
- const commit = getCurrentCommit();
172
+ const commit = getCurrentCommit(path.dirname(file.filePath));
156
173
  const snapshot = {
157
174
  commit,
158
175
  timestamp: new Date().toISOString(),
@@ -27,6 +27,7 @@
27
27
  * slice can add them once middle-man analysis has an AST-node entry point
28
28
  * rather than this text-based one.
29
29
  */
30
+ import { escapeRegExp } from "./string-utils.js";
30
31
  /** Self-reference token + member-access separator, per languageId. Both the
31
32
  * self→field and field→method hops use the SAME separator in every language
32
33
  * covered here (true even for PHP's `$this->field->method()`). */
@@ -65,9 +66,6 @@ const MIN_CANDIDATE_METHODS = 2;
65
66
  * "mostly" — a class with a couple of forwarding convenience methods among
66
67
  * real logic must NOT flag (the exact distinction #325 asks for). */
67
68
  const DELEGATION_RATIO_THRESHOLD = 0.9;
68
- function escapeRegExp(s) {
69
- return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
70
- }
71
69
  function buildForwardRegex(token, sep) {
72
70
  const t = escapeRegExp(token);
73
71
  const s = escapeRegExp(sep);
@@ -98,7 +96,7 @@ function paramNamesFromSignature(signature, languageId) {
98
96
  .map((raw) => {
99
97
  let name = raw;
100
98
  // Rest/spread params forward as themselves textually (`...args`).
101
- const isRest = /^\.\.\./.test(name);
99
+ const isRest = name.startsWith("...");
102
100
  name = name.replace(/^\.\.\./, "");
103
101
  name = name.split("=")[0].trim(); // strip default value
104
102
  name = name.split(":")[0].trim(); // strip type annotation