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,30 +1,73 @@
1
- /** Small insertion-ordered LRU used for process-lifetime memo tables. */
2
- export class BoundedLruCache {
3
- maxEntries;
1
+ /**
2
+ * Small insertion-ordered FIFO map used for process-lifetime memo tables
3
+ * whose eviction order must not be perturbed by reads: `get` never
4
+ * re-inserts, and `set` never reorders an already-present key — it behaves
5
+ * exactly like native `Map#set` on an existing key. A caller that wants a
6
+ * WRITE (not a read) to refresh recency does its own `delete` + `set` around
7
+ * this map, exactly as it would against a raw `Map` (#2442).
8
+ *
9
+ * {@link BoundedLruCache} extends this class and overrides exactly the two
10
+ * methods that differ (`get` re-inserts; `set` deletes first so a write moves
11
+ * the key to most-recently-used). Eviction, capacity and the whole `Map`-ish
12
+ * surface live here once — the two classes shipped as near-identical copies
13
+ * until #2442's review round flagged the duplication.
14
+ */
15
+ export class BoundedFifoMap {
4
16
  entries = new Map();
17
+ maxEntries;
5
18
  constructor(maxEntries) {
6
19
  this.maxEntries = maxEntries;
7
20
  }
8
21
  get(key) {
9
- const value = this.entries.get(key);
10
- if (value !== undefined) {
11
- this.entries.delete(key);
12
- this.entries.set(key, value);
13
- }
14
- return value;
22
+ return this.entries.get(key);
15
23
  }
16
24
  has(key) {
17
25
  return this.entries.has(key);
18
26
  }
27
+ /**
28
+ * Insert or update `key` without reordering. Evicts the oldest
29
+ * (insertion-order) entries once the map exceeds capacity.
30
+ *
31
+ * Returns the evicted `[key, value]` pairs, oldest first — callers with
32
+ * eviction-side bookkeeping use the return value instead of hand-rolling
33
+ * `keys().next().value` (#2442). The VALUE is returned, not just the key,
34
+ * because the eviction-coupled sites need it: `tree-sitter-cache.ts` has to
35
+ * retire the dropped entry's WASM-heap tree (#417/#890) and
36
+ * `tree-sitter-client.ts` has to `delete()` the dropped compiled query.
37
+ * Handing back only the key would force those sites to read the value out
38
+ * of the map before eviction — i.e. to hand-roll the block this class
39
+ * exists to delete.
40
+ */
19
41
  set(key, value) {
20
- this.entries.delete(key);
21
42
  this.entries.set(key, value);
43
+ return this.evictOverflow();
44
+ }
45
+ /**
46
+ * Change the capacity ceiling, evicting down to it immediately when it
47
+ * shrinks. Returns the evicted `[key, value]` pairs, oldest first, on the
48
+ * same contract as {@link set} — a caller with a DYNAMIC cap (an env-read
49
+ * ceiling, `TreeCache.setMaxSize`) needs the shrink to free the same
50
+ * resources an ordinary overflow frees.
51
+ */
52
+ setMaxEntries(maxEntries) {
53
+ this.maxEntries = maxEntries;
54
+ return this.evictOverflow();
55
+ }
56
+ /** Current capacity ceiling. */
57
+ getMaxEntries() {
58
+ return this.maxEntries;
59
+ }
60
+ /** Drop oldest-first until within capacity. The one eviction block. */
61
+ evictOverflow() {
62
+ const evicted = [];
22
63
  while (this.entries.size > this.maxEntries) {
23
- const oldest = this.entries.keys().next().value;
64
+ const oldest = this.entries.entries().next().value;
24
65
  if (oldest === undefined)
25
66
  break;
26
- this.entries.delete(oldest);
67
+ this.entries.delete(oldest[0]);
68
+ evicted.push(oldest);
27
69
  }
70
+ return evicted;
28
71
  }
29
72
  delete(key) {
30
73
  return this.entries.delete(key);
@@ -48,3 +91,100 @@ export class BoundedLruCache {
48
91
  return this.entries[Symbol.iterator]();
49
92
  }
50
93
  }
94
+ /**
95
+ * Small insertion-ordered FIFO SET used for process-lifetime membership-only
96
+ * caps — "is this key known", with no associated value.
97
+ *
98
+ * A thin wrapper over `BoundedFifoMap<T, true>` (#2460 review T1): eviction,
99
+ * capacity and the whole Map-ish surface already live in `BoundedFifoMap`
100
+ * exactly once, and this class's earlier standalone `Set`-backed
101
+ * implementation was a byte-for-byte copy of that eviction block one layer
102
+ * down — the exact duplication `BoundedFifoMap`'s own header describes
103
+ * `BoundedLruCache` existing to delete (#2442). The `true` dummy value never
104
+ * escapes this wrapper: it is not a cost paid by call sites, only by this
105
+ * file, so the earlier objection to building membership on top of the K,V
106
+ * map (#2460's first review, about `clients/observed-mutation.ts`'s
107
+ * `handled` set) does not apply here — that objection was about a caller
108
+ * storing a dummy value itself, not about a wrapper hiding one.
109
+ *
110
+ * Same eviction contract as {@link BoundedFifoMap}: `add` never reorders an
111
+ * already-present member (matches native `Set#add`), and both `add` and
112
+ * {@link setMaxEntries} return the evicted values, oldest first, so a call
113
+ * site with an eviction side effect (naming the dropped identity in a bounded
114
+ * telemetry record) consumes the return value instead of hand-rolling
115
+ * `values().next().value` or a `for (... of set) { …; break }` walk (#2442).
116
+ */
117
+ export class BoundedSet {
118
+ entries;
119
+ constructor(maxEntries) {
120
+ this.entries = new BoundedFifoMap(maxEntries);
121
+ }
122
+ has(value) {
123
+ return this.entries.has(value);
124
+ }
125
+ /**
126
+ * Insert `value` without reordering an already-present member. Evicts the
127
+ * oldest (insertion-order) members once the set exceeds capacity.
128
+ *
129
+ * Returns the evicted values, oldest first — see the class doc for why a
130
+ * call site consumes this instead of hand-rolling the drop.
131
+ */
132
+ add(value) {
133
+ return this.entries.set(value, true).map(([evicted]) => evicted);
134
+ }
135
+ /**
136
+ * Change the capacity ceiling, evicting down to it immediately when it
137
+ * shrinks. Returns the evicted values, oldest first, on the same contract
138
+ * as {@link add}.
139
+ */
140
+ setMaxEntries(maxEntries) {
141
+ return this.entries.setMaxEntries(maxEntries).map(([evicted]) => evicted);
142
+ }
143
+ /** Current capacity ceiling. */
144
+ getMaxEntries() {
145
+ return this.entries.getMaxEntries();
146
+ }
147
+ delete(value) {
148
+ return this.entries.delete(value);
149
+ }
150
+ clear() {
151
+ this.entries.clear();
152
+ }
153
+ get size() {
154
+ return this.entries.size;
155
+ }
156
+ values() {
157
+ return this.entries.keys();
158
+ }
159
+ [Symbol.iterator]() {
160
+ return this.entries.keys();
161
+ }
162
+ }
163
+ /**
164
+ * Small insertion-ordered LRU used for process-lifetime memo tables. Same
165
+ * bounded-`Map` surface as {@link BoundedFifoMap}; the only two differences
166
+ * are overridden below.
167
+ */
168
+ export class BoundedLruCache extends BoundedFifoMap {
169
+ /**
170
+ * Read and promote to most-recently-used.
171
+ *
172
+ * A stored `undefined` value is NOT promoted — the hit is indistinguishable
173
+ * from a miss on this signature, and pre-#2442 `BoundedLruCache` behaved
174
+ * the same way. No caller stores `undefined`; the note exists so a future
175
+ * one does not assume otherwise.
176
+ */
177
+ get(key) {
178
+ const value = this.entries.get(key);
179
+ if (value !== undefined) {
180
+ this.entries.delete(key);
181
+ this.entries.set(key, value);
182
+ }
183
+ return value;
184
+ }
185
+ /** Insert or update `key`, moving it to the most-recently-used position. */
186
+ set(key, value) {
187
+ this.entries.delete(key);
188
+ return super.set(key, value);
189
+ }
190
+ }
@@ -10,9 +10,9 @@
10
10
  * get the bounding right. This module is the intersection of those four, not
11
11
  * a speculative framework. Three of its four options come straight from those
12
12
  * sites. The fourth, `capPerTurn`, expresses #1733's per-turn bound
13
- * structurally but has no caller yet: that site's bound is its call cadence,
14
- * and pinning a cap there would red its own wiring tests. It ships tested and
15
- * mutation-proofed, awaiting its first site.
13
+ * structurally and now caps re-raised auxiliary coverage-gap detail rows
14
+ * (#2356), while the aggregate count and ledger retain bounded latest
15
+ * identities plus the dropped count.
16
16
  *
17
17
  * Three rules the helper makes structural instead of prose:
18
18
  *
@@ -24,10 +24,9 @@
24
24
  * that must re-arm at `session_start` and will eventually be forgotten
25
25
  * (the #1266–#1625 defect class).
26
26
  * 2. **A per-turn cap names its turn.** `capPerTurn` carries the caller's
27
- * `turnIndex` in the same object as the limit, so there is no separate
28
- * reset to wire and forget: the counters clear when the observed turn
29
- * changes. This is the same lazy clear-on-transition shape as
30
- * `noteImportResolutionMemoTurn` (`clients/blocker-freshness.ts`).
27
+ * `turnIndex` in the same object as the limit. A bounded 64-turn window
28
+ * keeps overlapping async turns independent; results older than that window
29
+ * fail closed. Session start clears the window when numbering restarts.
31
30
  * 3. **Identity always survives.** Every emitted record carries the
32
31
  * discriminating identity in `filePath` and in `metadata.identity`, so
33
32
  * aggregation can still answer "which file, which method, which record is
@@ -39,7 +38,7 @@
39
38
  * stated reason for staying raw.
40
39
  *
41
40
  * Nothing here ever throws. Telemetry must not perturb the path it observes —
42
- * every one of the four migrated sites had already decided its outcome before
41
+ * each caller has already decided its outcome before
43
42
  * calling in.
44
43
  */
45
44
  import { incrementDegradationCount } from "./degradation-ledger.js";
@@ -51,6 +50,13 @@ import { logLatency } from "./latency-logger.js";
51
50
  * must not also be written by a raw `logLatency` call somewhere else.
52
51
  */
53
52
  export const BOUNDED_TELEMETRY_PHASES = [
53
+ /**
54
+ * #2467: a demand for the analyzer bootstrap clients was not served and
55
+ * the caller proceeded without them. Rising-edge per demand reason, with
56
+ * the ledger holding the exact count — a wedged or missing analyzer graph
57
+ * must not turn every tool call into a log line.
58
+ */
59
+ "bootstrap_clients_unavailable",
54
60
  /** #1705: a deferred-format record whose origin worktree never claimed it. */
55
61
  "agent_end_deferred_format_orphan_origin_mismatch",
56
62
  /** #1713: a `textDocument`/`workspace` diagnostic pull abandoned at budget. */
@@ -85,6 +91,16 @@ export const BOUNDED_TELEMETRY_PHASES = [
85
91
  * exact count.
86
92
  */
87
93
  "lsp_warm_client_missing",
94
+ /** #2052: a file's nearest LSP root is outside the session ceiling. */
95
+ "lsp_capability_skip",
96
+ /**
97
+ * #2044: failed-first test state was retired after a confirmed missing path,
98
+ * retained because the filesystem verdict was indeterminate, or evicted at
99
+ * the state cap. Selection checks and detailed rows are both capped per turn.
100
+ */
101
+ "test_runner_failed_target_state",
102
+ /** #2366: bounded lifecycle records for automatic test-result delivery. */
103
+ "test_runner_delivery",
88
104
  /**
89
105
  * #1723: an event-loop block at or above the floor. Not a degradation, so
90
106
  * no ledger kind; bounded by call cadence (one `turn_end` runs it once per
@@ -97,6 +113,63 @@ export const BOUNDED_TELEMETRY_PHASES = [
97
113
  * queue of them, so it is rising-edge per event name.
98
114
  */
99
115
  "session_event_stale_ctx_skip",
116
+ /**
117
+ * #2007: a worktree-mutating git command declined because another live
118
+ * session shares this dirty checkout. Rising edge per checkout root; the
119
+ * ledger keeps the exact repeat count.
120
+ */
121
+ "shared_checkout_switch_blocked",
122
+ /**
123
+ * #2007: `git status` could not report the working-tree state, so the same
124
+ * command was declined on an UNKNOWN rather than assumed clean.
125
+ */
126
+ "shared_checkout_probe_failed",
127
+ /** #2356: a notify-stall auxiliary remained uncovered after its bounded
128
+ * replacement window. Detailed rows are capped per turn; the ledger and
129
+ * aggregate turn-end row retain the complete count and identity. */
130
+ "lsp_scanner_coverage_gap",
131
+ /** #2358: a busy CPU discriminator deferred notify-stall teardown. */
132
+ "lsp_notify_stall_cpu_busy",
133
+ /**
134
+ * #2430: an observational pre/post snapshot was declined or cut short — the
135
+ * per-turn budget was already spent, the capture timed out, or the turn was
136
+ * aborted. Every unclassified tool call in a busy turn can reach this, so
137
+ * it is capped per turn with the ledger holding the exact count.
138
+ */
139
+ "observed_mutation_budget_exhausted",
140
+ /**
141
+ * #2430: the `agent_settled` tracked-file sweep could not complete within
142
+ * its bound. Once per settle at most, capped for the same reason.
143
+ */
144
+ "observed_sweep_skipped_budget",
145
+ /**
146
+ * #2430: a tracked file's size/mtime moved but no hashed baseline could
147
+ * confirm the bytes actually changed — too large for the sweep's read
148
+ * budget, or never hashed. Named rather than replayed, because a `touch`
149
+ * moves mtime without moving a byte (#2449 review round 2, F7). Bounded
150
+ * per turn: one settle can find many such files at once.
151
+ */
152
+ "observed_sweep_unverifiable",
153
+ /**
154
+ * #2430: an armed observation's universe was TRUNCATED because the tool
155
+ * named a directory with more entries than the net may watch. Bounded per
156
+ * turn with the ledger holding the exact count, and the identity is the
157
+ * tool name so the record answers WHICH tool is being watched partially
158
+ * (#2449 review round 3).
159
+ */
160
+ "observed_target_dir_capped",
161
+ /**
162
+ * #2430: the "pi-lens wrote these bytes itself" set hit
163
+ * `OBSERVED_HANDLED_MAX` and dropped its oldest entry. The dropped file is
164
+ * NAMED, because the eviction silently reintroduces the exact defect
165
+ * #2449 round 3 (S5) fixed: the ledger still holds the pre-drain bytes
166
+ * while the only record that those bytes were ours is gone, so the next
167
+ * settled sweep replays pi-lens's own formatter output as third-party
168
+ * drift. A cap that drops a mark has to say which one (catalog shape 10);
169
+ * capped per turn because a turn that overflows the set overflows it many
170
+ * times (#2449 review round 4, S2).
171
+ */
172
+ "observed_handled_evicted",
100
173
  ];
101
174
  const REGISTERED_PHASES = new Set(BOUNDED_TELEMETRY_PHASES);
102
175
  /** Report whether a phase name is in the registry (used by the sweep). */
@@ -104,14 +177,15 @@ export function isBoundedTelemetryPhase(phase) {
104
177
  return REGISTERED_PHASES.has(phase);
105
178
  }
106
179
  /**
107
- * Per-turn admission counters, keyed by phase. Cleared lazily when a call
108
- * observes a turn index different from the one the counters were built for,
109
- * and eagerly by `resetBoundedTelemetry` at `session_start` (a new session
110
- * restarts turn numbering at 0, so the lazy check alone could carry a stale
111
- * count across a session boundary that happened to land on the same index).
180
+ * Per-turn admission counters, keyed by turn and phase. Multiple turns remain
181
+ * live because async work can finish T1 after T2 has already emitted. The map
182
+ * retains a bounded recent window; work older than that window fails closed
183
+ * rather than reopening an exhausted budget. Session start clears all state.
112
184
  */
185
+ const MAX_TRACKED_TURN_CAPS = 64;
113
186
  let turnCounts = new Map();
114
- let countedTurnIndex;
187
+ let latestCountedTurnIndex;
188
+ let retiredThroughTurnIndex = Number.NEGATIVE_INFINITY;
115
189
  /**
116
190
  * Decide whether this occurrence earns a detailed record, and count it in the
117
191
  * ledger either way. Returns `true` when the caller should write the record.
@@ -180,14 +254,25 @@ export function emitBounded(phase, identity, payload, options = {}) {
180
254
  function admitAgainstTurnCap(phase, cap) {
181
255
  if (cap === undefined)
182
256
  return true;
183
- if (countedTurnIndex !== cap.turnIndex) {
184
- turnCounts = new Map();
185
- countedTurnIndex = cap.turnIndex;
257
+ if (cap.turnIndex <= retiredThroughTurnIndex)
258
+ return false;
259
+ let counts = turnCounts.get(cap.turnIndex);
260
+ if (!counts) {
261
+ if (turnCounts.size >= MAX_TRACKED_TURN_CAPS) {
262
+ const oldestTrackedTurn = Math.min(...turnCounts.keys());
263
+ if (cap.turnIndex < oldestTrackedTurn)
264
+ return false;
265
+ turnCounts.delete(oldestTrackedTurn);
266
+ retiredThroughTurnIndex = Math.max(retiredThroughTurnIndex, oldestTrackedTurn);
267
+ }
268
+ counts = new Map();
269
+ turnCounts.set(cap.turnIndex, counts);
186
270
  }
187
- const used = turnCounts.get(phase) ?? 0;
271
+ latestCountedTurnIndex = Math.max(latestCountedTurnIndex ?? cap.turnIndex, cap.turnIndex);
272
+ const used = counts.get(phase) ?? 0;
188
273
  if (used >= cap.limit)
189
274
  return false;
190
- turnCounts.set(phase, used + 1);
275
+ counts.set(phase, used + 1);
191
276
  return true;
192
277
  }
193
278
  /**
@@ -198,9 +283,16 @@ function admitAgainstTurnCap(phase, cap) {
198
283
  */
199
284
  export function resetBoundedTelemetry() {
200
285
  turnCounts = new Map();
201
- countedTurnIndex = undefined;
286
+ latestCountedTurnIndex = undefined;
287
+ retiredThroughTurnIndex = Number.NEGATIVE_INFINITY;
288
+ }
289
+ /** Test-only: one turn's live admission count for a phase. */
290
+ export function _boundedTurnCountForTest(phase, turnIndex = latestCountedTurnIndex) {
291
+ if (turnIndex === undefined)
292
+ return 0;
293
+ return turnCounts.get(turnIndex)?.get(phase) ?? 0;
202
294
  }
203
- /** Test-only: the live per-turn admission count for a phase. */
204
- export function _boundedTurnCountForTest(phase) {
205
- return turnCounts.get(phase) ?? 0;
295
+ /** Test-only: number of turn buckets retained by the bounded counter. */
296
+ export function _boundedTrackedTurnsForTest() {
297
+ return turnCounts.size;
206
298
  }
@@ -0,0 +1,95 @@
1
+ /**
2
+ * #1775: `sessionstart.log` recorded no build identity — no commit hash, no
3
+ * build timestamp. Dogfood forensics ("does this session include PR #1727?")
4
+ * had to reconstruct that by hand: run `git merge-base --is-ancestor` in the
5
+ * serving checkout and compare `dist/` mtimes against the session-start time.
6
+ *
7
+ * The running extension can be a different checkout than the repo an
8
+ * investigator is looking at, so identity is derived from the RUNNING
9
+ * build's own files — `getPackageRoot` (already used by grammar-source.ts to
10
+ * find pi-lens's own assets under both the unbundled dev layout and a
11
+ * packaged install) plus the mtime of the very entry file that is executing
12
+ * right now — never from `process.cwd()` and never assumed to be a git
13
+ * checkout.
14
+ *
15
+ * Commit resolution reuses `review-graph/git-identity.ts`'s
16
+ * `resolveGitIdentity` — a PURE filesystem read, no `git` subprocess. This
17
+ * runs inside the `session_start` handler, ahead of the #473
18
+ * concurrent-secondary classification, so every in-process subagent pays
19
+ * whatever this costs; a synchronous `git rev-parse`/`git status` spawn pair
20
+ * (fix round 1's approach, reverted here) measured 150-300ms typical and up
21
+ * to ~10s worst case on two 5s timeouts. `resolveGitIdentity` is a handful of
22
+ * `fs.readFileSync` calls, LRU-cached per process, so this path never spawns.
23
+ *
24
+ * The dirty working-tree flag genuinely needs `git status`, which has no
25
+ * spawn-free equivalent — so it is dropped from this record rather than
26
+ * reintroducing a spawn on the session-start hot path (refs #1775 followup
27
+ * comment: dirty flag deferred, commit + entryMtime + version shipped).
28
+ */
29
+ import * as fs from "node:fs";
30
+ import * as path from "node:path";
31
+ import { fileURLToPath } from "node:url";
32
+ import { isTestMode } from "./env-utils.js";
33
+ import { getPackageRoot } from "./package-root.js";
34
+ import { normalizeFilePath } from "./path-utils.js";
35
+ import { resolveGitIdentity } from "./review-graph/git-identity.js";
36
+ /**
37
+ * A `.git` found by walking up from `root` only counts as owning `root` when
38
+ * its resolved worktree top-level IS `root` — not merely an ancestor of it.
39
+ * Without this check, a packaged install nested anywhere under an unrelated
40
+ * repo (a dotfiles checkout in $HOME is the common case) would confidently
41
+ * report that repo's HEAD as the running build's identity.
42
+ */
43
+ function resolveOwnedCommit(root) {
44
+ const identity = resolveGitIdentity(root);
45
+ if (!identity)
46
+ return "unknown";
47
+ const normalizedRoot = normalizeFilePath(path.resolve(root));
48
+ if (identity.worktreeRoot !== normalizedRoot)
49
+ return "unknown";
50
+ return identity.headCommit.slice(0, 8);
51
+ }
52
+ function readPackageVersion(root) {
53
+ try {
54
+ const raw = fs.readFileSync(path.join(root, "package.json"), "utf-8");
55
+ const pkg = JSON.parse(raw);
56
+ return pkg.version ?? "unknown";
57
+ }
58
+ catch {
59
+ return "unknown";
60
+ }
61
+ }
62
+ function statEntryMtime(entryImportMetaUrl) {
63
+ try {
64
+ return fs.statSync(fileURLToPath(entryImportMetaUrl)).mtime.toISOString();
65
+ }
66
+ catch {
67
+ // Entry path unresolvable (e.g. a non-file:// URL) — "unknown" rather
68
+ // than throw out of a session-start observability line.
69
+ return "unknown";
70
+ }
71
+ }
72
+ /**
73
+ * Resolve the identity of the build currently running — pure filesystem
74
+ * reads, no subprocess. `entryImportMetaUrl` must be the ENTRY module's own
75
+ * `import.meta.url` (index.ts) — passing a different module's URL would
76
+ * report that module's mtime instead of the running build's.
77
+ *
78
+ * Returns `undefined` inside the test runner: nothing computed, nothing
79
+ * logged (`sessionstart-logger`'s writer is a no-op there too), so a caller
80
+ * on the session-start hot path never pays this cost during tests.
81
+ */
82
+ export function getBuildIdentity(entryImportMetaUrl) {
83
+ if (isTestMode())
84
+ return undefined;
85
+ const root = getPackageRoot(entryImportMetaUrl);
86
+ return {
87
+ commit: resolveOwnedCommit(root),
88
+ entryMtime: statEntryMtime(entryImportMetaUrl),
89
+ version: readPackageVersion(root),
90
+ };
91
+ }
92
+ /** One bounded, human-readable line for sessionstart.log's free-text writer. */
93
+ export function formatBuildIdentity(identity) {
94
+ return `session_start: build identity — commit=${identity.commit} entryMtime=${identity.entryMtime} version=${identity.version}`;
95
+ }
@@ -0,0 +1,113 @@
1
+ /**
2
+ * Shared health classification + degradation reporting for a bundled
3
+ * resource directory read DIRECTLY by pi-lens (never handed to an external
4
+ * spawned tool as a `--config`/`--rule-dir` argument — that class already
5
+ * fails loudly through the tool's own spawn-failure path).
6
+ *
7
+ * #2626 fixed this shape for `clients/skills-resolver.ts`'s `skills/`
8
+ * directory: `resolvePackagePath(import.meta.url, …)` walks up from the
9
+ * loaded entry file to the nearest `package.json`, which is correct for both
10
+ * the source and compiled `dist/` layouts, but goes wrong SILENTLY, not
11
+ * loudly, when that walk lands somewhere with no resource dir beside it —
12
+ * the bundled dir missing from an installed package, or the entry file
13
+ * copied out of the package tree by a managed extension cache so the
14
+ * nearest `package.json` is the cache's own. #2636 is the class sweep:
15
+ * `clients/ast-grep-client.ts`'s `rules` fallback and the bundled
16
+ * tree-sitter-queries root (read identically by `clients/cache/rule-cache.ts`
17
+ * and `clients/tree-sitter-query-loader.ts`) have the exact same gap.
18
+ *
19
+ * Both callers reuse this ONE classify+report pair rather than each
20
+ * hand-rolling the ENOENT/EACCES distinction (#2626 review F4) a second and
21
+ * third time.
22
+ */
23
+ import * as fs from "node:fs";
24
+ import { incrementDegradationCount, } from "./degradation-ledger.js";
25
+ import { notifyUserDegradation } from "./user-notify.js";
26
+ /**
27
+ * Classify `dir`: does it exist, is it readable, and does it hold anything?
28
+ * Fail-closed and never throws. Distinguishes `ENOENT` (the common
29
+ * managed-cache-relocation case) from any OTHER `readdirSync` error
30
+ * (`EACCES` and friends) — an unreadable directory might genuinely hold a
31
+ * real resource pi-lens simply cannot read, so the honest report is "cannot
32
+ * read it", never "nothing is there" (#2626 review F4).
33
+ *
34
+ * `countEntries` lets a caller supply its OWN notion of "found" — e.g.
35
+ * `skills-resolver.ts` counts pi-loadable skill entry points via
36
+ * `scanEntriesForSkills`, not a bare directory listing — while still sharing
37
+ * this function's ENOENT/EACCES/empty classification shell (#2636 review F4:
38
+ * that shell was duplicated character-for-character between the two modules
39
+ * before this parameter existed). The default counts plain directory
40
+ * entries, which is what every OTHER caller (ast-grep's rules dir, the
41
+ * bundled tree-sitter-queries root) actually wants.
42
+ */
43
+ export function classifyBundledResourceDir(dir, countEntries = (d) => fs.readdirSync(d).length) {
44
+ let entryCount;
45
+ try {
46
+ entryCount = countEntries(dir);
47
+ }
48
+ catch (error) {
49
+ const fsErrorCode = error?.code ?? "UNKNOWN";
50
+ if (fsErrorCode === "ENOENT")
51
+ return { status: "absent" };
52
+ return { status: "unreadable", fsErrorCode };
53
+ }
54
+ return entryCount > 0
55
+ ? { status: "healthy", entryCount }
56
+ : { status: "empty" };
57
+ }
58
+ /**
59
+ * Render `health` as a reason string. `emptyDescription` lets a caller name
60
+ * what "nothing found" actually means for its own resource (skills' loader
61
+ * cares about `SKILL.md`, not bare directory entries); every other status's
62
+ * prose is shared verbatim, since ENOENT/EACCES mean the exact same thing
63
+ * regardless of which resource directory hit them.
64
+ */
65
+ export function describeBundledResourceHealth(health, dir, emptyDescription = `${dir} exists but holds nothing`) {
66
+ switch (health.status) {
67
+ case "absent":
68
+ return `no such directory: ${dir}`;
69
+ case "unreadable":
70
+ return `cannot read ${dir} (${health.fsErrorCode})`;
71
+ case "empty":
72
+ return emptyDescription;
73
+ default:
74
+ // unreachable for the "healthy" case — callers only report unhealthy ones.
75
+ return `${dir}: unexpected health status`;
76
+ }
77
+ }
78
+ /**
79
+ * Record ONE bounded `kind` degradation plus one `notifyUserDegradation`
80
+ * warning per (kind, dir) per session, when `health` is not healthy.
81
+ * Pure no-op when healthy. `incrementDegradationCount`'s own return value
82
+ * (not a hand-rolled Set) gates the notify to the rising edge, so a caller
83
+ * invoked on every dispatch (e.g. `RuleCache`'s constructor) can call this
84
+ * unconditionally without spamming the notify surface.
85
+ *
86
+ * `reasonOverride` lets a caller build a richer reason (skills-resolver.ts
87
+ * appends the entry file's directory) while still sharing the
88
+ * increment/notify mechanics below; the default is this module's own
89
+ * `describeBundledResourceHealth`.
90
+ *
91
+ * `notifyMessage` lets a caller replace the DEFAULT `pi-lens: <label>
92
+ * unavailable — <reason>.` sentence with its own finished wording (#2636
93
+ * review round 2, F1): `skills-resolver.ts`'s shipped notify text ("registers
94
+ * zero skills") is user-visible product text #2626 chose deliberately — the
95
+ * SYMPTOM the notification exists to surface — and folding onto this shared
96
+ * helper must never silently reword a string a maintainer already tuned.
97
+ */
98
+ export function reportBundledResourceDirHealth(kind, dir, health, label, reasonOverride, notifyMessage) {
99
+ if (health.status === "healthy")
100
+ return;
101
+ const reason = reasonOverride ?? describeBundledResourceHealth(health, dir);
102
+ const isFirstOccurrence = incrementDegradationCount({
103
+ kind,
104
+ subject: dir,
105
+ reason,
106
+ metadata: health.status === "unreadable"
107
+ ? { fsErrorCode: health.fsErrorCode }
108
+ : undefined,
109
+ });
110
+ if (isFirstOccurrence) {
111
+ notifyUserDegradation(notifyMessage ?? `pi-lens: ${label} unavailable — ${reason}.`, "warning");
112
+ }
113
+ }
@@ -44,11 +44,11 @@
44
44
  */
45
45
  import * as path from "node:path";
46
46
  import { isTestMode } from "./env-utils.js";
47
- import { getGlobalPiLensDir } from "./file-utils.js";
47
+ import { getGlobalPiLensLogDir } from "./probe-home-state.js";
48
48
  import { createNdjsonLogger } from "./ndjson-logger.js";
49
49
  import { getMaxLogSizeMB } from "./log-cleanup.js";
50
50
  import { logLatency } from "./latency-logger.js";
51
- const BUS_EVENTS_LOG_FILE = path.join(getGlobalPiLensDir(), "bus-events.log");
51
+ const BUS_EVENTS_LOG_FILE = path.join(getGlobalPiLensLogDir(), "bus-events.log");
52
52
  const writer = createNdjsonLogger({
53
53
  filePath: BUS_EVENTS_LOG_FILE,
54
54
  maxBytes: getMaxLogSizeMB() * 1024 * 1024,
@@ -23,6 +23,7 @@ import { appendRecentTouches } from "./recent-touches.js";
23
23
  import { createLiveBusEmitter, recordStaleBusFailure, resolveLiveBusEmitter, } from "./live-bus-emitter.js";
24
24
  export const BUS_FILES_TOUCHED_EVENT = "pilens:files:touched";
25
25
  export const BUS_FILES_TOUCHED_VERSION = 1;
26
+ const BUS_LOG_PATH_CAP = 64;
26
27
  const liveEmitter = createLiveBusEmitter();
27
28
  let hasLoggedFailure = false;
28
29
  // Log-once-per-process guards for the two structural-no-op outcomes below
@@ -148,6 +149,8 @@ export function publishFilesTouched(args) {
148
149
  cwd: payload.cwd,
149
150
  reason: args.reason,
150
151
  fileCount: payload.paths.length,
152
+ writer: "pi-lens",
153
+ paths: payload.paths.slice(0, BUS_LOG_PATH_CAP),
151
154
  });
152
155
  }
153
156
  catch (err) {